Click here to Skip to main content
Click here to Skip to main content

ListView Alternating Row Colours (Using Windows API)

, 8 Oct 2007
Rate this:
Please Sign up or sign in to vote.
Display alternating row colours in a ListView (Report style) control
Screenshot - AltRowCol.jpg


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.


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,
    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 = 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,
    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 = 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:

  1. provides the message interception, and
  2. 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.


  • 8th October, 2007: First version


This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Web Developer
Australia Australia
No Biography provided

Comments and Discussions

GeneralMy vote of 2 PinsitebuilderShog930-Mar-10 8:28 
GeneralOr use NM_CUSTOMDRAW... Pinmembernm_1148-Oct-07 9:25 
AnswerRe: Or use NM_CUSTOMDRAW... Pinmemberbcmarkey10-Oct-07 4:03 
GeneralRe: Or use NM_CUSTOMDRAW... Pinmembernm_11410-Oct-07 10:35 
bcmarkey wrote:
you will still need to intercept the WM_ERASEBKGND message to paint the client area to the right of the last column


bcmarkey wrote:
intercepting the WM_NOTIFY message at the parent WndProc requires code to determine which control that particular message relates to. I am aware that Visual C++ has a number of 'handlers' to facilitate this, but my aim was to provide a generic solution which does not rely on a particular compiler.

The wParam of the WM_NOTIFY contains the control's ID, and the lParam is a LPNMHDR that has both the ID and the handle. It does not require any special 'handlers' or particular compiler.

bcmarkey wrote:
Further, as WM_CUSTOMDRAW is implemented in Custom Draw v4.71 or later of Comctl32.dll (packaged with IE4 or later), systems with earlier versions won't support it. Again, I was after a solution for all versions of Windows.

Not to be picky but I think NM_CUSTOMDRAW requires v4.70 (IE3). But yes, having it work on all versions might be helpful to some. You might want to change the article a bit to say that it is different from other articles because it works on all versions of Windows, not because it doesn't require any particular compiler.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140718.1 | Last Updated 8 Oct 2007
Article Copyright 2007 by bcmarkey
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid