Skip over navigation

How to use the TListView OnCustomDrawXXX events

Contents

Why this article?

The OnCustomDrawXXX event handlers of Delphi's TListView can be useful to make minor changes to the appearance of a list view control. They let developers avoid having to owner draw the control if they only want to make a few tweaks to its appearance. Using the event handlers means that Delphi (or Windows) continues to take responsibility for drawing list items and dealing with the highlight etc.

However, a quick Google search shows that the OnCustomDrawXXX events are not well documented out on the net. So, having experimented with the events using the list view's report style I decided to document my findings – hence this article.

Some "ifs and buts"

Before launching into the article proper, there are a few observations that need to be made about the limitations of list view custom drawing and of Delphi's implementation of it via the OnCustomDrawXXX events.

Don't expect too much

The custom drawing support facility is meant for minor customisations of the list view control. The facility was not designed for major customisations.

So don't expect too much from the OnCustomDrawXXX events – don't push them too far! If you want to perform a major customisation then you should owner-draw the control.

Delphi version differences

The code in this article has been tested with Delphi 4 and Delphi 7. There are subtle differences in behaviour between the two versions of the compiler that we need to take into account. These differences will be flagged up in the text. The code has not been tested with Delphi 5 or 6 so I am not clear when the behaviour changed.

Yep, the code really is so old that I started the experimentation with Delphi 4 and haven't checked it since Delphi 7 was current. One day I'll get round to checking it on modern versions of Delphi.

If you've got any experience with later compilers, please let me know - see the Feedback section to find out how.

Bugs in Delphi implementation

There can be some problems with font rendering when using some of solutions presented in this article. Investigations indicate that the problem appears to be with Delphi's implementation of the code that triggers OnCustomDrawXXX events. The problems seem to relate to the list view's Canvas property. You can get round some of the problems by using the Windows GDI API directly rather than depending on the Canvas property.

Once again, this observation refers to Delphi 4-7. Please tell me if you know if things have got any better.

Overview

Before diving into some code we'll briefly discuss what the different OnCustomDrawXXX events do.

OnCustomDraw

OnCustomDraw allows drawing on the background of the list view control. We should handle this event if we want to draw or paint anything on the control's background.

In this article we'll look at using this event handler to:

  • Draw a background bitmap.
  • Shade columns in the background of the control.

The event could also be used to paint the background colour of the control, but if we're using just a plain colour it's easier just to set the Color property of the list view and let Delphi handle the painting.

OnCustomDrawItem

Handling OnCustomDrawItem allows you to influence how a list item is drawn without having to perform the actual drawing yourself. Changes are made to the list view's Canvas property before Delphi (or Windows) draws the list item. For example, the font or font attributes can be changed, as can the brush colour used to paint the background of the list item.

Whatever changes are made are applied to the whole list item including the Caption and any SubItems. To demonstrate this event handler an example is provided where alternate list items are displayed in different colours. It is not advisable to directly paint any part of the list item. You should configure the control's Canvas and leave the painting to Delphi / Windows. Use owner-drawing if you want to paint all or part of the list item yourself.

OnCustomDrawSubItem

OnCustomDrawSubItem is triggered for each subitem of every list item. It allows sub items to be customised individually.

The event provides a SubItem parameter that identifies the column to be painted. Column zero is the list view's Caption column while columns >= 1 represent any subitems. To access the string associated with the SubItems property use SubItems[SubItem-1], providing SubItem >= 1.

Once again, this event should be used to configure the canvas rather than to draw on it.

OnCustomDrawSubItem will only be called by Delphi if OnCustomDrawItem is also handled. So if you need OnCustomDrawSubItem without OnCustomDrawItem you must also create a do nothing OnCustomDrawItem event handler.

Delphi Version Differences

In Delphi 4 OnCustomDrawSubItem is called for each of the columns starting at column 0, i.e. the column containing the list item's Caption. However, in Delphi 7 the event is only called for true sub items, i.e. from column 1.

Therefore in Delphi 4 OnCustomDrawSubItem can be used to customise all the columns while in Delphi 7 we must use OnCustomDrawItem to customise column 0 and OnCustomDrawSubItem to customise the other columns. Note that the Delphi 7 approach also works for Delphi 4.

Yet again, I'd welcome information about how later versions of Delphi work.

Some rules

