XE6 TListView column widths become zero if you read column.width

帅比萌擦擦* 提交于 2019-12-02 02:29:04

问题


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;
   }
   ...
}

来源:https://stackoverflow.com/questions/25449362/xe6-tlistview-column-widths-become-zero-if-you-read-column-width

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!