Is there a bug in the Delphi list view control when using custom drawing?
Asked Answered
R

2

14

QC#101189

I'm trying to custom draw a progress bar in a Delphi TListView as suggested by NGLN's answer to another SO question. This works fine, apart from the interaction with hot tracking when drawn using the new explorer theme introduced in Vista.

The hot tracking painting and the Delphi custom drawing events appear to interfere with each other. For example, the sort of output I am seeing looks like this:

enter image description here

The text in Column 1 should read Item 3 but is obliterated. It looks like a bug in the Delphi wrapper to the list view control, but it could equally be that I'm doing something wrong!

Although I've been developing this in XE2, the same behaviour occurs in 2010 and, presumably, XE.

Here's the code to reproduce this behaviour:

Pascal file

unit Unit1;

interface

uses
  Windows, Classes, Controls, Forms, CommCtrl, ComCtrls;

type
  TForm1 = class(TForm)
    ListView: TListView;
    procedure FormCreate(Sender: TObject);
    procedure ListViewCustomDrawSubItem(Sender: TCustomListView;
      Item: TListItem; SubItem: Integer; State: TCustomDrawState;
      var DefaultDraw: Boolean);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  ListView.RowSelect := True;
  ListView.Items.Add.Caption := 'Item 1';
  ListView.Items.Add.Caption := 'Item 2';
  ListView.Items.Add.Caption := 'Item 3';
end;

procedure TForm1.ListViewCustomDrawSubItem(Sender: TCustomListView;
  Item: TListItem; SubItem: Integer; State: TCustomDrawState;
  var DefaultDraw: Boolean);
var
  R: TRect;
begin
  DefaultDraw := False;
  ListView_GetSubItemRect(Sender.Handle, Item.Index, SubItem, LVIR_BOUNDS, @R);
  Sender.Canvas.MoveTo(R.Left, R.Top);
  Sender.Canvas.LineTo(R.Right-1, R.Bottom-1);
end;

end.

Form file

object Form1: TForm1
  Caption = 'Custom Draw List View Bug'
  ClientHeight = 290
  ClientWidth = 554
  OnCreate = FormCreate
  object ListView: TListView
    Align = alClient
    Columns = <
      item
        Caption = 'Column 1'
        Width = 250
      end
      item
        Caption = 'Column 2'
        Width = 250
      end>
    ViewStyle = vsReport
    OnCustomDrawSubItem = ListViewCustomDrawSubItem
  end
end
Railway answered 19/11, 2011 at 8:56 Comment(19)
I can only say: Use Virtual TreeView, not TListView. TListView is weird and slow and you have to fight Windows all the way.Compote
@Compote TListView isn't really slow. Not in virtual mode. And I prefer to have the native control to get the best look and feel. Not withstanding personal preferences, at the very least I want to hunt this down to QC it.Railway
Custom drawing doesn't go well with themed controls. I'd try SetWindowTheme(ListView.Handle, nil, nil) to see if this is the case.Flurried
@Sertac Yes, removing the 'explorer' window theme stops the hot tracking and fixes the issue. Of course, now the control looks revolting!!Railway
@David - LOL!, well sorry for not being able to help. I don't remember the details but I think I failed myself once trying to trace a similar problem.Flurried
@Sertac I had a go myself but rapidly realised that I know precisely nothing about how the common controls workRailway
@Roberts XP doesn't have the explorer themed list view. This is the themeing that is enabled by calling SetWindowTheme(ListView.Handle, 'explorer', nil) which was introduced in VistaRailway
The black background is often caused by an erase background not being followed by actual drawing. DoubleBuffered is usually in the picture in these cases as well. Sometimes you have to set it to true, sometime you need it at false to get it to draw correctly. The manner in which custom controls are drawn can be affected by the parent on which they sit. (Solved a black rectangle once by putting a coolbar between a toolbar and a tabsheet on a pagecontrol on a pagecontrol on a form. Warning: tracking it down involved putting instrumenting code in the vcl...)Ogawa
@David - Does putting SetBkMode(Sender.Canvas.Handle, TRANSPARENT); into TForm1.ListViewCustomDrawSubItem help?Flurried
@Sertac You truly are a genius! That does the trick. Once you submit it as an answer, I will upvote and accept.Railway
@Sertac Hmm, not so fast. In the real code in my app, when I tried this, the other columns in the list view (there are 3 in total) were drawn incorrectly. I had 2 text columns and a third column with a progress bar. The first text column was fine. The middle text column was drawn in bold aliased text.Railway
@David - If the middle column already had black background, it is possible that it already contained indistinguishable bold text. Can test by changing the 'Message Box' font color of the OS to aqua or something like it..Flurried
@Sertac The middle column is no different from the first column. Seems weird that it just afflicts on of the two text only columns.Railway
@David - I can't duplicate the bold text issue, my test code is exiting the handler if SubItem<>2 and text for first two columns look normal.. Anyway, the code shouldn't be needing such a workaround/hack in any case.Flurried
@Sertac let me try again. Should I only be calling SetBkMode if I'm going to custom draw? And should I restore the back mode?Railway
@David - Yes, only when custom drawing, and no restoring background mode (in fact SetBkMode may be the last line in the handler).Flurried
@Sertac Yes, that gets the job done. I'd be pleased if you wrote it up as a very brief answer so I can give you credit. By the way, what is SetBkMode all about. I am hopelessly ignorant of basic GDI.Railway
@David - It just tells to disregard the background color when drawing text f.i..Flurried
Bug still present in Delphi 10.1 Berlin. Was about to ask a question, then found this.Auricle
F
13

