Custom Draw is a very simple way to change the look of a control, whereas Owner Draw is fairly complex.
In Owner Draw, you are responsible for every aspect of painting the control.
Lets say all you want to do is change the font and back color of column 2 in the Header Control, with Owner Draw, you have to draw the whole header control just to change the display characteristics of column 2. You are responsible for getting/creating a DC, RECT coords etc...
Custom Draw, you are not responsible for every aspect. You can change only what you need and let the OS draw the rest of the control. The OS gives you a handle to a DC and RECT coords.
Custom Draw is not available for all controls. It is available for the following controls:
- Header
- Listview
- Rebar
- ToolBar
- ToolTip
- TrackBar
- Treeview
- Buttons
In my testing, it seems you need to use a manifest to get all the messages without hooking.
With Custom Draw, we can easily do things like this:
Column1, 2, and 4 - We change the Font, Text color, and Background color
Column3 we let the control do its own thing.



For controls that support Custom Draw, they send a NM_CUSTOMDRAW message at different points during the painting process. As with many NM_* messages, they are sent as WM_NOTIFY messages.
We will be using 2 different structures:
NMCUSTOMDRAW (for the header control)
NMLVCUSTOMDRAW (for the Listview)
The NMCUSTOMDRAW structure contains an important field for us, that is the dwDrawStage. When we receive a NM_CUSTOMDRAW message, this will tell us at what drawing stage the control is in.
CDDS_POSTERASE - After the control erases itself
CDDS_POSTPAINT - After the control paints itself
CDDS_PREERASE - Before the control erases itself
CDDS_PREPAINT - Before the control paints itself (This is what we will be using)
CDDS_ITEMPREPAINT - Before an item is drawn (this one also)
CDDS_ITEMPOSTPAINT - After an item is drawn
CDDS_ITEMPREERASE - Before an item is erased
CDDS_ITEMPOSTERASE - After an item is erased
CDDS_SUBITEM - Subitem is being drawn
So there are various stages in the painting process where we can change something.
How does the OS know what we painted ourselves and what it should paint and what stages we want to be notified of? We do this with return values for the above messages.
CDRF_DODEFAULT - Tells the control to draw itself and not send anymore NM_CUSTOMDRAW notifications for this paint cycle.
CDRF_NOTIFYITEMDRAW - This tells the control to notify us before and after it draws an item
CDRF_NOTIFYPOSTPAINT - The control will notify us when the paint cycle for the entire control is complete
CDRF_SKIPDEFAULT - The control will not paint itself, we did our own painting.
CDRF_DOERASE - Tells the control to draw only the background
CDRF_SKIPPOSTPAINT - Control will not draw a focus rectangle around item
CDRF_NEWFONT - The docs mention this if we change a font attribute (Color, face, etc..) I could not get this to work.
Now I said we can do this without hooks, but I will hook the Header controls Window proc to handle WM_LBUTTONDOWN and WM_LBUTTONUP so I can change the color of the Header item when it is clicked.
Ready? Go!
ProcWndMain proc uses edi esi hWin:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
mov eax,uMsg
.if eax==WM_CREATE
push hBrushBlue
pop hCol2Back
push Green
pop hCol2Text
invoke CreateWindowEx, WS_EX_CLIENTEDGE, \
offset szClsListView, \
NULL, \
WS_CHILD or WS_VISIBLE or LVS_REPORT or LVS_SINGLESEL, \
5, 5, \
WIDTH_MAIN_WINDOW - 15, HEIGHT_MAIN_WINDOW - 35, \
hWin, 0, \
hInst, NULL
mov hLV, eax
invoke SendMessage, eax, LVM_SETEXTENDEDLISTVIEWSTYLE, LVExtendedStyles, LVExtendedStyles
invoke SendMessage, hLV, LVM_GETHEADER, 0, 0
mov hHeader, eax
call InsertLVColumns
call InsertLVItems
invoke SetWindowLong, hHeader, GWL_WNDPROC, offset HeaderHook
mov hOrgHeaderProc, eax
For this tutorial I will only change the foreground and background of column 2 when clicked, so I create 2 global variables to hold the colors - hCol2Back and hCol2Text and set them to the "default" colors for the Header Item.
After we create the Listview, we get the handle of the Header control by sending the listview the message LVM_GETHEADER, Insert the columns, items, and hook the Header controls window proc.
Now for the "Magic". We do our customizing (or not) in response to WM_NOTIFY:
.elseif eax == WM_NOTIFY
mov edi, lParam
mov esi, (NMHDR ptr [edi]).code
.if esi == NM_CUSTOMDRAW
mov ecx, (NMCUSTOMDRAW ptr[edi]).hdr.hwndFrom
.if ecx == hHeader
mov eax, (NMCUSTOMDRAW ptr[edi]).dwDrawStage
.if eax == CDDS_PREPAINT
mov eax, CDRF_NOTIFYITEMDRAW
ret
.elseif eax == CDDS_ITEMPREPAINT
invoke DrawHeader
.if eax == 1
mov eax, CDRF_SKIPDEFAULT
ret
.else
mov eax, CDRF_DODEFAULT
ret
.endif
.else
mov eax, CDRF_DODEFAULT
ret
.endif
.elseif ecx == hLV
mov eax, (NMLVCUSTOMDRAW ptr[edi]).nmcd.dwDrawStage
.if eax == CDDS_PREPAINT
mov eax, CDRF_NOTIFYITEMDRAW
ret
.elseif eax == CDDS_ITEMPREPAINT
call ColorLVLines
mov eax, CDRF_DODEFAULT
ret
; mov eax, CDRF_NOTIFYSUBITEMDRAW
; ret
;
; .elseif eax == CDDS_ITEMPREPAINT or CDDS_SUBITEM
; call ColorLVColumns
; mov eax, CDRF_DODEFAULT
; ret
.else
mov eax, CDRF_DODEFAULT
ret
.endif
.endif
.endif
Let's look at customizing the Header control first:
mov edi, lParam
mov esi, (NMHDR ptr [edi]).code
.if esi == NM_CUSTOMDRAW
mov ecx, (NMCUSTOMDRAW ptr[edi]).hdr.hwndFrom
.if ecx == hHeader
mov eax, (NMCUSTOMDRAW ptr[edi]).dwDrawStage
.if eax == CDDS_PREPAINT
mov eax, CDRF_NOTIFYITEMDRAW
ret
.elseif eax == CDDS_ITEMPREPAINT
invoke DrawHeader
.if eax == 1
mov eax, CDRF_SKIPDEFAULT
ret
.else
mov eax, CDRF_DODEFAULT
ret
.endif
.else
mov eax, CDRF_DODEFAULT
ret
.endif
WM_NOTIFY messages contain a pointer to a NMHDR structure in lParam. First we check to see if NMHDR.code contains the NM_CUSTOMDRAW message we are interested in.
The NMCUSTOMDRAW structure also contains a pointer to a NMHDR structure so we could of changed:
mov esi, (NMHDR ptr [edi]).code
to
mov esi, (NMCUSTOMDRAW ptr[edi]).hdr.code if you wanted to. Personal preference I guess.
Now that .code contains NM_CUSTOMDRAW, we check to see what control the message is coming from:
mov ecx, (NMCUSTOMDRAW ptr[edi]).hdr.hwndFrom
.if ecx == hHeader
Ok, the Header control is notifying us it is going to do some kind of paint action, do we want to do anything or let the control do the painting?
Let's get the current paint stage:
mov eax, (NMCUSTOMDRAW ptr[edi]).dwDrawStage
.if eax == CDDS_PREPAINT
mov eax, CDRF_NOTIFYITEMDRAW
ret
Remember how I spoke of return codes? By returning CDRF_NOTIFYITEMDRAW in response to the PrePaint stage, we are telling the control to send more painting stage messages. In this case, we want to be notified when it is about to draw an item. If we do not return this, then we will not get any more notifications.
.elseif eax == CDDS_ITEMPREPAINT
invoke DrawHeader
.if eax == 1
mov eax, CDRF_SKIPDEFAULT
ret
.else
mov eax, CDRF_DODEFAULT
ret
.endif
.else
mov eax, CDRF_DODEFAULT
ret
.endif
Now that an item is about to be painted, let's step in and do our own drawing in the DrawHeader function. It takes one parameter - a pointer to the NMCUSTOMDRAW structure that is already in edi. So this technically is a FASTCALL function. In that function, if the item we are drawing is 0, 1, or 3 we return 1 from DrawHeader and return CDRF_SKIPDEFAULT in response to CDDS_ITEMPREPAINT which tells the control that we painted the item and not to do any painting of the item itself.
If the item == 2, we return 0 from DrawHeader and return CDRF_DODEFAULT in response to CDDS_ITEMPREPAINT which tells the control that we are not interested in painting the item, so go ahead and paint it for us as it normally would.
For all other painting stages we are not concerned about, we return CDRF_DODEFAULT and let the control do its thing.
DrawHeader proc uses ebx esi
local rHeader:RECT
local hFont:DWORD
mov edx, (NMCUSTOMDRAW ptr[edi]).dwItemSpec
mov ebx, (NMCUSTOMDRAW ptr[edi]).hdc
push edx
mov esi, (NMCUSTOMDRAW ptr[edi]).rc.left
add esi, 27
lea ecx, (NMCUSTOMDRAW ptr[edi]).rc
invoke MemCopy, ecx, addr rHeader, sizeof RECT
invoke SetBkMode, ebx, TRANSPARENT
pop edx
.if edx == 0
invoke SelectObject, ebx, hFontArialBlk
mov hFont, eax
invoke FillRect, ebx, addr rHeader, hBrushRed
invoke SetTextColor, ebx, Yellow
mov ecx, (NMCUSTOMDRAW ptr[edi]).rc.top
invoke TextOut, ebx, esi, ecx, offset szCol1, sizeof szCol1
invoke SelectObject, ebx, hFont
.elseif edx == 1
invoke SelectObject, ebx, hFontComicSans
mov hFont, eax
invoke FillRect, ebx, addr rHeader, hCol2Back ; hBrushBlue
invoke SetTextColor, ebx, hCol2Text ; Green
mov ecx, (NMCUSTOMDRAW ptr[edi]).rc.top
invoke TextOut, ebx, esi, ecx, offset szCol2, sizeof szCol2
invoke SelectObject, ebx, hFont
.elseif edx == 3
invoke SelectObject, ebx, hFontVerdana
mov hFont, eax
invoke FillRect, ebx, addr rHeader, hBrushBlack
invoke SetTextColor, ebx, 08080FFh
mov ecx, (NMCUSTOMDRAW ptr[edi]).rc.top
add ecx, 2
invoke TextOut, ebx, esi, ecx, offset szCol4, sizeof szCol4
invoke SelectObject, ebx, hFont
.else
xor eax, eax
ret
.endif
xor eax, eax
inc eax
ret
DrawHeader endp
mov edx, (NMCUSTOMDRAW ptr[edi]).dwItemSpec mov ebx, (NMCUSTOMDRAW ptr[edi]).hdc push edx mov esi, (NMCUSTOMDRAW ptr[edi]).rc.left add esi, 27 lea ecx, (NMCUSTOMDRAW ptr[edi]).rc invoke MemCopy, ecx, addr rHeader, sizeof RECT invoke SetBkMode, ebx, TRANSPARENT
dwItemSpec contains the index of the item about to be painted.
hdc contains a handle to the Headers DC
rc contains a pointer to a RECT structure of the item about to be painted
We copy the items RECT to our local RECT structure and change the background mode to TRANSPARENT so we see our background when we paint it.
Depending on which item is about to be painted edx == dwItemSpec, we change the font, text color, paint a background, and paint the text:
invoke SelectObject, ebx, hFontArialBlk mov hFont, eax invoke FillRect, ebx, addr rHeader, hBrushRed invoke SetTextColor, ebx, Yellow mov ecx, (NMCUSTOMDRAW ptr[edi]).rc.top invoke TextOut, ebx, esi, ecx, offset szCol1, sizeof szCol1 invoke SelectObject, ebx, hFont
Anything you can do with a DC handle, you can do here.
Now you have a custom Header control with very little work compared to Owner Draw. But lets add one more thing, changing the color of the item when it is clicked. This is were our Header hook comes in:
HeaderHook proc hWin:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
local hht:HDHITTESTINFO
local pt:POINT
mov eax,uMsg
.if eax==WM_LBUTTONDOWN
invoke GetCursorPos, addr pt
invoke ScreenToClient, hWin, addr pt
mov eax, pt.x
mov ecx, pt.y
mov hht.pt.x, eax
mov hht.pt.y, ecx
invoke SendMessage, hWin, HDM_HITTEST, 0, addr hht
mov ecx, hht.flags
.if ecx == HHT_ONDIVIDER
mov bOnDivider, TRUE
jmp PassThrough
.endif
mov eax, hht.iItem
.if eax == 1
mov dwColClicked, eax
push hBrushBlack
pop hCol2Back
push Yellow
pop hCol2Text
invoke InvalidateRect, hHeader, NULL, TRUE
.endif
.elseif eax ==WM_LBUTTONUP
.if bOnDivider == TRUE
mov bOnDivider, FALSE
jmp PassThrough
.endif
.if dwColClicked == 1
push hBrushBlue
pop hCol2Back
push Green
pop hCol2Text
invoke InvalidateRect, hHeader, NULL, TRUE
.endif
.else
PassThrough:
invoke CallWindowProc,hOrgHeaderProc, hWin, uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
HeaderHook endp
When the user clicks a Header item, we get the position of the cursor with GetCursorPos, convert coords to client coords with ScreenToClient and send the Header control the message HDM_HITTEST with those coords. When it returns we will know the which item was clicked among other things.
if a divider was clicked, we don't want the message interfering so we pass it on to windows.
mov ecx, hht.flags .if ecx == HHT_ONDIVIDER mov bOnDivider, TRUE jmp PassThrough .endif
In this sample, we are only going to change the colors for column 2 when clicked:
mov eax, hht.iItem .if eax == 1 mov dwColClicked, eax push hBrushBlack pop hCol2Back push Yellow pop hCol2Text invoke InvalidateRect, hHeader, NULL, TRUE .endif
We save which column was clicked to dwColClicked for use in WM_LBUTTONUP. We change the colors in the globals hCol2Back and hCol2Text to what we want to use for a button down, and tell the control to repaint itself with the new colors with InvalidateRect.
.elseif eax ==WM_LBUTTONUP
.if bOnDivider == TRUE
mov bOnDivider, FALSE
jmp PassThrough
.endif
.if dwColClicked == 1
push hBrushBlue
pop hCol2Back
push Green
pop hCol2Text
invoke InvalidateRect, hHeader, NULL, TRUE
.endif
When the left button is release, we just revert the colors back to default.
Go ahead and click on column 2 to see the effect. There are other messages that need to be handled. Click on Column2 and while holding down the mouse button, move the cursor to the Listview and release the mouse. The color does not change back. I will leave that to you to fix.
Now for the Listview:
To change the lines in a Listview, our handler looks like this:
.elseif ecx == hLV
mov eax, (NMLVCUSTOMDRAW ptr[edi]).nmcd.dwDrawStage
.if eax == CDDS_PREPAINT
mov eax, CDRF_NOTIFYITEMDRAW
ret
.elseif eax == CDDS_ITEMPREPAINT
call ColorLVLines
mov eax, CDRF_DODEFAULT
ret
It looks similar to the Header code, except we are not doing any drawing ourselves. Instead we are changing the colors that the Listview uses so we return CDRF_DODEFAULT
and to change the Columns in a Listview it looks like:
.elseif ecx == hLV
mov eax, (NMLVCUSTOMDRAW ptr[edi]).nmcd.dwDrawStage
.if eax == CDDS_PREPAINT
mov eax, CDRF_NOTIFYITEMDRAW
ret
.elseif eax == CDDS_ITEMPREPAINT
mov eax, CDRF_NOTIFYSUBITEMDRAW
ret
.elseif eax == CDDS_ITEMPREPAINT or CDDS_SUBITEM
call ColorLVColumns
mov eax, CDRF_DODEFAULT
ret
It is a bit different to change the columns. We need to be notified when a subitem is drawn so we return CDRF_NOTIFYSUBITEMDRAW in response to CDDS_ITEMPREPAINT
The code we use to change the colors of items and columns, is basically the same; get the index of the item that is about to be drawn, and change the color. Both of these functions take edi as a parameter which already contains a pointer to the NMLVCUSTOMDRAW structure
ColorLVLines proc
mov eax, (NMLVCUSTOMDRAW ptr[edi]).nmcd.dwItemSpec
xor edx, edx
mov ecx, 4
div ecx
.if edx == 0
mov (NMLVCUSTOMDRAW ptr[edi]).clrText, Green
mov (NMLVCUSTOMDRAW ptr[edi]).clrTextBk, Blue
.elseif edx == 1
mov (NMLVCUSTOMDRAW ptr[edi]).clrText, 08080FFh
mov (NMLVCUSTOMDRAW ptr[edi]).clrTextBk, Black
.elseif edx == 2
mov (NMLVCUSTOMDRAW ptr[edi]).clrText, Yellow
mov (NMLVCUSTOMDRAW ptr[edi]).clrTextBk, Red
.else
mov eax, hSysColorFront
mov (NMLVCUSTOMDRAW ptr[edi]).clrText, eax
mov ecx, hSysColorBack
mov (NMLVCUSTOMDRAW ptr[edi]).clrTextBk, ecx
.endif
ret
ColorLVLines endp
So, to change the colors of a listview item/colum, you need to fill in clrText and clrTextBack to the colors you want to use and let the control use those colors.
ColorLVColumns proc uses ebx
mov eax, (NMLVCUSTOMDRAW ptr[edi]).iSubItem
.if eax == 0
mov (NMLVCUSTOMDRAW ptr[edi]).clrText, Green
mov (NMLVCUSTOMDRAW ptr[edi]).clrTextBk, Blue
.elseif eax == 1
mov (NMLVCUSTOMDRAW ptr[edi]).clrText, 08080FFh
mov (NMLVCUSTOMDRAW ptr[edi]).clrTextBk, Black
.elseif eax == 3
mov (NMLVCUSTOMDRAW ptr[edi]).clrText, Yellow
mov (NMLVCUSTOMDRAW ptr[edi]).clrTextBk, Red
.else
mov eax, hSysColorFront
mov (NMLVCUSTOMDRAW ptr[edi]).clrText, eax
mov ecx, hSysColorBack
mov (NMLVCUSTOMDRAW ptr[edi]).clrTextBk, ecx
.endif
ret
ColorLVColumns endp
Just alternating colors here.
There are other cool things you can do easily in response to the other messages (Post paint, Pre erase, Post erase). Give them a try!
Attached File(s)
-
Listview - Custom Draw.zip (8.82K)
Number of downloads: 244







MultiQuote



|