Colour text in a List View
by Michael Hutton 22/01/2009
Getting windows to print colour text in a list view involes intercepting windows messages about the state of its current Paint cycle. When a common control (in this case the list view) is about to paint (draw) something it sends a message to its parent window informing it of what it is about to do. We have to intercept this NM_CUSTOMDRAW message and process it ourselves. Unfortunately, by the time the message has got through to BB4W to be handled by a ON SYS statement some of the data asscociated with the message may or may not still be present. Because of this volatility of the data asscociated with the message we have to intercept it before it gets to the ON SYS statement. At present, it is not possible to SUBCLASS the window using the SUBCLASS.BBC library and so we must process the message in our own message loop. This require some assembly language. This article describes intercepting and processing the messages.
Here is an example program you may wish to look at:
http://tech.groups.yahoo.com/group/bb4w/files/%22Temp%20Folder%22/MDCH/LVCOLOURTEXT.bbc
and you may want to cross reference this article with the SDK:
https://msdn.microsoft.com/en-us/library/windows/desktop/ff919569(v=vs.85).aspx
First, before we assemble the code we will need to define some variables/constants.
WM_NOTIFY = 78 NM_CUSTOMDRAW=-12 CDDS_POSTERASE=4 CDDS_POSTPAINT=2 CDDS_PREERASE=3 CDDS_PREPAINT=1 CDDS_ITEM=65536 CDDS_ITEMPOSTERASE=65540 CDDS_ITEMPOSTPAINT=65538 CDDS_ITEMPREERASE=65539 CDDS_ITEMPREPAINT=65537 CDDS_SUBITEM=&20000 CDRF_DODEFAULT=&0 CDRF_NOTIFYITEMDRAW=&20 CDRF_NOTIFYSUBITEMDRAW=&20 CDRF_NOTIFYITEMERASE=&80 CDRF_NOTIFYPOSTERASE=&40 CDRF_NOTIFYPOSTPAINT=&10 CDRF_NEWFONT=&2 CDRF_SKIPDEFAULT=&4 MDCH_SUBITEM = CDDS_SUBITEM OR CDDS_ITEMPREPAINT DIM lvi{mask%, \ \ iItem%, \ \ iSubItem%, \ \ state%, \ \ stateMask%, \ \ pszText%, \ \ cchTextMax%, \ \ iImage%, \ \ lParam%, \ \ iIndent%, \ \ iGroupId%, \ \ cColumns%, \ \ puColumns%, \ \ piColFmt%, \ \ iGroup% \ \ }
Although we don't need all of these I have included them for future use. The MDCH_SUBITEM is my application defined variable we will need later. I have included the lvi{} here but if you have already set up and filled a list view you will probably already have a definition of an LVITEM structure. You may have to change the name of the lvi{} to suit your program. Also, the assembly process will need to know the handle to your list view. This is the value returned from the FN_createwindow( “SysListView32”…) function you use to set up the list view. In this article I will use hList% but you may need to change this name.
Now for the PROCassemble
DEF PROCassemble LOCAL lvit{}, Textbuffer%, gap%, code, L% DIM lvit{}=lvi{}, Textbuffer% 256, gap% 2048, code 500, L% -1 FOR pass% = 8 TO 10 STEP 2 P% = code [OPT pass% ; .oldwndproc dd 0 : .newwndproc cmp dword [esp+8],WM_NOTIFY jz wm_notify jmp [oldwndproc] ; .wm_notify mov eax,[esp+16] cmp dword [eax+8],NM_CUSTOMDRAW jz nm_customdraw jmp [oldwndproc] ; .nm_customdraw ;REM Query the NMLVCUSTOMDRAW.DrawStage member cmp dword [eax+12],CDDS_PREPAINT jz prepaint cmp dword [eax+12],CDDS_ITEMPREPAINT jz itemprepaint cmp dword [eax+12],MDCH_SUBITEM jz subitemprepaint jmp [oldwndproc] ;REM respond to the message by returning CDRF_NOTIFYITEMDRAW ;REM We will now get CDDS_ITEMPREPAINT messages for *this* paint cycle .prepaint mov eax,CDRF_NOTIFYITEMDRAW ret ;REM Respond to the CDDS_ITEMPREPAINT with a ;REM request for messages about SUBITEMPREPAINT .itemprepaint mov eax,CDRF_NOTIFYSUBITEMDRAW ret .subitemprepaint mov dword [eax+52],&9999 ;grey background cmp dword [eax+56],1 jz subitem1 cmp dword [eax+56],2 jz green ret .subitem1 push eax ;REM initialise the lvi{} structure mov dword [^lvit.iSubItem%], 1 mov dword [^lvit.pszText%], Textbuffer% mov dword [^lvit.cchTextMax%], 256 push lvit{} push [eax+36] ;REM NMLVCD.dwItemSpec push LVM_GETITEMTEXT push hList% call "SendMessage" cmp byte [Textbuffer%],&2D pop eax jz red jmp blue .red mov dword [eax+48],&FF ret .green mov dword [eax+48],&FF00 ret .blue mov dword [eax+48],&FF0000 ret ] NEXT SYS "GetWindowLong", @hwnd%, -4 TO !oldwndproc SYS "SetWindowLong", @hwnd%, -4, newwndproc ENDPROC
I will now go through this code line by line explaining what each line does. This code is from the example program and you will probably want to modify it to colour text according to your application. The example program should show you a list view with three columns with a coloured background, letters in the first column, numbers in the second with negative numbers coloured red, and some numbers in the third column all coloured green.
LOCAL lvit{}, Textbuffer%, gap%, code, L% DIM lvit{}=lvi{}, Textbuffer% 256, gap% 2048, code 500, L% -1 FOR pass% = 8 TO 10 STEP 2 P% = code [OPT pass%
First we define a space for a LVITEM structure which we will use, an address for a Text Buffer, a gap between the variables and the code, the code, and a variable (L%) as the LIMIT variable when assembling the code.
The DIM statement now defines the structure and reserves the space needed for the code.
FOR…[OPT pass% … is a standard way of performing two pass assembly. We need two pass assembly because we use forward reference labels. (See the manual or your favourite ASM reference for more details.)
.oldwndproc dd 0
We reserve 4 bytes of memory at oldwndproc for the address of the original Wndproc procedure. The WndProc is basically a procedure (in assembly language) which all Windows have to process messages which are sent to them. If we don't find a message that we want then it is essential to let the window do its default processing of every other message. We therefore will want to tell the program to go and do its stuff at oldwndproc.
.newwndproc cmp dword [esp+8],WM_NOTIFY jz wm_notify jmp [oldwndproc]
Here we define an address of our new WndProc which we will tell the window to go to before jumping to the standard message processing in the oldwndproc. Our task is to find out what the message is and to act on it if we want to. If you look at the SendMessage function in the SDK you will see it has four parameters. The parameters are on the stack (which is at esp) and the parameters are at esp+4, esp+8, esp+12 and esp+16. We want to know the ID of the message sent. If you look at the SendMessage function it tells us that it is the second parameter passed, so we need to examine esp+8. The Paint messages are sent to our window from the List View as WM_NOTIFY messages. We therefore want to query the parameter and see if it equals WM_NOTIFY.
The cmp… statement subtracts the value WM_NOTIFY (which we have already defined) from the dword (four bytes) in esp+8. Notice the use of the square brackets. The cmp statement doesn't store the result of this subtraction. It just performs the subtraction and then sets the condition flags according to the result. The jz statement now tests the zero flag. If the result of the cmp statement was zero (ie both the values were the same) it will jump to the label specified - wm_notify. (NB all lower case). We have found a WM_NOTIFY message!
If we didn't find a WM_NOTIFY message we now need to tell the program to go and process the message found as it would normally do without our intervention. The jmp [oldwndproc] will jump to the address stored at oldwndproc. If we didn't include the square brackets we would jump to the label oldwndproc which would most likely crash our program. Don't worry, we haven't found out what the address of the oldwndproc is yet, but we will.
.wm_notify mov eax,[esp+16] cmp dword [eax+8],NM_CUSTOMDRAW jz nm_customdraw jmp [oldwndproc]
When we have identified WM_NOTIFY messages we wanted to intercept we need to extract more information from the message to determine if it is a NM_CUSTOMDRAW notification message. The fourth parameter of the WM_NOTIFY message is a pointer to a NMHDR structure (Actually, in the case of a list view it is an NMLVCUSTOMDRAW structure) which windows has made for us. The NMHDR part of the NMLVCUSTOMDRAW structure contains information about the notification message. We want to know the value of the third member - NMHDR.code . So now we move the address of the NMLVCUSTOMDRAW structure which is still at esp+16 into a general register, we will use eax. mov eax,[esp+16]. We now compare the third member of the NMHDR structure, eax+8 (first is at eax, second at eax+4 and third at eax+8 - they are all four byte variables) to NM_CUSTOMDRAW and if it is NM_CUSTOMDRAW jump to a label which will process the message further. If it isn't, it is not what we are looking for and we jump to the windows default message processing with a jmp [oldwndproc].
.nm_customdraw ;REM Query the NMLVCUSTOMDRAW.DrawStage member cmp dword [eax+12],CDDS_PREPAINT jz prepaint cmp dword [eax+12],CDDS_ITEMPREPAINT jz itemprepaint cmp dword [eax+12],MDCH_SUBITEM jz subitemprepaint jmp [oldwndproc]
Now we have identified the NM_CUSTOMDRAW message we need to process it and reply to it so that we can manipulate the paint cycle. The list view (as all common controls will do with Windows 2000 and beyond) will send a message (CDDS_PREPAINT) to our main window every time it starts a new Paint cycle. If we don't respond to this message windows will go and do all its usual painting but we can respond to it and request more notification messages about certain Paint stages or DrawStages. We can then do our own drawing or manipulate the default windows drawing.
In the SDK you will see under “Custom Draw With List-View Controls”:
1. The first NM_CUSTOMDRAW notification will have the dwDrawStage member of the associated NMCUSTOMDRAW structure set to CDDS_PREPAINT. Return CDRF_NOTIFYITEMDRAW.
We need to find out the value of the NM_CUSTOMDRAW.dwDrawStage member. Looking at the SDK tells us that the dwDrawStage member is the fourth member after a NMHDR structure. This then is at the address of NMLVCUSTOMDRAW plus 12 ie eax+12. Agin we compare this value with CDDS_PREPAINT and if it is the same jump to a place where we respond to the message. The other comparisons are needed later to intercept the messages about drawing an item or a subitem. Notice that agian if the message isn't the one we want we must go to the default windows procedure for processing messages.
.prepaint mov eax,CDRF_NOTIFYITEMDRAW ret
To respond to the PREPAINT notification we put CDRF_NOTIFYITEMDRAW into eax and return from assembly. The Listview will now send us messages just before it is about to draw an ITEM. However, it will only do this for our current paint cycle. We must intercept every CDDS_PREPAINT message for every paint cycle to be able to continue manipulating the listview.
But we want to manipulate each SUBITEM therefore;
2. You will then receive an NM_CUSTOMDRAW notification with dwDrawStage set to CDDS_ITEMPREPAINT. If you specify new fonts or colors and return CDRF_NEWFONT, all subitems of the item will be changed. If you want instead to handle each subitem separately, return CDRF_NOTIFYSUBITEMDRAW.
We need to request notification messages when the Listview is about to draw a SUBITEM. We now need to respond to the CDDS_ITEMPREPAINT message. Again we do this:
.itemprepaint mov eax,CDRF_NOTIFYSUBITEMDRAW ret
We should now get a NM_CUSTOMDRAW draw notification before the listview draws any SUBITEM. As the SDK says:
3. If you returned CDRF_NOTIFYSUBITEMDRAW in the previous step, you will then receive an NM_CUSTOMDRAW notification for each subitem with dwDrawStage set to CDDS_SUBITEM | CDDS_ITEMPREPAINT. To change the font or color for that subitem, specify a new font or color and return CDRF_NEWFONT.
To change the colour of a subitem then, we want to find a NM_CUSTOMDRAW message with the NMLVCUSTOMDRAW.dwDrawStage value set to CDDS_SUBITEM | CDDS_ITEMPREPAINT (that's CDDS_SUBITEM OR CDDS_ITEMPREPAINT). We have previously defined MDCH_SUBITEM as CDDS_SUBITEM OR CDDS_ITEMPREPAINT and this is what the third comparison in the nm_customdraw routine is looking for.
.subitemprepaint mov dword [eax+52],&9999 ;grey background cmp dword [eax+56],1 jz subitem1 cmp dword [eax+56],2 jz green ret
Here we are. All we need to do now is manipulate the NMLVCUSTOMDRAW structure that windows has sent us to make it draw coloured text. Most of this routine is specific to the example program LVCOLOURTEXT.BBC to test which column we are in and the value of the SUBITEM being drawn. You will want to change this according to your application.
Remember, if we have got here, eax still contains the address of the NMLVCUSTOMDRAW structure. Windows defines this as:
typedef struct tagNMLVCUSTOMDRAW { NMCUSTOMDRAW nmcd; COLORREF clrText; COLORREF clrTextBk; #if (_WIN32_IE >= 0x0400) int iSubItem; #endif #if (_WIN32_IE >= 0x560) DWORD dwItemType; // Item Custom Draw COLORREF clrFace; int iIconEffect; int iIconPhase; int iPartId; int iStateId: // Group Custom Draw RECT rcText; UINT uAlign; #endif } NMLVCUSTOMDRAW, *LPNMLVCUSTOMDRAW;
Yuk! Translating this to BB4W gives:
DIM NMLVCUSTOMDRAW{ \
\ nmcd{}=NMCUSTOMDRAW{}, \
\ clrText%, \
\ clrTextBk%, \
\ iSubItem%, \
\ ItemType%, \
\ clrFace%, \
\ iIconEffect%, \
\ iIconPhase%, \
\ iPartId%, \
\ iStateId%, \
\ rcText{}=RECT{}, \
\ uAlign% }
where:
DIM RECT{l%,t%,r%,b%} DIM NMHDR{hwndFrom%, idFrom%, code%} DIM NMCUSTOMDRAW{ \ \ hdr{}=NMHDR{}, \ \ DrawStage%, \ \ hdc%, \ \ rc{}=RECT{}, \ \ ItemSpec%, \ \ ItemState%, \ \ ItemlParam% }
We don't need to include these definitions, they are given for reference only but we do need to know the relative address of the different members to be able to change them.
To change the background colour of a SUBITEM we need to change the NMLVCUSTOMDRAW.clrTextBk% member. If we count up the bytes each member occupies of the NMLVCUSTOMDRAW structure we find that clrTextBk% is at byte 52. Eh? Surely it's at 8 where nmcd{} is at byte 0? Well, we need to include all the bytes of the substructures aswell, so nmcd{} is made of 48 bytes (including their hdr{} and rc{} substructures…) and clrTextBk% is after clrTextBk%… we now need to change the value to a COLORREF value which is in the form &00BBGGRR where RR,GG and BB are the byte values of the Red, Green and Blue components. So for example to change it to a vomit-like greeny brown we change eax+52 to &9999.
Next, we'd like to change the colour of the text in the second of third column so we have to find out which column (SUBITEM) we are currently drawing. We query the value of NMLVCUSTOMDRAW.iSubItem%. It is a zero based index of columns ie the first column is 0. If it is column 1 jump to subitem1 routine otherwise if it is column 2 change it to green.
.red mov dword [eax+48],&FF ret .green mov dword [eax+48],&FF00 ret .blue mov dword [eax+48],&FF0000 ret
Here we put the COLORREF value we want to display into the NMLVCUSTOMDRAW.clrText%. Easy.
A trickier bit, however, is finding out the value of the subitem. Here is the code.
.subitem1 push eax ;REM initialise the lvi{} structure mov dword [^lvit.iSubItem%], 1 mov dword [^lvit.pszText%], Textbuffer% mov dword [^lvit.cchTextMax%], 256 push lvit{} push [eax+36] ;REM NMLVCD.dwItemSpec push LVM_GETITEMTEXT push hList% call "SendMessage" cmp byte [Textbuffer%],&2D pop eax jz red jmp blue
When we started the procedure we DIM'd space for a copy of the lvi{} and we called this lvit{}. As we made the name lvit{} LOCAL it won't clash with any structure in the main program, but notice we didn't make the space LOCAL. This ensures that the memory stays on the heap and can be referenced by the machine language but the name addressing it has gone and can't be accessed apart from within the machine code.
To get the value of the text we have to send a message to the ListView as we would do in BB4W with a SendMessage API call. We also have to define some values of a LVITEM structure on which the message can act. In BASIC we would say
lvit.iSubItem% = 1 lvit.psxText% = TextBuffer% lvit.cchTextMax% = 256 SYS "Sendmessage", hList%, LVM_GETITEMTEXT, NMLVCD.dwItemSpec, lvit{}
The NMLVCD.dwItemSpec contains the item number ie the row, and lvit{} is a LVITEM structure with the values we want to change.
This translates to:
mov dword [^lvit.iSubItem%], 1 mov dword [^lvit.pszText%], Textbuffer% mov dword [^lvit.cchTextMax%], 256 push lvit{} push [eax+36] ;REM NMLVCD.dwItemSpec push LVM_GETITEMTEXT push hList% call "SendMessage"
First, notice that we first save eax (the address of the NMLVCUSTOMDRAW{}) on the stack by push eax. This is because the “SendMessage” function will return a value in eax and overwrite it if we don't. We then check to see if the first character in the textbuffer (where the string was sent to) is “-” (&2D). If it is we know that it represents a negative value. As before, the zero flag will be set if it is. We then restore eax by “pop eax” and test the zero flag. If it is zero (ie value is negative) we jump to the red label which will write the COLORREF red to the clrText% member of the NMLVCUSTOMDRAW structure.
The final thing we must do is to finish the assembly process and then tell our machine code where the oldwndproc address is. This is done with the following lines.
] NEXT SYS "GetWindowLong", @hwnd%, -4 TO !oldwndproc SYS "SetWindowLong", @hwnd%, -4, newwndproc ENDPROC
And that should be it! I hope this is not too long winded. Obviously all the mistakes are my own. I hope people may find this useful. I would love to hear about any comments or experiences people have trying to use the code.
Michael.