4

I have an owner drawn TListBox (lbVirtualOwnerDraw), whose content gets updated dynamically (there can be as high as 10 updates in a second). There can be up to 300 items in the list box at a time. Each item may have about 5 lines of text and an image associated with it. Whenever an item is refreshed, I have to refresh (or invalidate) the TListBox so that the ListBoxDrawItem will be invoked by the VCL framework. But this adversely affects overall performance because of all the redundant repainting. So my question is:

  1. Is there a way to invalidate only a small portion of the canvas which contains the drawing of one item or one of its parts? (e.g., rectangle containing one line of text or the bitmap).

  2. How can we handle such a selective invalidate rectangle in Draw Item? If it were possible to pass an integer as part of the Refresh or invalidate I could use that in DrawItem to determine what to refresh.

  3. Is there a way to find if an item is visible at all on a TListBox (by index)?

Thanks in advance!

ssh
  • 943
  • 1
  • 14
  • 23

1 Answers1

12

You can use the InvalidateRect api to invalidate a part of a window. To find the area an item occupies you can use the ItemRect method of the ListBox. For instance to invalidate the 4th item:

var
  R: TRect;
begin
  R := ListBox1.ItemRect(3);
  InvalidateRect(ListBox1.Handle, @R, True);
end;

(or 'False' as 'bErase' of 'InvalidateRect', see its documentation). To invalidate only the bitmap or text, modify the rectangle accordingly before passing to InvalidateRect.


You cannot pass an index or any kind of user data to refresh or invalidate. In the painting routine you have to determine the item you're drawing depending on the location, or use global variables if it's absolutely necessary. But you won't need that, if you invalidate part of only one item, OnDrawItem will be called for only that item. And in any case, don't worry too much about drawing non-invalidated items, since there won't be any actual drawing outside of the update region, you won't have any significant performance hit (see the 3rd paragraph here).


To determine if an item is visible you would be starting from the first visible item at the top and adding the heights of the consecutive items as far as the ClientHeight of the control. The top item is at TopIndex. If the height of the items are fixed you already know how many items are visible at most. If not you'll need to sum them up.

Sertac Akyuz
  • 54,131
  • 4
  • 102
  • 169
  • Here is the result: the ListBox DrawItem gets invoked for every item in the list even if only a small portion of one item is invalidated. Moreover Canvas.ClipRect is always the whole ListBox client area when checked within the DrawItem. That makes it impossible to find out if the drawing is caused by InvalidateRect or not. One good thing is that if I update a list item which not in the visible area then it does not invoke the DrawItem. – ssh Apr 06 '11 at 15:31
  • BTW, I am using Delphi 7. I did some more research trying to understand the underlying ListBox paint method. In "TCustomListBox.WMPaint", there is a comment saying "{ Listboxes don't allow paint "sub-classing" like the other windows controls so we have to do it ourselves. }". In that method before Dispatching DrawItemMsg, it does get the clipbox rectangle (GetClipBox(Message.DC, R)). But that TRect, R is not used anywhere! Am I missing something? Thanks! – ssh Apr 06 '11 at 15:54
  • @ssh - I've tested the invalidation before posting the answer, with my test 'OnDrawItem' fired only once and only for the fourth item. And I also tried to draw outside of the passed rectangle (offsetted the rectangle before drawing), nothing was drawn outside of the invalidated rectangle. I used a 'lbOwnerDrawFixed' listbox, but it shouldn't make a difference. After your comment I also had a look at the 'ClipRect', it is clipped properly. You're using the listbox's canvas, not the form's right? If you can, have a test with a new application. – Sertac Akyuz Apr 06 '11 at 16:04
  • @ssh - Tested with D2007 and D3, same result with both. – Sertac Akyuz Apr 06 '11 at 16:05
  • Thanks Sertac Akyuz! Yes, I am testing it with a simple application to figure out the nuances. Let me check again, I might have made a mistake. Can you check TCustomListBox.WMPaint code in StdCtrls.pas? The method Dispatches DrawItemMsg for every item in the list as in this code snippet=> [ Y := 0; while Y < H do begin {Dispatch message} Inc(Y, FitemHeight); end; ] – ssh Apr 06 '11 at 16:22
  • @ssh - I looked at it when you mentioned GetClipBox, it is indeed weird. But the code never passes there (at least not here), message.DC is always 0. OnDrawItem is always called from a WM_DRAWITEM. – Sertac Akyuz Apr 06 '11 at 16:48
  • @Sertac Akyuz. You are correct. I got it working such that it only calls DrawItem for the invalidated item. I am trying to figure out what went wrong before. Many Thanks! – ssh Apr 06 '11 at 16:49
  • @ssh - You're welcome!  BTW, regarding the 3rd item in the question, it didn't occur to me at that moment, but to determine if an item is visible or not, probably comparing its 'ItemRect' with the 'ClientRect' of the ListBox would be easier. – Sertac Akyuz Apr 06 '11 at 17:02
  • @Sertac Akyuz - I figured out why it did not work in my earlier trial. In order to reduce flicker, I had set the ListBox as DoubleBuffered. Looks like if you turn on DoubleBuffering for the ListBox it calls the DrawItem for each and every item in the list for every small change! (It is still intriguing why PaintListBox does not get invoked in TCustomListBox.WMPaint). I am fine with that for now :-) Thank you!! – ssh Apr 06 '11 at 17:18
  • @ssh - Oh!, it figures. I'll try to keep that in mind, thanks for the update..   Same here about 'PaintListBox' in TCustomListBox.WMPaint, it looks like someone tried to sabotage the performance of the list box but we just got lucky!.. – Sertac Akyuz Apr 06 '11 at 17:32