The new vbAccelerator Site - more VB and .NET Code and Controls
Source Code
3 Code Libraries Source Code &nbsp

NOTE: this code has been superceded by the version at the new site.


Splitting for all types of forms, including MDI forms, by drawing on the Desktop DC

Download the cSplitDDC Demonstration (41kb)

The cSplitDDC class demonstrates a extension of the simple splitter class. Similar to the simple splitter, a PictureBox is used as the target area for splitting on the form. However, rather than moving the PictureBox itself to show the split, this version uses the current cursor position until the mouse is released to draw an image of the splitter on the Desktop. This is the same method that VB uses to achieve the effect of dockable tool windows and to show splitting.

Because we are drawing directly on the Desktop window (which is simply an image of the windows on the Desktop), there is absolutely no restriction where you draw the splitter. The limitation of the simple method was that the PictureBox had to be moved to a position to show the dragging in effect. This meant it was impossible to perform splits on, for example, a left aligned Picture Box in an MDI form because you cannot move the PictureBox to any position other than an aligned position in the MDI form. With the Desktop method you can do this. In fact, this method is the basis for all kinds of interesting code. For example, you could certainly achieve the dockable tool window effect with this method, because you can draw a drag outline (or a Blitted Image of the form you are moving - if you prefer your dockable windows to look like the ones in Office 97) anywhere on the screen.

How it works
In this method, a PictureBox is drawn on the form to show the position for the splitter. The only purpose of the PictureBox is to set the MousePointer to NS or EW depending on the orientation of the splitter and to allow the user to make the initial click.

Subsequent Mouse Input is directed to the parent form of the splitter window. To draw the splitter image, the code evaluates where the mouse is using the GetCursor API call. Then it determines the current boundaries of the window we are splitting on the screen by checking the position of the Window's Rectangles using the GetWindowRect and GetClientRect calls, which it compares to the screen location by using ClientToScreen.

To draw the splitter itself, the code requests Windows to provide a DC to the Desktop using CreateDC, and then draws the splitter rectangle using an XOR pen. This makes it easy to erase the rectangle again by simply redrawing it in its last position. Finally, to make the whole thing as neat as possible, the splitter class clips mouse input so it can only fall within the boundary of the form being split.

The code gets a little involved with the need to calculate exactly where a window's boundaries are on the screen. This complexity is all wrapped up in the cSplitDDC class so its still as easy to use as the simple splitter class. The download includes a demonstration vertical splitter on an MDI form as well as a vertical and horizontal splitter on a child form.

Here is the code to implement cSplitDDC:

Option Explicit

' ======================================================================
' Class:cSplitDDC
' Filename: cSplitDC.cls
' Author: SP McMahon
' Date: 07 July 1998
' A splitter class using the Desktop window to draw a
' splitter bar, therefore allowing splitting of MDI forms
' as well as standard forms.
' ======================================================================

'// some global declarations
Private bDraw As Boolean
Private rcCurrent As RECT
Private rcNew As RECT
Private rcWindow As RECT

