home

Splitter Bar: 
Ready for Prime Time

Ernest Murphy ernie@surfree.com

October 22, 2000 

Get the control source code  and test application code.
 

Abstract:
------------------------------------------------------------------------------------------ 

  A ready-for-prime-time Splitter Bar control is demonstrated. This project builds on the three previous projects, and is only explained in brief.
 
 

------------------------------------------------------------------------------------------ 

  So far, we have only looked at vertical splitter bars, but they may also be vertical.
Have a look at where we are going:

  Besides the obvious new horizontal splitter bar, there is something else significant this incarnation of the control performs: it can revise multiple adjacent controls. The horizontal bar has 3 controls (IDC_EDIT1, IDC_EDIT2, and IDC_SPLITTER2) in it's bottom pane. That means we must pass a LIST of window handles to our control.

  The easiest way to handle a list of arbitrary size is make the data itself indicate the list end; let's take our clue from character strings and define a NULL handle in the list as the list end. 

  Let us describe the application code first, then show how this is handled by the control.

  To make the lists of these controls, we use structures. Structures have the nice property of assigning default values, so we can start all elements as zero. This also means we don't have to use code to set the end of list NULL. For these two splitters, these structures are formed:
 
_Split1Top  STRUCT
    hwndEdit1       DWORD   0
    hwndEND         DWORD   0
_Split1Top  ENDS

_Split1Bottom  STRUCT
    hwndEdit2       DWORD   0
    hwndSplitter2   DWORD   0
    hwndEdit3       DWORD   0
    hwndEND         DWORD   0
_Split1Bottom  ENDS

_Split2Left  STRUCT
    hwndEdit2       DWORD   0
    hwndEND         DWORD   0
_Split2Left  ENDS

_Split2Right  STRUCT
    hwndEdit3       DWORD   0
    hwndEND         DWORD   0
_Split2Right  ENDS

Split1Top           _Split1Top           { }
Split1Bottom        _Split1Bottom        { }
Split2Left          _Split2Left          { }
Split2Right         _Split2Right         { }

  A horizontal splitter needs a list of controls above and below it, a vertical splitter needs a list of left and right sides. These structs define the lists for all the window handles the two splitters will need.

  We can initialize these structures like so:
 
 
invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL
mov hDlg,eax

; now inform the splitter what is should move
invoke GetDlgItem, hDlg, IDC_EDIT1
mov Split1Top.hwndEdit1, eax

invoke GetDlgItem, hDlg, IDC_EDIT2
mov Split1Bottom.hwndEdit2, eax
mov Split2Left.hwndEdit2, eax

invoke GetDlgItem, hDlg, IDC_EDIT3
mov Split1Bottom.hwndEdit3, eax
mov Split2Right.hwndEdit3, eax

invoke GetDlgItem, hDlg, IDC_SPLITTER1
mov hSplitter1, eax
invoke GetDlgItem, hDlg, IDC_SPLITTER2
mov hSplitter2, eax
mov Split1Bottom.hwndSplitter2, eax
 

invoke SendMessage, hSplitter1,WM_SET_PANES_HWND, ADDR Split1Top, ADDR Split1Bottom
invoke SendMessage, hSplitter1,WM_SET_BORDERS_MIN, 30, 50
invoke SendMessage, hSplitter2,WM_SET_PANES_HWND, ADDR Split2Left, ADDR Split2Right
invoke SendMessage, hSplitter2,WM_SET_BORDERS_MIN, 60, 30

  That is all the code we need. For resources, that too is simple:
 
 
MyDialog DIALOG 10, 10, 217, 185
STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK
CAPTION "Splitter Control Test Application"
CLASS "DLGCLASS"
BEGIN
    EDITTEXT   IDC_EDIT1,      4,  6, 209, 69, ES_AUTOHSCROLL | ES_LEFT
    CONTROL "",IDC_SPLITTER1, "HSplitterCtrl",  3, 4,75,209,3
    EDITTEXT   IDC_EDIT2,      4, 78,  83, 90, ES_AUTOHSCROLL | ES_LEFT
    CONTROL "",IDC_SPLITTER2, "VSplitterCtrl", 4, 87,78,3,90
    EDITTEXT   IDC_EDIT3,     90, 78, 123, 90, ES_AUTOHSCROLL | ES_LEFT