Putting the above together we get the following rules:

  1. To paint the list view's background, handle the OnCustomDraw event.
  2. To configure the painting of a whole list item (a row in a report style list view). Handle the OnCustomDrawItem event.
  3. To configure the painting of all the "columns" in a list item separately (i.e. the caption and all the visible sub items), first handle OnCustomDrawItem event to configure how column 0 (the Caption) is to be displayed and then handle OnCustomDrawSubItem to configure the other columns.

    Here is some boilerplate code to use when handling both OnCustomDrawItem and OnCustomDrawSubItem:

      1procedure TForm1.ListView1CustomDrawItem(Sender: TCustomListView;
      2  Item: TListItem; State: TCustomDrawState;
      3  var DefaultDraw: Boolean);
      4begin
      5  // Process column 0 (the Caption) here
      6end;
      7
      8procedure TForm1.ListView1CustomDrawSubItem(Sender: TCustomListView;
      9  Item: TListItem; SubItem: Integer; State: TCustomDrawState;
     10  var DefaultDraw: Boolean);
     11begin
     12  // Ensure we ignore SubItem = 0 (in case called, e.g. in Delphi 4)
     13  if SubItem = 0 then Exit;
     14  // Process column 1 and higher here
     15end;
    Listing 1

Examples

Most of the rest of this article is spent in looking at some examples of what we can do by handling the OnDrawItemXXX events of list view controls that have the report style.

ListView_XXX Routines

Several of these examples use ListView_XXX routines. These routines are Delphi implementations of the C "macros" defined in Microsofts's Windows header files. They are provided in Delphi's CommCtrl unit. So make sure you add CommCtrl to your uses clause otherwise some examples will not compile.

Displaying a background bitmap

To display the bitmap we need only handle the OnCustomDraw event. In this example we will tile a bitmap in the display. To tile the bitmap we have to calculate the list view's background area and offset the bitmap to allow for any scrolling and the size of any header. Much of the code of the event handler is devoted to calculating these offsets. The code is presented in Listing 2 below.

  1procedure TForm1.ListView1CustomDraw(Sender: TCustomListView;
  2  const ARect: TRect; var DefaultDraw: Boolean);
  3
  4  function GetHeaderHeight: Integer;
  5  var
  6    Header: HWND;           // header window handle
  7    Pl: TWindowPlacement;   // header window placement
  8  begin
  9    // Get header window
 10    Header := SendMessage(ListView1.Handle, LVM_GETHEADER, 0, 0);
 11    // Get header window placement
 12    FillChar(Pl, SizeOf(Pl), 0);
 13    Pl.length := SizeOf(Pl);
 14    GetWindowPlacement(Header, @Pl);
 15    // Calculate header window height
 16    Result  := Pl.rcNormalPosition.Bottom - Pl.rcNormalPosition.Top;
 17  end;
 18
 19var
 20  BmpXPos, BmpYPos: Integer;  // X and Y position for bitmap
 21  Bmp: TBitmap;               // Reference to bitmap
 22  ItemRect: TRect;            // List item bounds rectangle
 23  TopOffset: Integer;         // Y pos where bmp drawing starts
 24begin
 25  // Get top offset where bitmap drawing starts
 26  if ListView1.Items.Count > 0 then
 27  begin
 28    ListView_GetItemRect(ListView1.Handle, 0, ItemRect, LVIR_BOUNDS);
 29    TopOffset := ListView_GetTopIndex(ListView1.Handle) *
 30      (ItemRect.Bottom - ItemRect.Top);
 31  end
 32  else
 33    TopOffset := 0;
 34  BmpYPos := ARect.Top - TopOffset + GetHeaderHeight;
 35  // Draw the bitmap
 36  // get reference to bitmap
 37  Bmp := Image1.Picture.Bitmap;
 38  // loop until bmp is past bottom of list view
 39  while BmpYPos < ARect.Bottom do
 40  begin
 41    // draw bitmaps across width of display
 42    BmpXPos := ARect.Left;
 43    while BmpXPos < ARect.Right do
 44    begin
 45      ListView1.Canvas.Draw(BmpXPos, BmpYPos, Bmp);
 46      Inc(BmpXPos, Bmp.Width);
 47    end;
 48    // move to next row
 49    Inc(BmpYPos, Bmp.Height);
 50  end;
 51end;
Listing 2

The event handler's ARect parameter provides the client area of the list view. Any header control is included in the client area, so the top of the display area for the list items begins at an offset equal to the height of the header control. We find the required height by calling the GetHeaderHeight subsidiary function. This routine gets the header window handle, retrieves its display rectangle and works out the height of the header from this. We also need to adjust the offset of the bitmap to allow for any scrolling in the list view. We get the index of the top item in the list and multiply this by the height of a single list item.

