News:

Masm32 SDK description, downloads and other helpful links
Message to All Guests

Main Menu

How to set "item" text color in a list-view control

Started by NoCforMe, June 12, 2012, 07:20:16 AM

Previous topic - Next topic

NoCforMe

Hi; back again after just a few weeks hiatus and the place looks completely different!

Anyhow, I'm trying to set the text of "items" in a list-view control without any success. Using Microsoft's rather convoluted nomenclature, the "items" in a listview are what I would call the "header" row cells, while "subitems" are everything below this (i.e., the contents of each column).

I can easily set text colors for "subitems", by capturing the NM_CUSTOMDRAW notification and responding appropriately. This is illustrated in the attached test program.

But no matter what I do, I can only seem to affect the color of subitems (contents of cells below the first, "header" row).

First of all, I can set the color of all text by using the LVM_SETTEXTCOLOR message. This sets the color globally to a single color value.

Setting the color of individual subitems is a little trickier. You need to respond to different flavors of NM_CUSTOMDRAW notifications differently. When you receive such a notification with the dwDrawStage field set to CDDS_PREPAINT | CDDS_ITEM | CDDS_SUBITEM, then you can set the text color (simply by stuffing the clrText field of the NMLVCUSTOMDRAW structure with the desired color. With me so far?

I set up my little demo program to log all the NM_CUSTOMDRAW notifications. Here's what I get when I run it:


NM_CUSTOMDRAW notification (dwDrawStage=CDDS_PREPAINT)
NM_CUSTOMDRAW notification (dwDrawStage=CDDS_PREPAINT | CDDS_ITEM)
NM_CUSTOMDRAW notification (dwDrawStage=CDDS_PREPAINT | CDDS_ITEM | CDDS_SUBITEM)
NM_CUSTOMDRAW notification (dwDrawStage=CDDS_PREPAINT | CDDS_ITEM | CDDS_SUBITEM)
NM_CUSTOMDRAW notification (dwDrawStage=CDDS_PREPAINT | CDDS_ITEM | CDDS_SUBITEM)
NM_CUSTOMDRAW notification (dwDrawStage=CDDS_PREPAINT | CDDS_ITEM)
NM_CUSTOMDRAW notification (dwDrawStage=CDDS_PREPAINT | CDDS_ITEM | CDDS_SUBITEM)
NM_CUSTOMDRAW notification (dwDrawStage=CDDS_PREPAINT | CDDS_ITEM | CDDS_SUBITEM)
NM_CUSTOMDRAW notification (dwDrawStage=CDDS_PREPAINT | CDDS_ITEM | CDDS_SUBITEM)
NM_CUSTOMDRAW notification (dwDrawStage=CDDS_PREPAINT | CDDS_ITEM)
NM_CUSTOMDRAW notification (dwDrawStage=CDDS_PREPAINT | CDDS_ITEM | CDDS_SUBITEM)
NM_CUSTOMDRAW notification (dwDrawStage=CDDS_PREPAINT | CDDS_ITEM | CDDS_SUBITEM)
NM_CUSTOMDRAW notification (dwDrawStage=CDDS_PREPAINT | CDDS_ITEM | CDDS_SUBITEM)


This seems to be what the MSDN documentation says I should expect (except that for each subitem notification, both CDDS_ITEM and CDDS_SUBITEM are set, which isn't what they say should happen.)

What I need is for someone with more knowledge than I about these controls to guide me.

One thing I discovered, using WinSpy++ (very useful tool) is that the header row of a list view control is actually a separate control. Where the listview is of class SysListView32, the header is of class SysHeader32. Perhaps this explains why all operations seem to be able to affect only the text color of everything except the header row ...

There's another small thing, which may be due to my misunderstanding of how to set up listviews. In my little demo app I create a 3x3 listview (excluding header). But instead of getting 3 columns, I get 4, the last of which extends to the right edge of the control, no matter how wide I make it. Is this the way they're supposed to work? I find that I can make it look like 3 columns if I carefully adjust the overall width of the control, but this seems like a kluge to me. Am I missing something here?

They're very useful controls, but man do they have a lot of "quirks"!

Thanks in advance for any help here.

fearless

So for listviews, the header is a seperate control.

For listviews the list item refers to the first cell in the row, the sub-items are all other cells in the same row. So subitem 0 is what we see as the 2nd cell in the row. So for 3x3 grid of data it would be:

item 0, subitem 0, subitem 1
item 1, subitem 0, subitem 1
item 2, subitem 0, subitem 1

So for that we need 3 columns.

LVM_GETHEADER message will get the listviews header. Might have to check the header reference on MSDN to see if you can make use of some of the message or notifications for that: http://msdn.microsoft.com/en-us/library/windows/desktop/ff485936%28v=vs.85%29.aspx, or perhaps sublassing the header proc and handling the drawing in that proc might be what you need to do.

Invoke SendMessage, hListview, LVM_GETHEADER, 0, 0 ; returns header handle in eax, maybe save it later for re-use
mov hHeaderListview, eax
Invoke SetWindowLong, eax, GWL_WNDPROC, OFFSET ListViewHeaderProc
mov pListviewHeaderOldProc, eax ; save old procedure, for later use in our sublass of header

ListViewHeaderProc  PROC hWin:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
    .if (whatever to test for ownerdraw stuff, do it here)
       
    .else ; otherwise call default header handling proc
        invoke CallWindowProc, pListviewHeaderOldProc, hWin, uMsg, wParam, lParam
    .endif
    ret
ListViewHeaderProc  ENDP


Rough example code off the top of my head :D maybe that will point you in the right direction though.

QuoteThere's another small thing, which may be due to my misunderstanding of how to set up listviews. In my little demo app I create a 3x3 listview (excluding header). But instead of getting 3 columns, I get 4, the last of which extends to the right edge of the control, no matter how wide I make it. Is this the way they're supposed to work? I find that I can make it look like 3 columns if I carefully adjust the overall width of the control, but this seems like a kluge to me. Am I missing something here?

Yeh, its pretty much the way you describe it, and i do that as well, adjust colum widths to align up close to the edge of the control overall. Think of the extra as windows saying, "here is where more columns will go possibly, so ill just go ahead and make some spare space here and leave it blank for the moment" ;)


NoCforMe

Quote from: fearless on June 12, 2012, 09:48:55 AM
So for listviews, the header is a seperate control.

[...]

LVM_GETHEADER message will get the listviews header.

Thank you! So my suspicions (via WinSpy) were correct. I think you've given me all the ammunition I need.

LVM_GETHEADER gets you a handle to the header control. You can then send it all kinda messages to make it do what you want.

Hmm; except that no messages have any effect on text color. So I guess you're right: you'd need to subclass the header control.

I'm guessing the following sequence:

  • Get the control handle (using LVM_GETHEADER).
  • Subclass the control (divert its window proc to yours).
  • Capture NM_CUSTOMDRAW notifications.
  • Process the notifications similarly to how I've handled the listview notifications.

fearless

Hopefully that might do it, let us know how you get on.

NoCforMe

Alas, I can't see any way to do this (remember, I'm trying to set text colors in a header control for a listview control).