END

  Here, note the Splitter class has morphed into two different controls, a horizontal (HSplitterCtrl) and vertical (VSplitterCtrl) class. This is solely to give each a different default cursor bitmap, as these bitmaps are strictly a class property, and may not be changed for a particular class. If it could be set for a specific instance, one set of could (with some extra internal conditionals) would have sufficed.
 

The Control Library
------------------------------------------------------------------------------------------ 

  The horizontal splitter class is a simple change to the existing class. They are defined as such:
 
 
.IF reason==DLL_PROCESS_ATTACH
    mov eax, hInst
    mov hInstance,eax
    ; register a simple class as our splitter bar child window control

    mov wc.cbSize,SIZEOF WNDCLASSEX
    mov wc.style, CS_GLOBALCLASS        ; thanks Izcelion  :-)
    mov wc.lpfnWndProc, OFFSET VSplitterProc
    mov wc.cbWndExtra, SIZEOF DWORD  ; we need a pointer in here
    mov eax,  hInstance
    mov wc.hInstance, eax
    mov wc.hbrBackground,COLOR_BTNFACE + 2
    mov wc.lpszMenuName, NULL     ;OFFSET MenuName
    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
 

    mov wc.lpfnWndProc, OFFSET HSplitterProc
    mov wc.lpszClassName,OFFSET szHSplitterCtrl
    invoke LoadCursor,NULL,IDC_SIZENS       ; up/down cursor
    mov wc.hCursor,eax
    invoke RegisterClassEx, addr wc

  Both splitter classes get a data structure to hold instance information. They are really the same structure, just with some name changes to convey a bit more sense:
 
VSplitterData        STRUCT
    m_hWndParent        DWORD   0
    m_pLeftList         DWORD   0
    m_pRightList        DWORD   0
    m_RIGHT_MIN         DWORD   0
    m_LEFT_MIN          DWORD   0
VSplitterData        ENDS

HSplitterData        STRUCT
    m_hWndParent        DWORD   0
    m_pTopList          DWORD   0
    m_pBottomList       DWORD   0
    m_TOP_MIN           DWORD   0
    m_BOTTOM_MIN        DWORD   0
HSplitterData        ENDS

  The lists of window handles as passed in by reference, i.e., the address of the list is pushed. This means the same code (with minor name changes) is used to store them, and will not be discussed.

  The handle data is handled with a simple .WHILE loop. A local variable is set to the list pointer. This address is loaded to eax. Windows are processed .WHILE eax != 0 (i.e., while the current handle is not NULL). A NULL terminates the loop, and sends us on to our next task. If we do loop, the LOCAL pointer is incremented by SIZEOF DWORD (4) to point to the next handle to process. This loop is illustrated in the following fragment:
 
 
mov eax, pWindowHandleList
mov pCurrent, eax
mov eax, [eax]

.WHILE eax
    mov hWndCurrent, eax

 { process this window handle }

    ; set up for next window in list
    add pCurrent, SIZEOF DWORD
    mov eax, pCurrent
    mov eax, [eax]
.ENDW

{ on to next item to process }

  One more trick was added into this implementation. If you note in Windows Explorer, if you select the splitter with a left click, you are unable to drag the mouse cursor outside of the parent. This was included with a ClipRect inside the LBUTTONDOWN handler:
 
 
invoke GetClientRect, hWndParent, ADDR TopRect; borrow rect struct
mov ptClient.x, NULL
mov ptClient.y, NULL
invoke ClientToScreen, hWndParent, ADDR ptClient
mov eax, ptClient.x
add TopRect.left, eax
mov eax, ptClient.y
add TopRect.top, eax
mov eax, TopRect.left
add TopRect.right, eax
mov eax, TopRect.top
add TopRect.bottom, eax
invoke ClipCursor, ADDR TopRect

  This clipping is canceled with a simple "invoke ClipCursor, NULL" inside LBUTTONUP.

  As I was making my first release of this (yesterday as I type), I noticed a minor bug: if a window was resized beyond it's minimum, and the mouse button released, it would pop to some uncertain size. I realized the re-size routine was getting confused, as it had partially moved/resized some windows before it determined this move was too much, so it would start over with a new  smaller move.

  This was a problem because the starting point was lost. The fix was to repeat this section of code twice, the first time just to check if the min size was violated, and the second size to do the actual move.  Things are just fine now, thank you.
 

Conclusion:
------------------------------------------------------------------------------------------ 
  This is briefly how the control class works. The code should speak for itself. Any questions, you know how to reach me.

Part I Splitter Bars

Part II Splitter Bar DLL

Part III Using the Splitter Bar DLL
 

home