This is a workaround for the defective behavior rather than being an answer to the question if there's a bug in the VCL, and a few thoughts.

The workaround is to set the background mode of the device context assigned by the common control for item painting cyle to transparent after carrying out custom drawing:

procedure TForm1.ListViewCustomDrawSubItem(Sender: TCustomListView;
  Item: TListItem; SubItem: Integer; State: TCustomDrawState;
  var DefaultDraw: Boolean);
var
  R: TRect;
begin
  if not [CustomDrawing] then  // <- If we're not gonna do anything do not
    Exit;                      //    fiddle with the DC in any way

  DefaultDraw := False;
  ListView_GetSubItemRect(Sender.Handle, Item.Index, SubItem, LVIR_BOUNDS, @R);
  Sender.Canvas.MoveTo(R.Left, R.Top);
  Sender.Canvas.LineTo(R.Right-1, R.Bottom-1);

  SetBkMode(Sender.Canvas.Handle, TRANSPARENT); // <- will effect the next [sub]item
end; 



In an [sub]item paint cycle, the painting is always done in a top-down fashion, items having a lower index are sent NM_CUSTOMDRAW notification prior to ones with higher indexes. When the mouse is moved from one row to another, two rows need to be re-drawn - the one loosing the hot state, and the one gaining it. It would seem, when custom drawing is in-effect, drawing the row that's loosing the hot-state leaves the DC in an undesirable state. This is not a problem when moving the mouse upwards, because that item gets drawn last.

Custom drawing ListView and TreeView controls are different than custom drawing other controls and somewhat complicated (see: Custom Draw With List-View and Tree-View Controls). But you have full control over the entire process. The code in the NM_CUSTOMDRAW case of TCustomListView.CNNotify in 'comctrls.pas' of the VCL is equally complicated. But despite being provided a bunch of custom drawing handlers (half of them being advanced), you have no control over what the VCL does. For instance you can't return the CDRF_xxx you'd like or you can't set the clrTextBk you want. My biased opinion is that, there's a bug/design issue in the Delphi list view control, but I have nothing more concrete than an intuition as in finding a workaround.

Flurried answered 24/11, 2011 at 14:6 Comment(6)
@David - You're welcome! Despite my opinion, it might be a good idea to test the workaround when a new version of comctl32.dll arrives.Flurried
Work around still works to resolve this bug in Delphi 10.1 Berlin.Auricle
I'm using C++ and sadly this suggestion does nothing for me; in my case, I get a white fill-in when affected. Furthermore, if I prefix my DrawTextW() calls with SetBkMode(TRANSPARENT) and then resetting it to the previous value afterward, moving the mouse leads to the dreaded transparent overdraw, despite my handler ALSO filling the rect prior! If I use OPAQUE instead, the issue as shown in David's original question happens, with a black rectangle. I'm not sure what the listview is doing to be so weird, but it is doing something... Also unsure if I should ask as a separate question.Radiophone
And in re Marjan above, adding LVS_EX_DOUBLEBUFFER does not fix it.Radiophone
A few more things: I tried breaking on AlphaBlend() or GdiAlphaBlend() to see if the listview was calling that and it wasn't; I tried seeing what the ROP2 mode was but it isn't changed; I tried CDRF_NOERASE at the prepaint, item prepaint, and subitem prepaint stages and it also didn't have any effect (though I haven't tried any of the stages together); I tried ExtTextOutW() instead of DrawTextW() but that didn't change anything other than where the text was (re)drawn. This doesn't happen with a non-owner drawn ListView, so I don't get it. I'll try more things later tonight.Radiophone
Sorry for all the noise; it turns out this is likely my own fault. I wound up rewriting my drawing code to put it all in one place and format it more nicely, got rid of CDRF_NEWFONT (I am drawing the entire control myself anyway, unless should I return that regardless?), and am drawing only on CDDS_SUBITEM | CDDS_ITEMPREPAINT. I'm guessing my bug was something was being drawn on normal CDDS_ITEMPREPAINT, but I don't remember exactly what the issue is. Thanks anyway!Radiophone
R
0

I don't have a clue for the black rectangle at the text position, but the missing hot tracking is due to the DefaultDraw := False; in your code. OnCustomDrawSubItem is only called for subitem <> 0, so the first column is drawn as default while the second uses your code. Custom drawing of the first column can be made with OnCustomDrawItem.

Radiator answered 19/11, 2011 at 10:53 Comment(3)
I'm afraid I have misled you with a poorly thought out example. The FillRect in the first version of the Q was just something to make the black rectangle appear. I've modified the question and changed the code and screenshot to make it clear that it's the black rectangle that is the issue. And in fact, the value of DefaultDraw has no bearing on the black rectangle.Railway
I see. With your changed example the problem with the cutted hot tracking seems to be solved now. It was merely caused by drawing the rectangle than by the DefaultDraw setting. Side note: on my system the black text rectangle appears only when you move the mouse from a lower to a higher indexed item, but not when you move from a higher to lower one.Radiator
You just need to do some GDI out to get this to manifest. I picked FillRect but that clearly was a bad choice. And yes, I see the same as you with lower to higher index. Sorry for the confusion.Railway

© 2022 - 2024 — McMap. All rights reserved.