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