How to use the TListView OnCustomDrawXXX events
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:
- To paint the list view's background, handle the OnCustomDraw event.
- To configure the painting of a whole list item (a row in a report style list view). Handle the OnCustomDrawItem event.
-
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
6end;
7
8procedure TForm1.ListView1CustomDrawSubItem(Sender: TCustomListView;
9 Item: TListItem; SubItem: Integer; State: TCustomDrawState;
10 var DefaultDraw: Boolean);
11begin
12
13 if SubItem = 0 then Exit;
14
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;
7 Pl: TWindowPlacement;
8 begin
9
10 Header := SendMessage(ListView1.Handle, LVM_GETHEADER, 0, 0);
11
12 FillChar(Pl, SizeOf(Pl), 0);
13 Pl.length := SizeOf(Pl);
14 GetWindowPlacement(Header, @Pl);
15
16 Result := Pl.rcNormalPosition.Bottom - Pl.rcNormalPosition.Top;
17 end;
18
19var
20 BmpXPos, BmpYPos: Integer;
21 Bmp: TBitmap;
22 ItemRect: TRect;
23 TopOffset: Integer;
24begin
25
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
36
37 Bmp := Image1.Picture.Bitmap;
38
39 while BmpYPos < ARect.Bottom do
40 begin
41
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
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:
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
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.
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;
6begin
7 if Odd(Item.Index) then
8
9 ListView1.Canvas.Brush.Color := cStripe
10 else
11
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:
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
3const
4
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
16begin
17
18 SetLVColumnColour(0);
19end;
20
21procedure TForm1.ListView1CustomDrawSubItem(Sender: TCustomListView;
22 Item: TListItem; SubItem: Integer; State: TCustomDrawState;
23 var DefaultDraw: Boolean);
24
25begin
26
27
28
29 if SubItem = 0 then Exit;
30
31
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:
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
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
15 if SubItem = 0 then Exit;
16 if (SubItem = 3) and (Pos('(', Item.SubItems[2]) > 0) then
17
18 ListView1.Canvas.Font.Color := clRed
19 else
20
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:
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
4begin
5
6 fCurrentCol := Column.Index;
7
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;
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
4var
5 ColLeft: Integer;
6 ColBounds: TRect;
7 I: Integer;
8begin
9
10 ColLeft := ARect.Left;
11 for I := 0 to Pred(fCurrentCol) do
12 ColLeft := ColLeft + ListView_GetColumnWidth(ListView1.Handle, I);
13
14 ColBounds := Rect(
15 ColLeft,
16 ARect.Top,
17 ColLeft + ListView_GetColumnWidth(ListView1.Handle, fCurrentCol),
18 ARect.Bottom
19 );
20
21
22
23
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
5 ListView1.Canvas.Brush.Color := cShade
6 else
7
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
15begin
16 SetLVColumnShading(0);
17end;
18
19procedure TForm1.ListView1CustomDrawSubItem(Sender: TCustomListView;
20 Item: TListItem; SubItem: Integer; State: TCustomDrawState;
21 var DefaultDraw: Boolean);
22
23begin
24 if SubItem > 0 then
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:
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.
- 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".
- For bugs in the demo code see the
article-demo
project's README.md
file for details of how to report them.