Page 1 of 1

MASM - Custom Draw Listview and Header

#1 GunnerInc  Icon User is offline

  • "Hurry up and wait"
  • member icon




Reputation: 858
  • View blog
  • Posts: 2,287
  • Joined: 28-March 11

Post icon  Posted 17 June 2012 - 12:58 PM

What is Custom Draw?
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.

Attached Image

Attached Image

Attached Image
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)



Is This A Good Question/Topic? 1
  • +

Replies To: MASM - Custom Draw Listview and Header

#2 stackoverflow  Icon User is offline

  • D.I.C Addict
  • member icon

Reputation: 165
  • View blog
  • Posts: 545
  • Joined: 06-July 11

Posted 11 July 2012 - 01:14 PM

Very cool!
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1