Introduction
I've noticed a number of solutions for displaying alternating rows in different colours for ListView
s (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