ListView Alternating Row Colours (Using Windows API)
Display alternating row colours in a ListView (Report style) control

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)
// re-draw row backgrounds with the appropriate background colour
{
RECT rect; // row rectangle
POINT pt;
int iItems,
iTop;
HBRUSH brushCol1, // 1st colour
brushCol2; // 2nd colour
// create coloured brushes
brushCol1 = CreateSolidBrush (GetSysColor (COLOR_WINDOW));
brushCol2 = CreateSolidBrush (colorShade (GetSysColor (COLOR_WINDOW), 95.0));
// get horizontal dimensions of row
GetClientRect (hWnd, &rect);
// number of displayed rows
iItems = ListView_GetCountPerPage (hWnd);
// first visible row
iTop = ListView_GetTopIndex (hWnd);
ListView_GetItemPosition (hWnd, iTop, &pt);
for (int i=iTop ; i<=iTop+iItems ; i++) {
// set row vertical dimensions
rect.top = pt.y;
ListView_GetItemPosition (hWnd, i+1, &pt);
rect.bottom = pt.y;
// fill row with appropriate colour
FillRect (hDC, &rect, (i % 2) ? brushCol2 : brushCol1);
}
// cleanup
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)
// re-draw rows with the appropriate background colour
{
RECT rectUpd, // rectangle to update
rectDestin, // temporary storage
rect; // row rectangle
POINT pt;
int iItems,
iTop;
COLORREF c; // temporary storage
// get the rectangle to be updated
GetUpdateRect (hWnd, &rectUpd, FALSE);
// allow default processing first
CallWindowProc (
(FARPROC) PrevWndFunc, hWnd, WM_PAINT, 0, 0);
// set the row horizontal dimensions
SetRect (&rect, rectUpd.left, 0, rectUpd.right, 0);
// number of displayed rows
iItems = ListView_GetCountPerPage (hWnd);
// first visible row
iTop = ListView_GetTopIndex (hWnd);
ListView_GetItemPosition (hWnd, iTop, &pt);
for (int i=iTop ; i<=iTop+iItems ; i++) {
// set row vertical dimensions
rect.top = pt.y;
ListView_GetItemPosition (hWnd, i+1, &pt);
rect.bottom = pt.y;
// if row rectangle intersects update rectangle then it requires
// re-drawing
if (IntersectRect (&rectDestin, &rectUpd, &rect)) {
// change text background colour accordingly
c = (i % 2) ? colorShade (GetSysColor (COLOR_WINDOW), 95.0) :
GetSysColor (COLOR_WINDOW);
ListView_SetTextBkColor (hWnd, c);
// invalidate the row rectangle then...
InvalidateRect (hWnd, &rect, FALSE);
// ...force default processing
CallWindowProc (
(FARPROC) PrevWndFunc, hWnd, WM_PAINT, 0, 0);
}
}
}
The function colorShade
is used to provide an alternate colour:
COLORREF colorShade (COLORREF c, float fPercent)
// create a lighter shade (by fPercent %) of a given colour
{
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)
// subclassed window procedure
{
switch (iMessage) {
case WM_PAINT:
// intercept the WM_PAINT message which is called each time an area
// of the control's client area requires re-drawing
PaintAlternatingRows (hWnd);
return 0;
case WM_ERASEBKGND:
// intercept the WM_ERASEBKGRN message which is called each time an area
// of the control's client area background requires re-drawing
EraseAlternatingRowBkgnds (hWnd, (HDC) wParam);
return 0;
}
// continue with default message processing
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