06 January 2010

Custom Drawing Ocx Controls

Together with the TreeView common control, the ListView common control is one of the most important control used to present organized data. Since displaying data in columns is probably the most used features of the ListView control, I will discuss how to implement subitem coloring of cells in a report-view mode ListView Ocx control. In this installment I will discuss the necessary steps to start custom drawing a GB32 Ocx control. The ListView cell coloring is discussed in another entry.
Custom drawing
The common control library supports a technique called 'custom draw' to change the appearance of the elements of some of the controls. The custom draw technique was developed to eliminate the need to fully owner-draw entire controls. Custom draw allows trapping only a small set of the drawing operations, rather than perform all the drawing.
The common controls send custom draw messages as a WM_NOTIFY messages to the parent window. The WM_NOTIFY message specifies  a pointer to a NMHDR structure in the lParam argument. The NMHDR structure specifies the (notification) code, the window handle and ID of the control from which the message originates.
  hwndFrom As Long
  idfrom As Long
  code As Long
End Type
The _MessageProc event
In GB32 a parent-window is usually a Form. The WM_NOTIFY message is a sent message and can only be handled in Form's event sub Form_MessageProc(). The _MessageProc is invoked from inside the Form's window procedure (defined in the GfaWin23.Ocx) before  GB32 processes the message itself. The two additional arguments of the _MessageProc (retval% and ValidRet?) determine whether GB32 will actually continue processing this message in its own window procedure once it returned from _MessageProc, which could look like this:
Sub frm1_MessageProc(hWnd%, Mess%, wParam%, _
 lParam%, retval%, ValidRet?)

  Switch Mess
    ' use Pointer to cast a lParam to a type!
    Dim nmhdr As Pointer NMHDR
    Pointer nmhdr = lParam
    ' Custom draw message.
    If nmhdr.code == NM_CUSTOMDRAW
      ' (do something)
Noticeable is the elegant way GB32 allows you to cast a pointer to a structure. I explained Pointers in the past.
It is important to realize that NM_CUSTOMDRAW is sent always, for every common control that supports it. As soon as a common control starts it painting process it starts sending this message. It is up to the developer to tell the common control library to send follow-up messages for each stage in the painting process, or to stop sending notifications after the first one arrived. For performance reasons, the library only sends the message that signals the start of the paint-cycle (CDDS_PREPAINT) and then only sends more messages when you respond with a value "send-me-more".
Custom drawing Ocx controls
What to do once you intercepted the NM_CUSTOMDRAW message is described in the MSDN. However, the documentation always discusses C-style handling and provides sample code in case one control wants custom drawing. What if the control is a GB32 Ocx and multiple Ocxs want custom drawing? The question arises how to differentiate between the various Ocx controls when we process NM_CUSTOMDRAW?
Option #1 is to store the window handles of the Ocx-controls in global variables and compare these in a (possibly long) If-Else statement with nmhdr.hwndFrom. Once you find a match you know what to do. This option is certainly valid, but it is not my intention to show you how to solve this problem the API-way, but to show you the GB32- COM way.
The GB32-COM way
This is option #2. We will use nmhdr.hwndFrom to get an Ocx object and then use the object to perform general custom drawing for that object type. Since GB32 supports COM interfaces for ListView, TreeView, ToolBar, and Slider, not all common controls (Header, ToolTip, and ReBar) that support custom drawing can be handled this way. The ReBar common control is not implemented, unfortunately, and Header and ToolTip controls are part of other controls and are not support on their own. So, we need a function that separates the good from the bad.
Function OcxCustomDraw(hWndFrom%, lParam%, _
  ByRef retval%, ByRef ValidRet?) As Bool
  ' Non Ocx controls are rejected immediately, Only OCX() is invoked.

  Dim Ob As Object
  Set Ob = OCX(hWndFrom)

  If IsNothing(Ob)
    Return False        ' Not an OCX control