Private Type POINTAPI
&nbsp &nbsp X As Long
&nbsp &nbsp Y As Long
End Type
Private Type RECT
&nbsp &nbsp Left As Long
&nbsp &nbsp Top As Long
&nbsp &nbsp Right As Long
&nbsp &nbsp Bottom As Long
End Type
Private Declare Function Rectangle Lib "gdi32" (ByVal hDC As Long, ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long) As Long
Private Declare Function SetCapture Lib "user32" (ByVal hwnd As Long) As Long
Private Declare Function ReleaseCapture Lib "user32" () As Long
Private Declare Function ClientToScreen Lib "user32" (ByVal hwnd As Long, lpPoint As POINTAPI) As Long
Private Declare Function ScreenToClient Lib "user32" (ByVal hwnd As Long, lpPoint As POINTAPI) As Long
Private Declare Function DeleteDC Lib "gdi32" (ByVal hDC As Long) As Long
Private Declare Function CreateDCAsNull Lib "gdi32" Alias "CreateDCA" (ByVal lpDriverName As String, lpDeviceName As Any, lpOutput As Any, lpInitData As Any) As Long
Private Declare Function SetROP2 Lib "gdi32" (ByVal hDC As Long, ByVal nDrawMode As Long) As Long
&nbsp &nbsp Private Const R2_BLACK = 1 ' 0
&nbsp &nbsp Private Const R2_COPYPEN = 13' P
&nbsp &nbsp Private Const R2_LAST = 16
&nbsp &nbsp Private Const R2_MASKNOTPEN = 3 ' DPna
&nbsp &nbsp Private Const R2_MASKPEN = 9 ' DPa
&nbsp &nbsp Private Const R2_MASKPENNOT = 5 ' PDna
&nbsp &nbsp Private Const R2_MERGENOTPEN = 12&nbsp &nbsp ' DPno
&nbsp &nbsp Private Const R2_MERGEPEN = 15 ' DPo
&nbsp &nbsp Private Const R2_MERGEPENNOT = 14&nbsp &nbsp ' PDno
&nbsp &nbsp Private Const R2_NOP = 11&nbsp &nbsp ' D
&nbsp &nbsp Private Const R2_NOT = 6 ' Dn
&nbsp &nbsp Private Const R2_NOTCOPYPEN = 4 ' PN
&nbsp &nbsp Private Const R2_NOTMASKPEN = 8 ' DPan
&nbsp &nbsp Private Const R2_NOTMERGEPEN = 2 ' DPon
&nbsp &nbsp Private Const R2_NOTXORPEN = 10 ' DPxn
&nbsp &nbsp Private Const R2_WHITE = 16 ' 1
&nbsp &nbsp Private Const R2_XORPEN = 7 ' DPx
Private Declare Function GetWindowRect Lib "user32" (ByVal hwnd As Long, lpRect As RECT) As Long
Private Declare Function GetClientRect Lib "user32" (ByVal hwnd As Long, lpRect As RECT) As Long
Private Declare Sub ClipCursorRect Lib "user32" Alias "ClipCursor" (lpRect As RECT)
Private Declare Sub ClipCursorClear Lib "user32" Alias "ClipCursor" (ByVal lpRect As Long)
Private Declare Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long
Private Declare Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
Private Declare Function GetSystemMetrics Lib "user32" (ByVal nIndex As Long) As Long
&nbsp &nbsp Private Const SM_CXBORDER = 5
&nbsp &nbsp Private Const SM_CYBORDER = 6
&nbsp &nbsp Private Const SM_CYCAPTION = 4
&nbsp &nbsp Private Const SM_CYMENU = 15

Public Enum eOrientationConstants
&nbsp &nbsp espVertical = 1
&nbsp &nbsp espHorizontal = 2
End Enum
Private m_hWnd As Long
Private m_eOrientation As eOrientationConstants
Private m_lBorder(1 To 4) As Long
Private m_oSplit As Object
Public Enum ESplitBorderTypes
&nbsp &nbsp espbLeft = 1
&nbsp &nbsp espbTop = 2
&nbsp &nbsp espbRight = 3
&nbsp &nbsp espbBottom = 4
End Enum
Private m_bIsMDI As Boolean
Private m_bSplitting As Boolean

Public Property Get SplitObject() As Object
&nbsp &nbsp Set SplitObject = m_oSplit
End Property
Public Property Let SplitObject(ByRef oThis As Object)
&nbsp &nbsp Set m_oSplit = oThis
&nbsp &nbsp On Error Resume Next
&nbsp &nbsp oThis.BorderStyle = 0
&nbsp &nbsp If (m_eOrientation = espHorizontal) Then
&nbsp &nbsp &nbsp &nbsp oThis.MousePointer = vbSizeNS
&nbsp &nbsp Else
&nbsp &nbsp &nbsp &nbsp oThis.MousePointer = vbSizeWE
&nbsp &nbsp End If
End Property
Public Property Let Border(ByVal eBorderType As ESplitBorderTypes, ByVal lSize As Long)
m_lBorder(eBorderType) = lSize
End Property
Public Property Get Border(ByVal eBorderType As ESplitBorderTypes) As Long
Border = m_lBorder(eBorderType)
End Property
Public Property Get Orientation() As eOrientationConstants
&nbsp &nbsp Orientation = m_eOrientation
End Property
Public Property Let Orientation(ByVal eOrientation As eOrientationConstants)
&nbsp &nbsp m_eOrientation = eOrientation
&nbsp &nbsp If Not (m_oSplit Is Nothing) Then
&nbsp &nbsp &nbsp &nbsp If (m_eOrientation = espHorizontal) Then
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp m_oSplit.MousePointer = vbSizeNS
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp m_lBorder(espbTop) = 64
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp m_lBorder(espbBottom) = 64
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp m_lBorder(espbLeft) = 0
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp m_lBorder(espbRight) = 0
&nbsp &nbsp &nbsp &nbsp Else
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp m_oSplit.MousePointer = vbSizeWE
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp m_lBorder(espbTop) = 0
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp m_lBorder(espbBottom) = 0
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp m_lBorder(espbLeft) = 64
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp m_lBorder(espbRight) = 64
&nbsp &nbsp &nbsp &nbsp End If
&nbsp &nbsp End If
End Property

