Introduction
I've noticed a number of solutions for displaying alternating rows in different colours for ListViews (LVS_REPORT), each with varying levels of success. Most seem to rely on a particular compiler (Visual C++, C#, etc.). Here is a simple solution which uses only Windows API calls so it can be easily converted for use by a variety of compilers and languages.
Background
The code replies on the ability of your chosen programming language to intercept the WM_PAINT and WM_ERASEBKGND messages sent to the ListView control.
Let's take a look at the process involved;
Firstly, we need to identify a number of APIs for ListView controls.
-
ListView_SetTextBkColor (HWND, COLORREF)
Sets the text background colour. Be aware that this function does not redraw the entire background in the control in the new colour, but simply sets the colour used by any subsequent redraws.
-
ListView_GetTopIndex (HWND)
Retrieves the row index of the first visible row of the control.
-
ListView_GetCountPerPage (HNWD)
Retrieves the number of visible rows.
-
ListView_GetItemPosition (HWND, int, RECT *)
Retrieves the POINT coordinates of a given row.
Secondly, we need a few functions to allow us to work with update (or invalid) rectangles.
-
InvalidateRect (HWND, RECT *, BOOL)
Invalidates, or marks for updating, a rectangular area of a control.
-
GetUpdateRect HWND, RECT *, BOOL)
Retrieves the currently invalid update rectangle.
Using the Code
The WM_ERASEBKGND message is sent to a control each time the background (portion of the client area not used by the list columns) requires updating (or redrawing) on the screen.
To give the impression that the whole control is divided into different colours for alternating rows, the background needs to be redrawn accordingly, in response to this message.
We use a loop to iterate through the control from the first visible row to the last (even if only partially visible). For each row, depending on whether the row is odd or even, a rectangle is filled with the appropriate colour (HBRUSH).
void EraseAlternatingRowBkgnds (HWND hWnd, HDC hDC)
{
RECT rect; POINT pt;
int iItems,
iTop;
HBRUSH brushCol1, brushCol2;
brushCol1 = CreateSolidBrush (GetSysColor (COLOR_WINDOW));
brushCol2 = CreateSolidBrush (colorShade (GetSysColor (COLOR_WINDOW), 95.0));
GetClientRect (hWnd, &rect);
iItems = ListView_GetCountPerPage (hWnd);
iTop = ListView_GetTopIndex (hWnd);
ListView_GetItemPosition (hWnd, iTop, &pt);
for (int i=iTop ; i<=iTop+iItems ; i++) {
rect.top = pt.y;
ListView_GetItemPosition (hWnd, i+1, &pt);
rect.bottom = pt.y;
FillRect (hDC, &rect, (i % 2) ? brushCol2 : brushCol1);
}
DeleteObject (brushCol1);
DeleteObject (brushCol2);
}
The WM_PAINT message is sent to a control when a (rectangular) area requires updating on the screen. For example, another window overlaps the control. Similar to the previous process, we iterate through the visible rows, but this time, we only update those rows that intersect with the update rectangle. This is achieved by first setting the text background colour, invalidating the row area, then calling on the default action of the WM_PAINT message. This has the effect of redrawing the row, but using the background colour we specify.
void PaintAlternatingRows (HWND hWnd)
{
RECT rectUpd, rectDestin, rect; POINT pt;
int iItems,
iTop;
COLORREF c;
GetUpdateRect (hWnd, &rectUpd, FALSE);
CallWindowProc (
(FARPROC) PrevWndFunc, hWnd, WM_PAINT, 0, 0);
SetRect (&rect, rectUpd.left, 0, rectUpd.right, 0);
iItems = ListView_GetCountPerPage (hWnd);
iTop = ListView_GetTopIndex (hWnd);
ListView_GetItemPosition (hWnd, iTop, &pt);
for (int i=iTop ; i<=iTop+iItems ; i++) {
rect.top = pt.y;
ListView_GetItemPosition (hWnd, i+1, &pt);
rect.bottom = pt.y;
if (IntersectRect (&rectDestin, &rectUpd, &rect)) {
c = (i % 2) ? colorShade (GetSysColor (COLOR_WINDOW), 95.0) :
GetSysColor (COLOR_WINDOW);
ListView_SetTextBkColor (hWnd, c);
InvalidateRect (hWnd, &rect, FALSE);
CallWindowProc (
(FARPROC) PrevWndFunc, hWnd, WM_PAINT, 0, 0);
}
}
}
The function colorShade is used to provide an alternate colour:
COLORREF colorShade (COLORREF c, float fPercent)
{
return RGB ((BYTE) ((float) GetRValue (c) * fPercent / 100.0),
(BYTE) ((float) GetGValue (c) * fPercent / 100.0),
(BYTE) ((float) GetBValue (c) * fPercent / 100.0));
}
Now, in order to get our code working, there's one last thing we have to take care of.
We need to chain the default WndProc (Window Procedure) of the control with our routine which:
- provides the message interception, and
- gives us the method to force a default action
We do this with the help of the GetWindowLong and SetWindowLong APIs.
(where hWndListView is a HANDLE to the ListView control)
PrevWndFunc = (WNDPROC) GetWindowLong (hWndListView, GWL_WNDPROC);
Then set the default WndProc function to our WndProc (ListViewWndProc).
SetWindowLong (hWndListView, GWL_WNDPROC, (LONG) ListViewWndProc);
Note the global variable (WNDPROC prevWndFunc) which stores the default WndProc function pointer.
LRESULT CALLBACK ListViewWndProc (HWND hWnd, UINT iMessage, WPARAM wParam,
LPARAM lParam)
{
switch (iMessage) {
case WM_PAINT:
PaintAlternatingRows (hWnd);
return 0;
case WM_ERASEBKGND:
EraseAlternatingRowBkgnds (hWnd, (HDC) wParam);
return 0;
}
return CallWindowProc (
(FARPROC) PrevWndFunc, hWnd, iMessage, wParam, lParam);
}
Points of Interest
The example code is a complete Windows program (using only APIs) which creates a main window with a single ListView control. The colours used by the example are COLOR_WINDOW (which is the default colour for a ListView control) and a shade (95%) of COLOR_WINDOW.
This code works fine with Borland C++ (all versions) but should be easily ported to Visual C++ and the like.
The same procedure can be taken further. For example, to set columns in different colours, or even a combination of colours for different cells.
History
- 8th October, 2007: First version