home
Splitter
Bars
Part
II Making a Reusable Windows Class
Ernest Murphy ernie@surfree.com
October 14 2000
Get the source code
here.
Abstract:
------------------------------------------------------------------------------------------
Splitter bars are a distinctive visual
control element that help organize a large amount of information in a window.
We now build on the techniques learned in Part I and put them in a dynamic
link library. This encapsulates the code and allows it to be reused in
any project.
For simplicity, the splitter class
described in the code will only support a vertical splitter bar type. This
is to reduce the amount of code and checking our tutorial must explain.
In the near future a fully functional control will be released for your
use. Source code will be included for the curious. There are currently
no plans to document this class beyond application reuse information and
this source code.
Introduction:
------------------------------------------------------------------------------------------
The dynamic link library can be a
container for reusable code that may be shared across many applications.
The drawback is a serious versoning problem if the library is ever updated,
but the dll is so useful most developers will overlook this problem.
An application using our splitter
library needs to load it before it can be used. There are two basic loading
methods:
| • |
Implicit
loading: |
This is accomplished by a little help from
the library and help
from the linker. By adding a reference
to a dummy export of
our library, we force the linker
to compile our .exe so it loads
the library at the beginning of runtime.
The library remains in
memory for the duration of the application. |
| • |
Explicit loading: |
The LoadLibrary and FreeLibrary API methods
are used to load
and unload our library only as it
is needed. Great care must be
taken to have the library available
when it is in use. Unloading
the library while a control
instance is still alive will produce
GPFs as the class tries to access
the winproc inside the missing
library. |
An application using our library must
know how to use it, to this end both an include file and an object library
file (.inc and .lib) should be provided.
Registering the new Window Class
------------------------------------------------------------------------------------------
Let us first show how we build the
library. Every dll contains an export to be run as it is loaded. It may
be given various names, let us use DllMain.
DllMain is passed several parameters,
the important ones to us being the instance handle of the library itself,
and a "reason" code. There are various events DllMain is called, and the
"reason" informs us which event we are responding to.
When the library is first loaded,
the reason is set to DLL_PROCESS_ATTATCH. This is the point where we register
our new splitter class. Immediately before the library is unloaded, DllMain
is called with reason DLL_PROCESS_DETATCH. This is our chance to unregister
the splitter class.
The splitter window class is a very
simple type just requiring a few default items. These include the class
name, the cursor handle, the back color, application reuse of course our
window message procedure. The class name will define for our client applications
what to call our new class. The cursor handle is changed to the system
standard IDC_SIZEWE ( <--> ), that is the sole visual element to indicate
to the user the splitter is there. The back color is set to be COLOR_BUTTONFACE,
a typical parent background color.
Another important (and specific to
controls in a dll) is the style parameter. This should be CS_GLOBALCLASS
for the client application to be able to instance our class. (thanks
to Iczelion for pointing this out to me)
Thus, our DllMain code will be as
so:
| |
DllMain proc hInst:DWORD,
reason:DWORD, reserved1:DWORD
LOCAL wc:WNDCLASSEX
.IF reason==DLL_PROCESS_ATTACH
mov eax, hInst
mov hInstance,eax
; register a simple window as our splitter bar child window
mov wc.cbSize, SIZEOF WNDCLASSEX
mov wc.style, CS_GLOBALCLASS
; thanks Izcelion :-)
mov wc.lpfnWndProc, OFFSET SplitterProc
mov wc.cbWndExtra, SIZEOF DWORD ; we need a pointer in here
mov eax, hInstance
mov wc.hInstance, eax
mov wc.hbrBackground, COLOR_BTNFACE + 1
mov wc.lpszMenuName, NULL
mov wc.lpszClassName, OFFSET szVSplitterCtrl
xor eax, eax ; mov eax, NULL
mov wc.hIcon, eax
mov wc.hIconSm, eax
invoke LoadCursor, NULL, IDC_SIZEWE
; <--> cursor
mov wc.hCursor, eax
invoke RegisterClassEx, addr wc
.ELSEIF reason == DLL_PROCESS_DETACH
; clean up is just unregistering our dll
invoke UnregisterClass, OFFSET szVSplitterCtrl, hInstance
.endif
mov eax, TRUE ; signal SUCCESS
ret
DllMain Endp |
Dummy Export Function
------------------------------------------------------------------------------------------
In order to get the linker load the
dll for us, we need to export a dummy procedure. When the linker catches
the reference to this export in the client code, it will provide information
to the client .exe that will load the dll as the client starts.
Note this function does not need to
be executed, it just has to exist in the client. Perhaps the best place
for it is immediately after the ExitProcess function.
| |
InitSplitterCtrl
PROC
; dummy proc. Referencing this instruction anywhere in the code
; will load the lib and register our class
xor eax, eax ; return OK
ret
InitSplitterCtrl
ENDP |
Instance Data
------------------------------------------------------------------------------------------
Naming the windows class as CLASS
is a good term, as one should think of windows in object terms. Each instance
of a class takes on a life of it's own, and may well need some custom information.
The splitter control needs such information.
Each splitter must keep track of the
parent window it is over, and also track the windows to either side. Each
side window is also allowed to set it's minimum width. This data must be
somehow associated with each class instance.
This data will be contained in a SplitterData
structure like so:
| |
SplitterData
STRUCT
m_hWndParent DWORD
0
m_hWndLeft DWORD
0
m_hWndRight DWORD
0
m_RIGHT_MIN DWORD
0
m_LEFT_MIN DWORD
0
SplitterData
ENDS |
The WINCLASSEX structure has a bit
of room to add data to a window class instance though the cbWndExtra parameter.
MS recommends using as small a structure here as possible because it takes
up very valuable GDI resources (64K max). Thus, we only allocate a single
dword to use as a pointer to our SplitterData. Upon creation, each splitter
creates a bit of heap for SplitterData, and saves a reference to this in
the class instance.
The Message Loop
------------------------------------------------------------------------------------------
The main assistance our dll gives
us is providing the message loop for this control. It processes the following
messages:
|
MESSAGE
|
DESCRIPTION
|
WM_CREATE |
1. Create the heap
for SplitterData
2. Initialize SplitterData
3. Copy the parent's
backcolor
to the Splitter. |
WM_DESTROY |
Free the heap created
in WM_CREATE |
WM_LBUTTONDOWN |
Capture the mouse |
WM_LBUTTONUP |
Release the mouse |
WM_MOUSEMOVE |
Move the windows
about |
WM_SET_PANE_HWND |
Client calls SendMessage
just after the control is
created to
set the side window handles. |
WM_SET_BORDER_MIN |
Client calls SendMessage
just after the control is
created to
set the minimum width the side window may be
set to. |
If you cannot find the last two messages
inside MSDN, that is because I just made them up. They are WM_USER messages,
used to pass values to the splitter. These messages are defined as follows:
|
WM_SET_PANE_HWND
An application sends
a WM_SET_PANE_HWND message to inform the Splitter control the window handles
of the adjacent windows it is supposed to split.
WM_SET_PANE_HWND
wParam = (WPARAM)
hWndLeft; // Window handle of left side window
lParam = (LPARAM)
Right; // Window handle of right side window
WM_SET_BORDER_MIN
An application sends
a WM_SET_BORDER_MIN message to set the minimum width of the adjacent windows
it may set. Value is expressed in pixels, and includes any border of the
side windows
WM_SET_PANE_HWND
wParam = (WPARAM)
iLeftMin; // Min width (in pixels) of left side
lParam = (LPARAM)
iRightMin; // Min width (in pixels) of right side window
|
Here is a portion of the message loop.
Some of the code inside the WM_MOUSEMOVE handler has been omitted for clarity
since it is very large. Actually, MOUSEMOVE is quite simple, but it does
perform a large number of simple additions and subtractions, and the code
to calculate the move for the side windows and check minimum widths was
omitted here.
|
SplitterProc proc
hWnd:DWORD, uMsg:UINT, wParam:DWORD, lParam:DWORD
LOCAL SplitterRect:RECT, LeftRect:RECT, RightRect:RECT
LOCAL ParentArea:AREA, SplitterArea:AREA
LOCAL LeftArea:AREA, RightArea:AREA
LOCAL MoveX:DWORD, pData:DWORD,
ptClient:POINT
LOCAL hWndParent:DWORD, hWndLEFT:DWORD, hWndRIGHT:DWORD
LOCAL RIGHT_MIN:DWORD, LEFT_MIN:DWORD
.IF uMsg==WM_CREATE
; create a heap for THIS instance of the class
invoke GetProcessHeap
invoke HeapAlloc, eax, HEAP_ZERO_MEMORY, SIZEOF SplitterData
mov pData, eax
; store this reference in our class data area
invoke SetWindowLong, hWnd, GWL_USERDATA, pData
invoke GetParent, hWnd
mov hWndParent, eax
mov ecx, pData
.IF ecx ; then we have a valid heap buffer
; set default values
mov (SplitterData PTR [ecx]).m_hWndParent, eax
mov (SplitterData PTR [ecx]).m_hWndLeft, NULL
mov (SplitterData PTR [ecx]).m_hWndRight, NULL
mov (SplitterData PTR [ecx]).m_RIGHT_MIN, 25
mov (SplitterData PTR [ecx]).m_LEFT_MIN, 25
.ENDIF
invoke GetBkColor, hWndParent ; make our splitter the same
invoke SetBkColor, hWnd, eax ; color as the parent
.ELSEIF uMsg==WM_DESTROY
; free our heap
invoke GetProcessHeap
mov MoveX, eax ; borrow a variable for this
invoke GetWindowLong, hWnd, GWL_USERDATA
.IF eax
invoke HeapFree, MoveX, NULL, eax
.ENDIF
.ELSEIF uMsg==WM_LBUTTONDOWN
; get the hwnds of all windows concerned
invoke GetWindowLong, hWnd, GWL_USERDATA
mov pData, eax
mov ecx, (SplitterData PTR [eax]).m_hWndParent
mov hWndParent, ecx
movsx eax, (packedDW PTR [lParam]).loword ; get lP back
mov SplitterX0, eax ; save Y
invoke SetCapture, hWnd
.ELSEIF uMsg==WM_LBUTTONUP
invoke ReleaseCapture
.ELSEIF uMsg==WM_MOUSEMOVE
.IF (wParam && MK_LBUTTON)
; get basic defining values
movsx eax,(packedDW PTR [lParam]).loword
mov MoveX, eax
mov eax, SplitterX0
sub MoveX, eax ; MoveX = lParam.loword -
SplitterX0
.IF MoveX != 0
; get the hwnds of all windows concerned
invoke GetWindowLong, hWnd, GWL_USERDATA
mov pData, eax
mov ecx, (SplitterData PTR [eax]).m_hWndParent
mov hWndParent, ecx
mov ecx, (SplitterData PTR [eax]).m_hWndLeft
mov hWndLEFT, ecx
mov ecx, (SplitterData PTR [eax]).m_hWndRight
mov hWndRIGHT, ecx
mov ecx, (SplitterData PTR [eax]).m_RIGHT_MIN
mov RIGHT_MIN, ecx
mov ecx, (SplitterData PTR [eax]).m_LEFT_MIN
mov LEFT_MIN, ecx
; get bounding rectangles of all windows
invoke GetWindowRect, hWnd, ADDR SplitterRect
invoke GetWindowRect, hWndLEFT, ADDR LeftRect
invoke GetWindowRect, hWndRIGHT, ADDR RightRect
mov ptClient.x, NULL
mov ptClient.y, NULL
invoke ClientToScreen, hWndParent, ADDR ptClient
Recompute:
; convert splitter co-ords
; convert SplitterRect co-ords to SplitterArea
; SplitterArea.wwidth =
;
SplitterRect.right - SplitterRect.left
mov eax, SplitterRect.right
mov SplitterArea.wwidth, eax
mov eax, SplitterRect.left
sub SplitterArea.wwidth, eax
; SplitterArea.hheight =
;
SplitterRect.bottom - SplitterRect.top
mov eax, SplitterRect.bottom
mov SplitterArea.hheight, eax
mov eax, SplitterRect.top
sub SplitterArea.hheight, eax
; SplitterArea.left =
;
SplitterRect.left - ptClient.x
mov eax, SplitterRect.left
mov SplitterArea.left, eax
mov eax, ptClient.x
sub SplitterArea.left, eax
; SplitterArea.top =
;
SplitterRect.top - ptClient.y
mov eax, SplitterRect.top
mov SplitterArea.top, eax
mov eax, ptClient.y
sub SplitterArea.top, eax
; compute the move for this window
mov eax, MoveX
add SplitterArea.left, eax
{ code omitted for clarity }
{ see source code for full listing }
; now move the splitter
invoke MoveWindow, hWnd,
SplitterArea.left, SplitterArea.top,
SplitterArea.wwidth, SplitterArea.hheight, FALSE
; now move the EDITLEFT window
invoke MoveWindow, hWndLEFT,
LeftArea.left,LeftArea.top,
LeftArea.wwidth, LeftArea.hheight, FALSE
; now move the EDITRIGHT window
invoke MoveWindow, hWndRIGHT,
RightArea.left, RightArea.top,
RightArea.wwidth, RightArea.hheight, FALSE
; now repaint the main window
invoke InvalidateRgn, hWndParent, NULL, FALSE
.ENDIF
.ENDIF
.ELSEIF uMsg == WM_SET_PANES_HWND
; store this data in our object area
invoke GetWindowLong, hWnd, GWL_USERDATA
mov ecx, wParam
mov (SplitterData PTR [eax]).m_hWndLeft, ecx
mov ecx, lParam
mov (SplitterData PTR [eax]).m_hWndRight, ecx
.ELSEIF uMsg == WM_SET_BORDERS_MIN
; store this data in our object area
invoke GetWindowLong, hWnd, GWL_USERDATA
mov ecx, lParam
mov (SplitterData PTR [eax]).m_RIGHT_MIN, ecx
mov ecx, wParam
mov (SplitterData PTR [eax]).m_LEFT_MIN, ecx
.ELSE
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
ret
.ENDIF
xor eax,eax
ret
SplitterProc ENDP |
Assembling the Dynamic Link Library
------------------------------------------------------------------------------------------
For our library, we need to define
our custom window messages. These are defined inside an include file so
that any code using our library will play with the same constants.
Also, the dummy exported function is prototyped. Here is the whole include
file:
|
;--------------------------------------------------------------------
; include fle for
the Splitter Control Dynamic Link Library
;
; message
wparam
lparam
;------------------
------------------ ---------------------
; WM_SET_PANES_HWND
hWnd of left pane hWnd of right pane
;
; WM_SET_BORDERS_MIN
min pixle width min pixle width
;
of left pane of right pane
;
;
; these messages
must be called to set the control's properties
;
;
;--------------------------------------------------------------------
InitSplitterCtrl
proto
WM_BASE
EQU 0
WM_SET_PANES_HWND
EQU WM_USER + WM_BASE
WM_SET_BORDERS_MIN
EQU WM_USER + WM_BASE + 1 |
The linker will bark at you if you
do not a .def (definition) file of exported functions and variables. This
is also quite simple for this library:
|
LIBRARY SplitterCtrl
EXPORTS InitSplitterCtrl |
This defines the name
of the dll, and the sole exported function. By defining the function here,
we also eliminate any pesky name "decoration."
It sure would be nice if Quick Editor
had a "Build DLL" setting in it's PROJECT menu, but so far it does not
(well, MY copy does) (perhaps I should nudge hutch about this for his next
release.) A dll is made at the link stage out of assembled objects, either
of these statements will do the trick for you:
|
; for No resources
\masm32\bin\Link
/DLL /SUBSYSTEM:WINDOWS /DEF:%1.def
/LIBPATH:c:\masm32\lib %1.obj
; with resources (an
rcrs.rc file compiled to rcrs.obj)
\masm32\bin\Link
/DLL /SUBSYSTEM:WINDOWS /DEF:%1.def
/LIBPATH:c:\masm32\lib %1.obj rsrc.obj |
Conclusion
-----------------------------------------------------------------------------------------
Dynamic Link Libraries provide a
useful container to write reuseable functions. And when all is done, they
don't take all that much more work then writing the code in the first place
inside a single application.
In part III, we'll go thru how to
use our new control.
Part
I Splitter Bar DLL
Part
III Using the Splitter Bar DLL
Part
IV Ready for Prime Time
home |