Using ecdClientDrawOnly with the Owner Draw Combo List Box Control
Create customised ListBoxes and Combo Boxes, demonstrating how to build line-style and texture-style pickers
This article demonstrates how to use the ecdClientDrawOnly mode of the vbAccelerator Owner Draw and Combo List Box control to draw completely customised Combo Boxes (note that exactly the same technique applies for List Boxes as well). The sample provides three useful implementations - a line picker, a line dash picker and a texture or background picker.
Custom Drawing with vbalODCL.ocx
To draw your own styles with the vbAccelerator Owner Draw Combo and List Box, there are three things you need to do:
Responding to the MeasureItem event
The MeasureItem event provides you with three parameters: the Index of the item to measure and two byref long values, WidthPixels and HeightPixels. Set WidthPixels to the desired width of the item in pixels and HeightPixels to the height. If you do not do this, the drop-down part of your combo box will be very small!
Responding to the DrawItem event
The DrawItem event provides the following parameters:
In order to draw an item you need to be able to perform drawing into the Device Context specified by the hDC parameter. You can either do this using the GDI API, or, if you are mad, by drawing into a VB object and then copying the contents into the DC using BitBlt. These methods are covered in turn.
Drawing Using the GDI API - The Samples
There are three sample combo boxes implemented in the sample code:
The implementation of each of these samples is fundamentally the same, just the specifics of the drawing code in each case is different. Therefore we'll just cover the Line-Style picker.
Items in the Line-Style picker all have the same height and width, and this is set to be the same as the size of the combo box itself. Thus the response to the MeasureItem event is:
Private Sub cboLineStyle_MeasureItem( _ Index As Long, WidthPixels As Long, HeightPixels As Long) WidthPixels = cboLineStyle.Width \ Screen.TwipsPerPixelX HeightPixels = cboLineStyle.Height \ Screen.TwipsPerPixelY End Sub
The line picker itself displays the size of the line and then draws a sample of the line itself. The code uses the ComboBox List property to store the line size to display as text, the ItemData property to store the actual line size (multiplied by 100 since ItemData is a long and the line width itself can have decimal places) and the ItemExtraData to store the style of line to draw. If you wanted to store more sophisticated information against a ComboBox item there are many possible techniques, such as a using an key to an item within Collection (set the collection key to some character followed by the ItemData) or array, or a pointer to an object.
Drawing the item then is a matter of drawing the background, getting the line size text and drawing that, then getting the line size and style and drawing a line sample next to the text. When drawing the background, we need to take account of whether the item is selected or not. Drawing the background is accomplished as follows:
' Ensure text draws with transparent background: SetBkMode hdc, TRANSPARENT ' Set up a rectangle based on DrawItem parameters: tR.left = LeftPixels tR.tOp = TopPixels tR.Bottom = BottomPixels tR.Right = RightPixels ' Set the background and text color to draw: If (bSelected) Then hBrush = GetSysColorBrush(COLOR_HIGHLIGHT) SetTextColor hdc, GetSysColor(COLOR_HIGHLIGHTTEXT) Else hBrush = GetSysColorBrush(COLOR_WINDOW) SetTextColor hdc, GetSysColor(COLOR_WINDOWTEXT) End If ' Draw the background: FillRect hdc, tR, hBrush ' Clear up - probably not needed for GetSysColorBrush DeleteObject hBrush
Once the background is displayed, the code checks if the control is in focus and draws the dotted focus rectangle around the item if it is:
If (GetFocus() = cboLineStyle.hwnd) And bSelected Then DrawFocusRect hdc, tR End If
Now the actual item can be drawn. We check if any item is actually selected in the control: if not, then there is no more drawing to be done. If the item is selected, the text and line style are read and drawn:
If (Index <> -1) Then ' Get details of item to draw: iLineWidth = cboLineStyle.ItemExtraData(Index) iLineStyle = cboLineStyle.ItemData(Index) sText = cboLineStyle.List(Index) ' Draw the text: LSet tTXR = tR tTXR.left = tTXR.left + 4 ' Calculate the size needed for the text: DrawText hdc, sText, -1, tTXR, DT_CALCRECT ' Draw it DrawText hdc, sText, -1, tTXR, DT_LEFT ' Adjust drawing rectangle to line: tR.left = 24 tR.Right = tR.Right - 4 ' Finally draw the line: Select Case iLineStyle Case 1 ' single line: lY = tR.tOp + (tR.Bottom - tR.tOp) \ 2 DrawLine hdc, tR.left, tR.Right, lY, (iLineWidth \ 100) ' ... other line styles here ... End Select
The DrawLine function is a VB wrapper to creating an appropriate pen and drawing the line:
Private Function DrawLine( _ ByVal hdc As Long, _ ByVal lXStart As Long, ByVal lXEnd As Long, _ ByVal lY As Long, ByVal lWidth As Long, _ Optional ByVal lStyle As Long = PS_SOLID) Dim hPen As Long Dim hPenOld As Long Dim tP As POINTAPI Dim tLB As LOGBRUSH ' Create the Pen: tLB.lbColor = GetSysColor(COLOR_WINDOWTEXT) hPen = ExtCreatePen(PS_GEOMETRIC Or PS_ENDCAP_FLAT Or lStyle, _ lWidth, tLB, 0, ByVal 0&) ' Select the pen for drawing: hPenOld = SelectObject(hdc, hPen) ' Draw the line using the pen: BeginPath hdc MoveToEx hdc, lXStart, lY, tP LineTo hdc, lXEnd, lY EndPath hdc StrokePath hdc ' Remove the pen and clear up: SelectObject hdc, hPenOld DeleteObject hPen End Function
Alternative (More VB-like) Method
An alternative to using the GDI API to draw the items is to use an intermediate PictureBox to draw onto using VB commands, and then transfer the result using the GDI BitBlt method. To do this, add a PictureBox to your form. Set the Visible property to False and then set AutoRedraw to True.
When the DrawItem event fires, ensure that the PictureBox is large enough to draw the item into, using for example:
If (picDraw.Width < (RightPixels - LeftPixels) \ Screen.TwipsPerPixelX Then picDraw.Width = (RightPixels - LeftPixels) \ Screen.TwipsPerPixelX End If If (picDraw.Height < (BottomPixels - TopPixels) \ Screen.TwipsPerPixelY Then picDraw.Height = (BottomPixels - BottomPixels) \ Screen.TwipsPerPixelY End If
Now you can draw onto the PictureBox using any of the standard (awful) VB drawing methods such as CurrentX, CurrentY, Line, Print, PaintPicture etc. Once complete, you can transfer the results to the hDC passed in by DrawItem like this:
Private Declare Function BitBlt Lib "gdi32" _ (ByVal hDestDC As Long, _ ByVal X As Long, ByVal Y As Long, _ ByVal nWidth As Long, ByVal nHeight As Long, _ ByVal hSrcDC As Long, _ ByVal xSrc As Long, ByVal ySrc As Long, _ ByVal dwRop As Long) As Long ... BitBlt hdc, LeftPixels, TopPixels, _ (RightPixels - LeftPixels - 1), (BottomPixels - TopPixels - 1), _ picDraw.hDC, _ 0, 0, _ vbSrcCopy