Having calculated the starting Y-offset for the image (and stored it in BmpYPos) we draw the bitmaps by using two nested loops. The outer loop displays the rows of bitamp, updating the Y-offset by the height of the bitmap. This loop terminates when the Y-offset is beyond the bottom of ARect. The inner loop handles tiling the bitmap across a row. We use BmpXPos to store the next X coordinate of the bitmap, beginning at ARect.Left and ending when BmpXPos goes beyond the right edge of ARect.

Delphi / Windows take care of displaying the list items after the background is drawn. By default list items are drawn with solid backgrounds, overwriting the newly drawn background, as can be seen in Figure 1 below:

Screen shot of a listview with a bitmap background & solid list item background
Figure 1: Listview with bitmap background & solid list item background

But what if we want the list items to appear transparently over the bitmap? We simply add the code shown in Listing 3 to the end of the OnCustomDraw event handler from Listing 2:

  1  // Ensure that the items are drawn transparently
  2  SetBkMode(ListView1.Canvas.Handle, TRANSPARENT);
  3  ListView_SetTextBkColor(ListView1.Handle, CLR_NONE);
  4  ListView_SetBKColor(ListView1.Handle, CLR_NONE);
Listing 3

This new code ensures list items are drawn with tranpsarent backgrounds. The effect is shown Figure 2.

Screen shot of a listview with a bitmap background & transparent list item background
Figure 2: Listview with bitmap background & transparent list item background

Drawing rows in alternating colors

This example is much simpler than the preceding one. We will draw alternating list items in different colours, emulating green and white line printer paper. This is achieved by handing just the OnCustomDrawItem event as Listing 4 shows:

  1procedure TForm1.ListView1CustomDrawItem(Sender: TCustomListView;
  2  Item: TListItem; State: TCustomDrawState;
  3  var DefaultDraw: Boolean);
  4const
  5  cStripe = $CCFFCC;  // colour of alternate list items
  6begin
  7  if Odd(Item.Index) then
  8    // odd list items have green background
  9    ListView1.Canvas.Brush.Color := cStripe
 10  else
 11    // even list items have window colour background
 12    ListView1.Canvas.Brush.Color := clWindow;
 13end;
Listing 4

The code is quite simple. We check whether the list item's index is odd or even. If it is odd we ensure that the list item's background is green, otherwise we use the system's window colour for the background. The required background colour is specified by setting the colour of the list view canvas' brush. Figure 3 shows the resulting list view:

Screenshot of a listview with alternating coloured rows
Figure 3: Listview with alternating coloured rows

Drawing columns in different colours

Now we move on to demonstrate the OnCustomDrawSubItem event handler. Here we will display each list view column in a different colour. Again, we use a four column list view in report mode. Listing 5 has the code:

  1procedure TForm1.SetLVColumnColour(ColIdx: Integer);
  2  // Sets the list view brush colour for the column
  3const
  4  // The colours for each list view column
  5  cRainbow: array[0..3] of TColor = (
  6    $FFCCCC, $CCFFCC, $CCCCFF, $CCFFFF
  7  );
  8begin
  9  ListView1.Canvas.Brush.Color := cRainBow[ColIdx];
 10end;
 11
 12procedure TForm1.ListView1CustomDrawItem(Sender: TCustomListView;
 13  Item: TListItem; State: TCustomDrawState;
 14  var DefaultDraw: Boolean);
 15  // Draw the "Caption" column
 16begin
 17  // Set the colour for column 0
 18  SetLVColumnColour(0);
 19end;
 20
 21procedure TForm1.ListView1CustomDrawSubItem(Sender: TCustomListView;
 22  Item: TListItem; SubItem: Integer; State: TCustomDrawState;
 23  var DefaultDraw: Boolean);
 24  // Draw the sub item columns
 25begin
 26  // Check if SubItem is 0 and exit
 27  // (e.g. Delphi 4 calls this event with SubItem = 0, 
 28  // while Delphi 7 starts with SubItem = 1
 29  if SubItem = 0 then Exit;
 30  // We set the background colour to the colour required for
 31  // the column per the SubItem parameter
 32  SetLVColumnColour(SubItem);
 33end;
Listing 5

Firstly we declare a private method, SetLVColumnColour, to set the colour of a given column to some preset value.