According to this MSDN page, for header controls you get a pointer to a NMCUSTOMDRAW structure with the notification code. Problem is, unlike the NMLVCUSTOMDRAW structure, there are no fields to set text color. So I don't see how I can change text colors in the header.

What I need is a function (or message) that can change the color of a control, given its window handle. Unfortunately, all I can find is SetTextColor(), which takes a handle to a device context.

Anyone have any ideas? I guess I could handle WM_PAINT for the header, but how would I do that? How would I know which part of the control is being painted?

Gunner

You just answered your own question:
Quoteyou get a pointer to a NMCUSTOMDRAW structure
Quoteall I can find is SetTextColor(), which takes a handle to a device context
Now go back and look at the fields of NMCUSTOMDRAW, find it?  The NMCUSTOMDRAW structure contains a hdc field.  This is the handle you use to do any drawing to, or even pass to SetTextColor
~Rob

NoCforMe

What you said makes perfect sense. You raised my hopes. But when I tried it, no joy.

Here's what I did (this from the header-control's window proc):

MOV EAX, uMsg
CMP EAX, WM_NOTIFY
JE do_notify

dodefault:
INVOKE CallWindowProc, OldHdrProcPtr, hWin, uMsg, wParam, lParam
RET

do_notify:
MOV ECX, lParam ;ECX--> NMHDR structure.
CMP [ECX + NMHDR.code], NM_CUSTOMDRAW
JNE dodefault

; Handle NM_CUSTOMDRAW notification:
paintItem:
PUSH EBX
MOV EBX, [ECX + NMCUSTOMDRAW.hdc]
INVOKE SetTextColor, EBX, $colorWhite
INVOKE SetBkColor, EBX, $colorBlack
POP EBX
MOV EAX, CDRF_DODEFAULT
RET


Basically setting the foreground and background text colors each time I got an NM_CUSTOMDRAW notification. No effect.

I realize this is overkill (I should be much more selective about when I set the text colors), but the point is I couldn't get the colors to "stick" into the DC.

Of course, I may not be returning the correct values from these notifications ... back to the laboratory.

Gunner

You need to respond to the different "draw stages".  You do the drawing when dwDrawStage  == CDDS_PREPAINT.  This is where you can change the text color, font, etc...

No, I think it is done in response to CDDS_ITEMPREPAINT
~Rob

NoCforMe

Quote from: Gunner on June 13, 2012, 12:52:00 PM
You need to respond to the different "draw stages".  You do the drawing when dwDrawStage  == CDDS_PREPAINT.  This is where you can change the text color, font, etc...

No, I think it is done in response to CDDS_ITEMPREPAINT

Yes, I'm not sure either (and I'm not sure the MSDN documentation is correct on this point, based on capturing notifications in my test program).

But if I set the text color regardless of what the drawing stage is, as I'm doing indiscriminately in the code above, shouldn't it "stick" at some stage? As it is, it has no effect at all.

fearless

Im wondering if the header passes its draw notifications to its parent the listview, if thats the case, you might just have to subclass the listview and not the header, and with the LVM_GETHEADER you will have the handle to the header and can seperate the logic for drawing if its the header or the list items or subitems. maybe. not sure.

NoCforMe

OK, further research has revealed the following:

