There is a bug in TListView
.
Reading a column's Width
can cause the listview to try to get the column width from the underlying Windows LISTVIEW
control directly - before the Win32 control's columns have been initialized.
Because the columns have not been initialized, the listview's LVM_GETCOLUMNWIDTH
message fails, returning zero. The TListView
takes this to mean that the width is zero, and makes all columns zero.
This bug was introduced sometime after Delphi 5.
Steps to reproduce
Add a report style listview with three columns to a form:
Add an OnResize
event handler to the listview:
procedure TForm1.ListView1Resize(Sender: TObject);
begin
{
Any column you attempt to read the width of
will **cause** the width to become zero
}
ListView1.Columns[0].Width;
// ListView1.Columns[1].Width;
ListView1.Columns[2].Width;
end;
Run it:
The Bug
The TListColumn
code tries to read the column width out of the Windows listview
class directly, before the columns have even been added to the Windows listview
control
Formatting their code for readability:
function TListColumn.GetWidth: TWidth;
var
IsStreaming: Boolean;
LOwner: TCustomListView;
begin
LOwner := TListColumns(Collection).Owner;
IsStreaming := [csReading, csWriting, csLoading] * LOwner.ComponentState <> [];
if (
(FWidth = 0)
and (LOwner.HandleAllocated or not IsStreaming)
)
or
(
(not AutoSize)
and LOwner.HandleAllocated
and (LOwner.ViewStyle = vsReport)
and (FWidth <> LVSCW_AUTOSIZE)
and (LOwner.ValidHeaderHandle)
) then
begin
FWidth := ListView_GetColumnWidth(LOwner.Handle, FOrderTag);
end;
Result := FWidth;
end;
The problem happens while the form is being constructed during dfm
deserialization:
ComCtrls.TListColumn.GetWidth: TWidth;
TForm1.ListView1Resize(Sender: TObject);
Windows.CreateWindowEx(...)
Controls.TWinControl.CreateWindowHandle(const Params: TCreateParams);
Controls.TWinControl.CreateWnd;
ComCtrls.TCustomListView.CreateWnd;
The issue is that TCustomListView.CreatWnd
the columns are added sometime after the call to CreateWnd
:
procedure TCustomListView.CreateWnd;
begin
inherited CreateWnd; //triggers a call to OnResize, trying to read the column widths
//...
Columns.UpdateCols; //add the columns
//...
end;
The code in TListColumn.GetWidth
doesn't realize that the columns have not yet been initialized.
Why doesn't it fail in Delphi 5?
Delphi 5 uses similar TCustomListView
construction:
procedure TCustomListView.CreateWnd;
begin
inherited CreateWnd; //triggers a call to OnResize
//...
Columns.UpdateCols;
//...
end;
Except Delphi 5 doesn't try to psyche itself out, and overthink things:
function TListColumn.GetWidth: TWidth;
begin
if FWidth = 0 then
FWidth := ListView_GetColumnWidth(TListColumns(Collection).Owner.Handle, Index);
Result := FWidth;
end;
If we have a width, use it.
The question
Why was TListColumn.GetWidth
changed? What bug were they trying to solve? I see they don't comment their code changes, so it's impossible to tell from the VCL source what the rationale was.
More importantly, how do i fix it? I cannot remove the code from OnResize
, but i can create a TFixedListView
custom control; except i would have to re-write everything from scratch so that it uses a TFixedListViewColumn
class.
That's no good.
The most important question: How does Embarcadero fix it? What is code that should be in TListColumn.GetWidth
to fix the bug? ComponentState
is empty. It seems like they will have to introduce a new variable of:
FAreColumnsInitialized: Boolean;
Or they could put the code back to how it was.
What would you suggest they fix the code to?
Why does it work with themes disabled?
The bug only happens with Visual Styles enabled.
Windows has a WM_PARENTNOTIFY
message that "notifies the parent of important events in the life of the control". In the case of the listview
, this sends the handle of the header
control that the listview uses internally. Delphi then saves this header hwnd
:
procedure TCustomListView.WMParentNotify(var Message: TWMParentNotify);
begin
with Message do
if (Event = WM_CREATE) and (FHeaderHandle = 0) then
begin
FHeaderHandle := ChildWnd;
//...
end;
inherited;
end;
With themes disabled, Windows does not send the WM_PARENTNOTIFY
message until later in the construction cycle. This means that with themes disabled the TListColumn
will fail to meet one of the criteria that allows it to talk to the listview:
if (
(FWidth = 0)
and (LOwner.HandleAllocated or not IsStreaming)
)
or
(
(not AutoSize)
and LOwner.HandleAllocated
and (LOwner.ViewStyle = vsReport)
and (FWidth <> LVSCW_AUTOSIZE)
and (LOwner.ValidHeaderHandle) //<--- invalid
) then
begin
FWidth := ListView_GetColumnWidth(LOwner.Handle, FOrderTag);
end;
But when we use the new version of the Windows listview
control, Windows happens to send the WM_PARENTNOTIFY
message sooner in the construction:
if (
(FWidth = 0)
and (LOwner.HandleAllocated or not IsStreaming)
)
or
(
(not AutoSize)
and LOwner.HandleAllocated
and (LOwner.ViewStyle = vsReport)
and (FWidth <> LVSCW_AUTOSIZE)
and (LOwner.ValidHeaderHandle) //<--- Valid!
) then
begin
FWidth := ListView_GetColumnWidth(LOwner.Handle, FOrderTag);
end;
Even through the header handle is valid, it does not mean that the columns have been added yet.
The Proposed Fix
It seems like the VCL fix is to use WM_PARENTNOTIY
as the correct opportunity to add the columns to the listview:
procedure TCustomListView.WMParentNotify(var Message: TWMParentNotify);
begin
with Message do
if (Event = WM_CREATE) and (FHeaderHandle = 0) then
begin
FHeaderHandle := ChildWnd;
UpdateCols; //20140822 Ian Boyd Fixed QC123456 where the columns aren't usable in time
//...
end;
inherited;
end;
Bonus Chatter
Looking at the Windows 2000 source code, the ListView has some comments that recognize some poor apps exist out there:
lvrept.c
BOOL_PTR NEAR ListView_CreateHeader(LV* plv)
{
...
plv->hwndHdr = CreateWindowEx(0L, c_szHeaderClass, // WC_HEADER,
NULL, dwStyle, 0, 0, 0, 0, plv->ci.hwnd, (HMENU)LVID_HEADER, GetWindowInstance(plv->ci.hwnd), NULL);
if (plv->hwndHdr)
{
NMLVHEADERCREATED nmhc;
nmhc.hwndHdr = plv->hwndHdr;
// some apps blow up if a notify is sent before the control is fully created.
CCSendNotify(&plv->ci, LVN_HEADERCREATED, &nmhc.hdr);
plv->hwndHdr = nmhc.hwndHdr;
}
...
}
GetWidth()
related bug reports: #22672, #26194, #30134 – Barite