We handle the OnCustomDrawItem event to set the colour for the Caption column, by calling SetLVColumnColour for column 0. This code sets the background for the whole list item, but we override this effect for each sub-item column in the OnCustomDrawSubItem event handler. Recall that this event handler is passed the index of the column to be painted in its SubItem parameter. We pass SubItem to SetLVColumnColour to set the background colour of the column. We have used the boilerplate code presented in Listing 1 to ensure the code works with both Delphi 4 and Delphi 7: we ignore column 0 in OnCustomDrawSubItem because it has been dealt with in OnCustomDrawItem.

The resulting list view is shown in Figure 4:

Screenshot of a list view with different coloured columns
Figure 4: Listview with different column colours

Using different fonts in different columns

We can make each column use a different font or font style by handling the OnCustomDrawItem and the OnCustomDrawSubItem events. In this example we display the list item's Caption ("Date" column) in Comic Sans MS italic. We then present the remaining sub items in the standard list item's font, except that any negative values in the "Amount" column are to be displayed in red. Listing 6 shows the code:

  1procedure TForm1.ListView1CustomDrawItem(Sender: TCustomListView;
  2  Item: TListItem; State: TCustomDrawState;
  3  var DefaultDraw: Boolean);
  4  // Set column 0 to use Comic Sans MS italic
  5begin
  6  ListView1.Canvas.Font.Name := 'Comic Sans MS';
  7  ListView1.Canvas.Font.Style := [fsItalic];
  8end;
  9
 10procedure TForm1.ListView1CustomDrawSubItem(Sender: TCustomListView;
 11  Item: TListItem; SubItem: Integer; State: TCustomDrawState;
 12  var DefaultDraw: Boolean);
 13begin
 14  // Ensure SubItem 0 not updated here in Delphi 4
 15  if SubItem = 0 then Exit;
 16  if (SubItem = 3) and (Pos('(', Item.SubItems[2]) > 0) then
 17    // Display negative values in "Amount" column in red
 18    ListView1.Canvas.Font.Color := clRed
 19  else
 20    // Display all other sub item colums in black
 21    ListView1.Canvas.Font.Color := clBlack;
 22end;
Listing 6