Public Sub SplitterMouseDown( _
&nbsp &nbsp &nbsp &nbsp ByVal hwnd As Long, _
&nbsp &nbsp &nbsp &nbsp ByVal X As Long, _
&nbsp &nbsp &nbsp &nbsp ByVal Y As Long _
&nbsp &nbsp )

&nbsp &nbsp m_hWnd = hwnd

&nbsp &nbsp ' Send subsequent mouse messages to the owner window
&nbsp &nbsp SetCapture m_hWnd
&nbsp &nbsp ' Get the window rectangle on the desktop of the owner window:
&nbsp &nbsp GetWindowRect m_hWnd, rcWindow
&nbsp &nbsp ' Clip the cursor so it can't move outside the window:
&nbsp &nbsp ClipCursorRect rcWindow
&nbsp &nbsp
&nbsp &nbsp ' Check if this is an MDI form:
&nbsp &nbsp If (ClassName(m_hWnd) = "ThunderMDIForm") Then
&nbsp &nbsp &nbsp &nbsp ' Get the inside portion of the MDI form:
&nbsp &nbsp &nbsp &nbsp ' I'm assuming you have a caption,menu and border in your MDI here
&nbsp &nbsp &nbsp &nbsp rcWindow.Left = rcWindow.Left + GetSystemMetrics(SM_CXBORDER)
&nbsp &nbsp &nbsp &nbsp rcWindow.Right = rcWindow.Right - GetSystemMetrics(SM_CXBORDER)
&nbsp &nbsp &nbsp &nbsp rcWindow.Bottom = rcWindow.Bottom - GetSystemMetrics(SM_CYBORDER)
&nbsp &nbsp &nbsp &nbsp rcWindow.Top = rcWindow.Top + GetSystemMetrics(SM_CYBORDER) * 3 + GetSystemMetrics(SM_CYCAPTION) + GetSystemMetrics(SM_CYMENU)
&nbsp &nbsp &nbsp &nbsp m_bIsMDI = True
&nbsp &nbsp Else
&nbsp &nbsp &nbsp &nbsp ' Get the client rectangle of the window in screen coordinates:
&nbsp &nbsp &nbsp &nbsp GetClientRect m_hWnd, rcWindow
&nbsp &nbsp &nbsp &nbsp tP.X = rcWindow.Left
&nbsp &nbsp &nbsp &nbsp tP.Y = rcWindow.Top
&nbsp &nbsp &nbsp &nbsp ClientToScreen m_hWnd, tP
&nbsp &nbsp &nbsp &nbsp rcWindow.Left = tP.X
&nbsp &nbsp &nbsp &nbsp rcWindow.Top = tP.Y
&nbsp &nbsp &nbsp &nbsp tP.X = rcWindow.Right
&nbsp &nbsp &nbsp &nbsp tP.Y = rcWindow.Bottom
&nbsp &nbsp &nbsp &nbsp ClientToScreen m_hWnd, tP
&nbsp &nbsp &nbsp &nbsp rcWindow.Right = tP.X
&nbsp &nbsp &nbsp &nbsp rcWindow.Bottom = tP.Y
&nbsp &nbsp &nbsp &nbsp m_bIsMDI = False
&nbsp &nbsp End If
&nbsp &nbsp bDraw = True '// start actual drawing from next move message
&nbsp &nbsp
&nbsp &nbsp rcCurrent.Left = 0: rcCurrent.Top = 0: rcCurrent.Right = 0: rcCurrent.Bottom = 0
&nbsp &nbsp
&nbsp &nbsp X = (m_oSplit.Left + X) \ Screen.TwipsPerPixelX
&nbsp &nbsp Y = (m_oSplit.Top + Y) \ Screen.TwipsPerPixelY
&nbsp &nbsp SplitterFormMouseMove X, Y
&nbsp &nbsp
End Sub

