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