Once again we modify the list view's Canvas property to get the desired result. We use the OnCustomDrawItem event handler to set the font required for column 0. The OnCustomDrawSubItem event handler then sets the font for sub items (columns 1 to 3). In column 3 we detect negative numbers – they begin with a '(' character – and set the font to red whenever we find one. The resulting list view displays as shown in Figure 5:

Screenshot of a listview with differing font styles
Figure 5: Listview with different font styles

Display a shaded column

The Explorer in Windows XP displays the sorted column in pale grey. In this final example we mimic Explorer's behaviour by handling all three OnCustomDrawXXX events to display a list view with a specified column shaded in pale grey. For the purposes of this example we don't implement sorting, we just shade a column when its header is clicked.

In addition to the OnCustomDrawXXX event handlers we also need to handle clicks on the column headers in the list view's OnColumnClick event handler. Listing 7 shows the OnColumnClick event handler:

  1procedure TForm1.ListView1ColumnClick(Sender: TObject;
  2  Column: TListColumn);
  3  // Handles column click: updates shaded column
  4begin
  5  // Updates the index of the shaded column
  6  fCurrentCol := Column.Index;
  7  // Redisplays list view with new column highlighted
  8  ListView1.Invalidate;
  9end;
Listing 7

This code simply records the index of the clicked column in a private form field and then invalidates the list view to redisplay it with the newly selected column highlighted. The private field is also used by the OnCustomDrawXXX event handlers and is defined in the form class definition as show in Listing 8:

  1private
  2  fCurrentCol: Integer; // currently selected column
Listing 8

At first sight it would appear that we merely need to handle the OnCustomDrawItem and OnCustomDrawSubItem events to set the column shading, in a similar manner to Listing 5. However a close examination of Figure 4 shows that the column shading does not extend down to the bottom of the list view – it ends at bottom of the last list item. There is also a margin between the header control and the first list item. We want the shading to occupy the whole column from the header control to the bottom of the list view control.

To overcome this problem we handle the OnCustomDraw event to draw the column shading from the top to bottom of list view's client area. Listing 9 defines the required OnCustomDraw event handler:

  1procedure TForm1.ListView1CustomDraw(Sender: TCustomListView;
  2  const ARect: TRect; var DefaultDraw: Boolean);
  3  // Displays shading in any area not occupied by list items
  4var
  5  ColLeft: Integer; // left edge of selected column
  6  ColBounds: TRect; // bounds of the selected column
  7  I: Integer;       // loops thru columns
  8begin
  9  // Calculate left side of selected column
 10  ColLeft := ARect.Left;
 11  for I := 0 to Pred(fCurrentCol) do
 12    ColLeft := ColLeft + ListView_GetColumnWidth(ListView1.Handle, I);
 13  // Calculate bounding rectangle of selected column
 14  ColBounds := Rect(
 15    ColLeft,
 16    ARect.Top,
 17    ColLeft + ListView_GetColumnWidth(ListView1.Handle, fCurrentCol),
 18    ARect.Bottom
 19  );
 20  // Shade the column
 21  // Delphi / Windows will overwrite this where there are list items but
 22  // this code ensures shading extends from top to bottom of the client
 23  // rectangle
 24  ListView1.Canvas.Brush.Color := cShade;
 25  ListView1.Canvas.FillRect(ColBounds);
 26end;
Listing 9

First we locate the position of the left hand edge of the selected column and store it in ColLeft. This is done by beginning at the left hand edge of the list view's display and adding the widths of all columns that precede the selected column, if any.

Next we calculate the bounds of the selected column. The bounding rectangle has top and bottom the same as those of the list view's display rectangle (per the ARect parameter). The left of the column is given by ColLeft and its width can be found by using the ListView_GetColumnWidth API call. Once we have the bounding rectangle of the selected column we fill it with the shading colour.

If we left it at that we would find that when Delphi / Windows draws the list items the shading under them would be overwritten. We overcome this by handling OnCustomDrawItem and OnCustomDrawSubItem to ensure that each column has the correct background colour. Listing 10 shows the code for the event handler, which by now should be very familiar with OnCustomDrawItem handling column 0 and OnCustomDrawSubItem handling columns >=1.

  1procedure TForm1.SetLVColumnShading(ColIdx: Integer);
  2begin
  3  if fCurrentCol = ColIdx then
  4    // given column is selected: shade it
  5    ListView1.Canvas.Brush.Color := cShade
  6  else
  7    // given column not shaded: ensure correct background used
  8    ListView1.Canvas.Brush.Color := ColorToRGB(ListView1.Color);
  9end;
 10
 11procedure TForm1.ListView1CustomDrawItem(Sender: TCustomListView;
 12  Item: TListItem; State: TCustomDrawState;
 13  var DefaultDraw: Boolean);
 14  // Shade the first column (index 0) if this is selected
 15begin
 16  SetLVColumnShading(0);
 17end;
 18
 19procedure TForm1.ListView1CustomDrawSubItem(Sender: TCustomListView;
 20  Item: TListItem; SubItem: Integer; State: TCustomDrawState;
 21  var DefaultDraw: Boolean);
 22  // Shade a sub item column if selected
 23begin
 24  if SubItem > 0 then // ensure not column 0 (Delphi 4)
 25    SetLVColumnShading(SubItem);
 26end;
Listing 10

Similar to Listing 5, we use a helper method – this time SetLVColumnShading – to set the required colours. This method is passed a column index and checks to see if the column is the selected one. If so it sets the background colour to the shading colour, otherwise it sets the background colour to the list view's Color property. The event handlers should be self-explanatory by now and won't be discussed further.

Figure 6 has a picture of the resulting display after clicking the "Item" column header:

Screenshot of a listview with a shaded column
Figure 6: Listview with a shaded column

Demo program

A demo program to accompany this article can be found in the delphidabbler/article-demos Git repository on GitHub.

You can view the code in the article-16 sub-directory. Alternatively download a zip file containing all the demos by going to the repository's landing page and clicking the Clone or download button and selecting Download ZIP.

See the demo's README.md file for details.

This source code is merely a proof of concept and is intended only to illustrate this article. It is not designed for use in its current form in finished applications. The code is provided on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.

The demo is open source. See the demo's LICENSE.md file for licensing details.

Summary

This article has summarised the purpose of the three list view OnCustomDrawXXX event handlers and has gone on to present several examples of using the event handlers with a report style list view.

The flaws in the Delphi implementation of the OnCustomDrawXXX event handlers were pointed out. The article also noted that the events are only intended for lightweight customisations of the list view. Owner drawing should continue to be used for major customisations.

A demo program that demonstrates the examples has also been made available.

Feedback

I hope you found this article useful.

If you have any observations, comments, or have found any errors there are two places you can report them.

  1. For anything to do with the article content, but not the downloadable demo code, please use this website's Issues page on GitHub. Make sure you mention that the issue relates to "article #16".
  2. For bugs in the demo code see the article-demo project's README.md file for details of how to report them.