Public Sub SplitterFormMouseMove( _
&nbsp &nbsp ByVal X As Long, _
&nbsp &nbsp ByVal Y As Long)
Dim hDC As Long
Dim hWndClient As Long
&nbsp &nbsp If (bDraw) Then
&nbsp &nbsp &nbsp &nbsp '// Draw two rectangles in the screen DC to cause splitting:
&nbsp &nbsp &nbsp &nbsp
&nbsp &nbsp &nbsp &nbsp ' First get the Desktop DC:
&nbsp &nbsp &nbsp &nbsp hDC = CreateDCAsNull("DISPLAY", ByVal 0&, ByVal 0&, ByVal 0&)
&nbsp &nbsp &nbsp &nbsp ' Set the draw mode to XOR:
&nbsp &nbsp &nbsp &nbsp SetROP2 hDC, R2_NOTXORPEN
&nbsp &nbsp
&nbsp &nbsp &nbsp &nbsp '// Draw over and erase the old rectangle
&nbsp &nbsp &nbsp &nbsp ' (if this is the first time, all the coords will be 0 and nothing will get drawn):
&nbsp &nbsp &nbsp &nbsp Rectangle hDC, rcCurrent.Left, rcCurrent.Top, rcCurrent.Right, rcCurrent.Bottom
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp
&nbsp &nbsp &nbsp &nbsp ' It is simpler to use the mouse cursor position than try to translate
&nbsp &nbsp &nbsp &nbsp ' X,Y to screen coordinates!
&nbsp &nbsp &nbsp &nbsp GetCursorPos tP
&nbsp &nbsp &nbsp &nbsp
&nbsp &nbsp &nbsp &nbsp ' Determine where to draw the splitter:
&nbsp &nbsp &nbsp &nbsp If (m_eOrientation = espHorizontal) Then
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp rcNew.Left = rcWindow.Left + m_lBorder(espbLeft)
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp rcNew.Right = rcWindow.Right - m_lBorder(espbRight)
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp If (tP.Y >= rcWindow.Top + m_lBorder(espbTop)) And (tP.Y &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp rcNew.Top = tP.Y - 2
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp rcNew.Bottom = tP.Y + 2
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp Else
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp If (tP.Y &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp rcNew.Top = rcWindow.Top + m_lBorder(espbTop) - 2
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp rcNew.Bottom = rcNew.Top + 5
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp Else
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp rcNew.Top = rcWindow.Bottom - m_lBorder(espbBottom) - 2
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp rcNew.Bottom = rcNew.Top + 5
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp End If
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp End If
&nbsp &nbsp &nbsp &nbsp Else
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp rcNew.Top = rcWindow.Top + m_lBorder(espbTop)
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp rcNew.Bottom = rcWindow.Bottom - m_lBorder(espbBottom)
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp If (tP.X >= rcWindow.Left + m_lBorder(espbLeft)) And (tP.X &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp rcNew.Left = tP.X - 2
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp rcNew.Right = tP.X + 2
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp Else
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp If (tP.X &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp rcNew.Left = rcWindow.Left + m_lBorder(espbLeft) - 2
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp rcNew.Right = rcNew.Left + 5
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp Else
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp rcNew.Left = rcWindow.Right - m_lBorder(espbRight) - 2
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp rcNew.Right = rcNew.Left + 5
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp &nbsp End If
&nbsp &nbsp &nbsp &nbsp &nbsp &nbsp End If
&nbsp &nbsp &nbsp &nbsp End If
&nbsp &nbsp &nbsp &nbsp
&nbsp &nbsp &nbsp &nbsp '// Draw the new rectangle
&nbsp &nbsp &nbsp &nbsp Rectangle hDC, rcNew.Left, rcNew.Top, rcNew.Right, rcNew.Bottom
&nbsp &nbsp &nbsp &nbsp
&nbsp &nbsp &nbsp &nbsp ' Store this position so we can erase it next time:
&nbsp &nbsp &nbsp &nbsp LSet rcCurrent = rcNew
&nbsp &nbsp &nbsp &nbsp
&nbsp &nbsp &nbsp &nbsp ' Free the reference to the Desktop DC we got (make sure you do this!)
&nbsp &nbsp &nbsp &nbsp DeleteDC hDC
&nbsp &nbsp End If
&nbsp &nbsp
End Sub

Public Function SplitterFormMouseUp( _
&nbsp &nbsp ByVal X As Long, _
&nbsp &nbsp ByVal Y As Long _
) As Boolean
Dim hDC As Long
Dim hWndClient As Long

&nbsp &nbsp '// Don't leave orphaned rectangle on desktop; erase last rectangle.
If (bDraw) Then
&nbsp &nbsp bDraw = False
&nbsp &nbsp
&nbsp &nbsp ' Release mouse capture:
&nbsp &nbsp ReleaseCapture
&nbsp &nbsp ' Release the cursor clipping region (must do this!):
&nbsp &nbsp ClipCursorClear 0&
&nbsp &nbsp
&nbsp &nbsp ' Get the Desktop DC:
&nbsp &nbsp hDC = CreateDCAsNull("DISPLAY", 0, 0, 0)
&nbsp &nbsp ' Set to XOR drawing mode:
&nbsp &nbsp SetROP2 hDC, R2_NOTXORPEN
&nbsp &nbsp ' Erase the last rectangle:
&nbsp &nbsp Rectangle hDC, rcCurrent.Left, rcCurrent.Top, rcCurrent.Right, rcCurrent.Bottom
&nbsp &nbsp ' Clear up the desktop DC:
&nbsp &nbsp DeleteDC hDC
&nbsp &nbsp
&nbsp &nbsp ' Here we ensure the splitter is within bounds before releasing:
&nbsp &nbsp GetCursorPos tP

&nbsp &nbsp If (tP.X &nbsp &nbsp &nbsp &nbsp tP.X = rcWindow.Left + m_lBorder(espbLeft)
&nbsp &nbsp End If
&nbsp &nbsp If (tP.X > rcWindow.Right - m_lBorder(espbRight)) Then
&nbsp &nbsp &nbsp &nbsp tP.X = rcWindow.Right - m_lBorder(espbRight)
&nbsp &nbsp End If
&nbsp &nbsp If (tP.Y &nbsp &nbsp &nbsp &nbsp tP.Y = rcWindow.Top + m_lBorder(espbTop)
&nbsp &nbsp End If
&nbsp &nbsp If (tP.Y > rcWindow.Bottom - m_lBorder(espbBottom)) Then
&nbsp &nbsp &nbsp &nbsp tP.Y = rcWindow.Bottom - m_lBorder(espbBottom)
&nbsp &nbsp End If
&nbsp &nbsp ScreenToClient m_hWnd, tP
&nbsp &nbsp
&nbsp &nbsp ' Move the splitter to the validated final position:
&nbsp &nbsp If (m_eOrientation = espHorizontal) Then
&nbsp &nbsp &nbsp &nbsp m_oSplit.Top = (tP.Y - 2) * Screen.TwipsPerPixelY
&nbsp &nbsp Else
&nbsp &nbsp &nbsp &nbsp m_oSplit.Left = (tP.X - 2) * Screen.TwipsPerPixelX
&nbsp &nbsp End If
&nbsp &nbsp
&nbsp &nbsp ' Return true to tell the owner we have completed splitting:
&nbsp &nbsp SplitterFormMouseUp = True
End If

End Function

Private Sub Class_Initialize()
&nbsp &nbsp m_eOrientation = espVertical
&nbsp &nbsp m_lBorder(espbLeft) = 64
&nbsp &nbsp m_lBorder(espbRight) = 64
End Sub
Private Function ClassName(ByVal lHwnd As Long) As String
Dim lLen As Long
Dim sBuf As String
&nbsp &nbsp lLen = 260
&nbsp &nbsp sBuf = String$(lLen, 0)
&nbsp &nbsp lLen = GetClassName(lHwnd, sBuf, lLen)
&nbsp &nbsp If (lLen 0) Then
&nbsp &nbsp &nbsp &nbsp ClassName = Left$(sBuf, lLen)
&nbsp &nbsp End If
End Function

Back to top

Back to Source Code Overview


AboutContributeSend FeedbackPrivacy

Copyright 1998-1999, Steve McMahon ( All Rights Reserved.
Last updated: 14 July 1998