19 January 2010

ListView Subitems Custom Draw

Just before Christmas 2009 Peter Heinzig mailed me a copy of his "listView_CellColor" program. He was experimenting with Custom Draw for common controls to provide cell coloring for subitems in a ListView in report-mode. To be honest, I never used custom draw, so I had some catching up to do. After reading the SDK I checked the GB32 disassembly to check if GB32 uses custom draw. Bingo! It does use it with the ListView Ocx control to implement the ListItem's properties ForeColor, BackColor, and Font. Anyone trying to implement custom draw for a ListItem object could get in problems with GB32's implementation, so it is time to implement a custom draw routine for ListView controls taking the GB32 implementation into account.

We step right into the drawing stage of custom draw Ocx controls. I discussed the message handling of custom draw for Ocx controls in a previous entry Custom Draw for Ocx controls. Note this is GFABASIC 32 specific implementation, other languages might use a different method.

Using the correct Ocx type
We left off with the function that invokes a specific subroutine when the NM_CUSTOMDRAW message has come from a ListView, TreeView, ToolBar, or Slider Ocx control. This function, which I called OcxCustomDraw() to emphasize we are processing GB32-Ocx specific custom drawing, passes the general Control variable as the first parameter, by value. In the process of passing, the type of COM variable is changed to the required COM interface.
Note - In VB/GB32 the interface name is the name of a COM object without the leading I. Here we have an Control variable, which is a pointer (4-bytes) addressing the memory occupying a structure for an Control type. When we pass the variable by value, we put the contents of the Control variable on the stack and interpret it in the called subroutine as an address of a (I)ListView type (interface ). Any actions performed on the local Ocx variable will instruct the compiler to use COM syntax for the type (I)ListView. Confusing? Probably worth a new blog entry ...

The drawing process
I'll be short about interpreting the NM_CUSTOMDRAW draw message data passed in the lParam argument. You'll find it in the SDK or MSDN. First we need to cast the pointer in lParam to the custom draw structure for a ListView control.

' ListView custom drawing
Function CustomDrawListView(Lv As ListView _
, lParam%, ByRef retval%, ByRef ValidRet?) As Bool
Dim nmcdr As Pointer NMLVCUSTOMDRAW
Pointer nmcdr = lParam

Now we can access the structure-members using the dot-operator syntax. For a ListView's subitem to draw, we must respond to three draw stages; CDDS_PREPAINT, CDDS_ITEMPREPAINT, and CDDS_ITEMPREPAINT | CDDS_SUBITEM. That is it.

1 CDDS_PREPAINT
Let us start with CDDS_PREPAINT. This notification is sent at the beginning of the drawing of the row. Each row sends it once only. You must response with the correct value to specify what you want next. Well, both GB32 as ourselves want more (sub)item draw messages, so we must respond with CDRF_NOTIFYITEMDRAW. Since GB32 already returns this value, our custom draw function may skip this message (although it wouldn't hurt if you didn't).

Switch nmcdr.nmcd.dwDrawStage
Case CDDS_PREPAINT
'retval = CDRF_NOTIFYITEMDRAW
'ValidRet? = True
Return True

We return from this function with True to let the Form's event sub _MessageProc() know that we handled this message and that it can leave the event sub without further processing.

2 CDDS_ITEMPREPAINT
The next drawing stage is CDDS_ITEMPREPAINT. When GB32 receives this message it fills in the appropriate NMLVCUSTOMDRAW  members on return. The common control library than uses the selected colors and fonts for drawing. After it processes this message, GB32 returns with a value indicating it is ready with this list item (row). We trap this message before GB32 handles it an specify that we want additional custom draw messages for each subitem of the row.

Case CDDS_ITEMPREPAINT
retval = CDRF_NOTIFYSUBITEMDRAW
ValidRet? = True
Return True

Because CDDS_ITEMPREPAINT returned CDRF_NOTIFYSUBITEM the common library will start sending notification messages, in the form CDDS_ITEMPREPAINT,  before it starts drawing each of the subitems.

3 CDDS_ITEMPREPAINT + CDDS_SUBITEM
To indicate subitem drawing the control library adds CDDS_SUBITEM to the value. So, in the next stage we must process, we must fill in the members of NMLVCUSTOMDRAW that the control library will use to draw. More precisely, we can fill in the .clrText and .clrTextBk fields for the colors and select a new font in the hDC. This is exactly the behavior of GB32. So, we will let GB32 process the message first.

Case CDDS_ITEMPREPAINT | CDDS_SUBITEM
' Set back to the ListView control default colors for each subitem.
nmcdr.clrText   = CLR_NONE
nmcdr.clrTextBk = CLR_NONE

  ' Let GB32 copy the ListItem properties of the row (color _and_ font)
' Go through the ListView window procedure, so forward the WM_NOTIFY message
' (as OCM_NOTIFY) to the ListView's window procedure.
nmcdr.nmcd.dwDrawStage = CDDS_ITEMPREPAINT
retval = SendMessage(Lv.hWnd, WM_NOTIFY + $2000, 0, *nmcdr)
ValidRet? = True

  ' Finally, we get a chance to overrule the color settings:
Local Int Row = nmcdr.nmcd.dwItemSpec + 1   ' 0 to 1 -based
Local Int Column = nmcdr.iSubItem + 1       ' 0 to 1 -based
If lv4_Colors(Row, Column).Fore <> CLR_NONE
nmcdr.clrText = lv4_Colors(Row, Column).Fore
EndIf
If lv4_Colors(Row, Column).Back <> CLR_NONE
nmcdr.clrTextBk = lv4_Colors(Row, Column).Back
EndIf

Return True     ' Notify the Form_MessageProc
EndSwitch  ' dwDrawStage

After overruling the colors the subitem is drawn. Unfortunately, the function uses a global array named lv4_Colors() of type SubItemColors. The following is from the frm1_Load() sub event, lv4 is the Ocx ListView in report mode.

Type SubItemColors
- Int Fore, Back
EndType
Local Int Rows = lv4.ListItems.Count
Local Int Columns = lv4.ColumnHeaders.Count

Global lv4_Colors(Rows, Columns) As SubItemColors

' The array MUST be initialized with the default ListView system colors CLR_NONE (= -1).
' Whenever a subitem cell color must be reset to the default value,
' simply set the Fore and Back members to CLR_NONE.
MemLFill ArrayAddr(lv4_Colors()), ArraySize(lv4_Colors()), CLR_NONE

' Cell (1,1) is left-top-most cell
lv4_Colors(1, 1).Fore = $23FFFF
lv4_Colors(1, 1).Back = $777777
lv4_Colors(2, 1).Fore = $FF
lv4_Colors(2, 1).Back = RGB(0, 255, 0)

The CLR_NONE value indicates the use of the ListView default colors. It is the clue to all drawing.

The integration in GB32 custom drawing might be complicated. If so ask me.

1 comment:

  1. Anonymous6/2/10

    Nice explanation, thank you that much, I will try to take it for my sources! -Joe

    ReplyDelete