It does not appear that any messages intended specifically for the header control are sent to the listview-control proc. I checked for any such messages (where the handle was the header handle, not the listview handle) and found none. So it appears that normally (i.e., if the header isn't subclassed), these messages are handled internally by the GDI. In other words, the listview proc only receives messages for the listview control (which may include the header control, but not using a separate handle for that control).

(By the way, I'm not sure why I would want to subclass the listview as you suggested: don't all its messages come through its parent's  window proc anyhow?)

OK, when I subclass the header control, I get the following messages (sent to a log file):

Hdr. proc(): msg=WM_WINDOWPOSCHANGING
Hdr. proc(): msg=WM_WINDOWPOSCHANGING
Hdr. proc(): msg=WM_WINDOWPOSCHANGING
Hdr. proc(): msg=WM_WINDOWPOSCHANGING
Hdr. proc(): msg=WM_WINDOWPOSCHANGING
Hdr. proc(): msg=WM_WINDOWPOSCHANGING
Hdr. proc(): msg=WM_WINDOWPOSCHANGING
Hdr. proc(): msg=WM_WINDOWPOSCHANGING
Hdr. proc(): msg=WM_PAINT
Hdr. proc(): msg=WM_NCPAINT
Hdr. proc(): msg=WM_ERASEBKGND


Actually, these are only the "known" messages (i.e., those that are in our windows.inc file). There were a bunch of other messages captured, with values 1205, 1207. 120A, 120B and 120F (all hex). What these messages are, I have no idea.

Unfortunately, the header proc receives no WM_NOTIFY messages at all. So I don't see how I can possibly change colors in the header. It must be possible, though, since there are plenty of applications that do just that.

Any more ideas?

==============================================

Hmm, interesting: when I subclass the listview, here are the messages I get (ignore the "hdr. proc()"--that's just the text I used in my message-capturing code):

Hdr. proc(): unknown msg (0x1036)
Hdr. proc(): unknown msg (0x101B)
Hdr. proc(): unknown msg (0x101B)
Hdr. proc(): unknown msg (0x101B)
Hdr. proc(): unknown msg (0x1007)
Hdr. proc(): unknown msg (0x1007)
Hdr. proc(): unknown msg (0x1007)
Hdr. proc(): msg=WM_USER
Hdr. proc(): msg=WM_PAINT
Hdr. proc(): msg=WM_NOTIFY
Hdr. proc(): msg=WM_ERASEBKGND
Hdr. proc(): msg=WM_TIMER
Hdr. proc(): msg=WM_NCHITTEST
Hdr. proc(): msg=WM_SETCURSOR
Hdr. proc(): msg=WM_MOUSEMOVE
Hdr. proc(): msg=WM_NCHITTEST
Hdr. proc(): msg=WM_SETCURSOR
Hdr. proc(): msg=WM_MOUSEMOVE
Hdr. proc(): msg=WM_NCHITTEST
Hdr. proc(): msg=WM_SETCURSOR
Hdr. proc(): msg=WM_MOUSEMOVE
Hdr. proc(): msg=WM_NCHITTEST
Hdr. proc(): msg=WM_SETCURSOR
Hdr. proc(): msg=WM_MOUSEMOVE
Hdr. proc(): msg=WM_NCHITTEST
Hdr. proc(): msg=WM_SETCURSOR
Hdr. proc(): msg=WM_MOUSEMOVE
Hdr. proc(): msg=WM_NCHITTEST
Hdr. proc(): msg=WM_SETCURSOR
Hdr. proc(): msg=WM_MOUSEMOVE
Hdr. proc(): msg=WM_NCHITTEST
Hdr. proc(): msg=WM_SETCURSOR
Hdr. proc(): msg=WM_NCMOUSEMOVE

There's a different set of "unknown" messages at the top there.

Gunner

You don't have to subclass anything.  This is what I get:
QuoteNotify from Header (LVHeaders.asm, 208)
Notify from Header (LVHeaders.asm, 208)
Notify from Header (LVHeaders.asm, 208)
Notify from Header (LVHeaders.asm, 208)
Notify from Listview (LVHeaders.asm, 210)
Notify from Listview (LVHeaders.asm, 210)
Notify from Listview (LVHeaders.asm, 210)
Notify from Listview (LVHeaders.asm, 210)

Here is a sample:
_NOTIFY:
    mov     edi, lParam
    mov     eax, (NMHEADER ptr[edi]).hdr.code
    .if eax == NM_CUSTOMDRAW
        mov     ecx, (NMCUSTOMDRAW ptr[edi]).hdr.hwndFrom
        .if ecx == hHeader
            PrintText "Notify from Header"
           
        .elseif ecx == ListviewHandle
            PrintText "Notify from Listview"
        .endif
       
        mov     eax, (NMCUSTOMDRAW ptr[edi]).dwDrawStage
        .if eax == CDDS_PREPAINT
            ; *** HAVE TO *** return this here in in order
            ; to get other notifications.
            mov     eax, CDRF_NOTIFYITEMDRAW
            ret
       
        .elseif eax == CDDS_ITEMPREPAINT     
            ; do drawing here
           
            ; tell control we changed font
            mov     eax, CDRF_NEWFONT
            ; or you can return CDRF_DODEFAULT
            ret     
        .else
            mov     eax, CDRF_DODEFAULT
            ret
        .endif
    .endif


I cannot seem to get anything to change either for some reason.  I am going to keep playing with this.  Everything seems to say it can be done this way.  The other way is owner drawen and that is a lot of work just to change the text color.
~Rob

Gunner

Ok, without the owner draw flag this is from just playing around:


I would wrap this code up into a function and call it when needed with params.
So, the modify the code above and came up with this:
_NOTIFY:
    mov     edi, lParam
    mov     eax, (NMHEADER ptr[edi]).hdr.code
    .if eax == NM_CUSTOMDRAW
        mov     ecx, (NMCUSTOMDRAW ptr[edi]).hdr.hwndFrom
        .if ecx == hHeader       
        mov     eax, (NMCUSTOMDRAW ptr[edi]).dwDrawStage
        .if eax == CDDS_PREPAINT
            ; *** HAVE TO *** return this here in in order
            ; to get other notifications.
            mov     eax, CDRF_NOTIFYITEMDRAW
            ret
       
        .elseif eax == CDDS_ITEMPREPAINT     
            ; do drawing here
            mov     edx, (NMCUSTOMDRAW ptr[edi]).dwItemSpec
            mov     ebx, (NMCUSTOMDRAW ptr[edi]).hdc
            .if edx == 0 ; column 1
                mov     eax, (NMCUSTOMDRAW ptr[edi]).rc.left
                mov     HeadRect.left, eax
                mov     ecx, (NMCUSTOMDRAW ptr[edi]).rc.top
                mov     HeadRect.top, ecx
                mov     edx, (NMCUSTOMDRAW ptr[edi]).rc.right
                mov     HeadRect.right, edx
                mov     eax, (NMCUSTOMDRAW ptr[edi]).rc.bottom
                mov     HeadRect.bottom, eax
                invoke  FillRect, ebx, addr HeadRect, hBrush
                invoke  DrawEdge, ebx, addr HeadRect, EDGE_ETCHED, BF_RECT or BF_FLAT
                invoke  SetTextColor, ebx, Blue
                invoke  SetBkMode, ebx, TRANSPARENT
                mov     esi, (NMCUSTOMDRAW ptr[edi]).rc.left
                add     esi, 27
                mov     ecx, (NMCUSTOMDRAW ptr[edi]).rc.top
                add     ecx, 5               
                invoke  TextOut, ebx, esi, ecx, offset Col1HeaderText, sizeof Col1HeaderText


            .elseif edx == 1 ; column 2            
                invoke  SetTextColor, ebx, Blue
                invoke  SetBkColor, ebx, Green
                invoke  SetBkMode, ebx,OPAQUE
                mov     esi, (NMCUSTOMDRAW ptr[edi]).rc.left
                add     esi, 27
                mov     ecx, (NMCUSTOMDRAW ptr[edi]).rc.top
                add     ecx, 5
                invoke  TextOut, ebx, esi, ecx, offset Col2HeaderText, sizeof Col2HeaderText
                mov     eax, (NMCUSTOMDRAW ptr[edi]).rc.left
                mov     HeadRect.left, eax
                mov     ecx, (NMCUSTOMDRAW ptr[edi]).rc.top
                mov     HeadRect.top, ecx
                mov     edx, (NMCUSTOMDRAW ptr[edi]).rc.right
                mov     HeadRect.right, edx
                mov     eax, (NMCUSTOMDRAW ptr[edi]).rc.bottom
                mov     HeadRect.bottom, eax
                invoke  DrawEdge, ebx, addr HeadRect, EDGE_ETCHED, BF_RECT or BF_FLAT

            .elseif edx == 2 ; column 3
                invoke  SetTextColor, ebx, Green
                invoke  SetBkMode, ebx,TRANSPARENT
                mov     esi, (NMCUSTOMDRAW ptr[edi]).rc.left
                add     esi, 27
                mov     ecx, (NMCUSTOMDRAW ptr[edi]).rc.top
                add     ecx, 5
                invoke  TextOut, ebx, esi, ecx, offset Col3HeaderText, sizeof Col3HeaderText
                mov     eax, (NMCUSTOMDRAW ptr[edi]).rc.left
                mov     HeadRect.left, eax
                mov     ecx, (NMCUSTOMDRAW ptr[edi]).rc.top
                mov     HeadRect.top, ecx
                mov     edx, (NMCUSTOMDRAW ptr[edi]).rc.right
                mov     HeadRect.right, edx
                mov     eax, (NMCUSTOMDRAW ptr[edi]).rc.bottom
                mov     HeadRect.bottom, eax
                invoke  DrawEdge, ebx, addr HeadRect, EDGE_ETCHED, BF_RECT or BF_FLAT            
            .endif
               
            ;PrintDec edx
            ; tell control we did the drawing
            mov     eax, CDRF_SKIPDEFAULT
            ret     
        .else
            mov     eax, CDRF_DODEFAULT
            ret
        .endif
    .endif
    .endif

~Rob

Gunner

Ok, I looked into it and Custom Draw is simple actually.  Wrote a tutorial for customizing the Listview and Header without using Owner Draw, you can find tut and sample code here: http://www.dreamincode.net/forums/topic/283055-masm-custom-draw-listview-and-header/
~Rob

fearless