ElseIf TypeOf(Ob) Is ListView ' Most often used in custom drawing
    Return CustomDrawListView(Ob, lParam%, retval%, ValidRet?)    ' Big chance we have a hit.
  ElseIf TypeOf(Ob) Is TreeView
    Return CustomDrawTreeView(Ob, lParam%, retval%, ValidRet?)
  ElseIf TypeOf(Ob) Is ToolBar
    Return CustomDrawToolBar(Ob, lParam%, retval%, ValidRet?)
  ElseIf TypeOf(Ob) Is Slider
    Return CustomDrawSlider(Ob, lParam%, retval%, ValidRet?)

  ' Not a custom draw Ocx control, 
' continue processing
  ' NM_CUSTOMDRAW in Form_MessageProc()
  Return False
The window handle in nmhdr.hwndFrom identifies the common control, so we pass this value in the first parameter hwndFrom of  the OcxCustomDraw() function. The second parameter is the lParam value as it is passed to _MessageProc, holding a pointer to NMHDR. The out-parameters retval% and ValidRet? are passed as well, since they must be set once we processed the message. In addition, we make this a Boolean function to signal the caller (_MessageProc()) to continue processing WM_NOTIFY or to exit the event sub immediately. (The _MessageProc should not perform any action when the message is handled in OcxCustomDraw(), but It might do more processing when the hwndFrom passed in isn't an Ocx object or when NM_CUSTOMDRAW originates from a GB32 Ocx that doesn't support custom drawing. In these cases OcxCustomDraw() returns False.)
To check the Ocx type for a given window handle we need a two step process. First, obtain the address of the object that wraps the control using the OCX(hWnd) function. This is a simple addrObject = GetWindowLong(hWnd, GWL_USERDATA) call. When no object is attached to the window handle Ob Is Nothing and we can skip the rest of the function. When the Ob contains a valid Ocx type we test whether it is of type ListView, TreeView, ToolBar, or Slider using the TypeOf(O) Is Interfacetype construction. When True, we execute either one of four Ocx custom draw operations. These functions are defined as follows:
' ListView custom drawing
Function CustomDrawListView(Lv As ListView _
  , lParam%, ByRef retval%, ByRef ValidRet?) As Bool
  Dim nmcdr As Pointer NMLVCUSTOMDRAW
  Pointer nmcdr = lParam
Return False
' TreeView custom drawing
Function CustomDrawTreeView(Treeview As TreeView, _
  lParam As Long, ByRef retval%, ByRef ValidRet?) As Bool
  Dim nmcdr As Pointer NMTVCUSTOMDRAW
  Pointer nmcdr = lParam
  Return False
' ToolBar custom drawing
Function CustomDrawToolBar(Toolbar As ToolBar, _
  lParam As Long, ByRef retval%, ByRef ValidRet?) As Bool
  Dim nmcdr As Pointer NMTBCUSTOMDRAW
  Pointer nmcdr = lParam
  Return False
' Slider custom drawing
Function CustomDrawSlider(Slider As Slider, _
  lParam As Long, ByRef retval%, ByRef ValidRet?) As Bool
  Dim nmcdr As Pointer NMCUSTOMDRAW
  Pointer nmcdr = lParam
  Return False
For a ListView control Ocx the function calls CustomDrawListView(). When invoking the function the general Ob variable of type Object is cast to a ListView type implicitly. Each of these CustomDrawType() functions casts the Object variable Ob to the appropriate COM type. Inside each function the lParam is cast to the correct API custom-draw structure (see SDK).
Note - The types are all defined in the commctrl.inc.lg32 included in the GB32 package. You can either load this library using $Library or copy-paste the relevant Types.
Final note
The TypeOf(O) Is Interfacetype construction is a library function that checks the object against the IID value of InterfaceType. Therefore it calls the QueryInterface() for that type. This decreases performance of course, although the GB32 implementation of Is TypeOf is pretty fast.
Next time I discuss how to color subitems in report ListView.

No comments:

Post a Comment