XE6 TListView-Spaltenbreiten werden Null, wenn Sie column.width lesen

Es ist ein Fehler inTListView.

Lesen einer SpalteWidth kann dazu führen, dass die Listenansicht versucht, die Spaltenbreite vom zugrunde liegenden Windows zu ermittelnLISTVIEW direkt steuern - bevor die Spalten des Win32-Steuerelements initialisiert wurden.

Da die Spalten nicht initialisiert wurden, ist die ListenansichtLVM_GETCOLUMNWIDTH Die Nachricht schlägt fehl und kehrt zurückNull. DasTListView Damit ist gemeint, dass die Breiteist Null und macht alle Spalten Null.

Dieser Fehler wurde irgendwann nach Delphi 5 eingeführt.

Schritte zum Reproduzieren

Fügen Sie einem Formular eine Listenansicht im Berichtsstil mit drei Spalten hinzu:

Hinzufügen einesOnResize Eventhandler zur Listenansicht:

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;

Starte es:

Der Fehler

DasTListColumn Code versucht, die Spaltenbreite aus dem Windows zu lesenlistview Klasse direkt, bevor die Spalten wurdenhinzugefügt zu den Windowslistview Steuerung

Formatieren des Codes zur besseren Lesbarkeit:

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; 

Das Problem tritt auf, während das Formular erstellt wirddfm Deserialisierung:

ComCtrls.TListColumn.GetWidth: TWidth;
TForm1.ListView1Resize(Sender: TObject);
Windows.CreateWindowEx(...)
Controls.TWinControl.CreateWindowHandle(const Params: TCreateParams);
Controls.TWinControl.CreateWnd;
ComCtrls.TCustomListView.CreateWnd;

Das Problem ist dasTCustomListView.CreatWnd Die Spalten werden irgendwann hinzugefügtnach der anruf anCreateWnd:

procedure TCustomListView.CreateWnd;
begin
   inherited CreateWnd; //triggers a call to OnResize, trying to read the column widths
   //...
   Columns.UpdateCols; //add the columns
   //...
end;

Der Code inTListColumn.GetWidth merkt nicht, dass die Spalten noch nicht initialisiert wurden.

Warum schlägt es in Delphi 5 nicht fehl?

Delphi 5 verwendet ähnlicheTCustomListView Konstruktion:

procedure TCustomListView.CreateWnd;
begin
   inherited CreateWnd; //triggers a call to OnResize
   //...
   Columns.UpdateCols;
   //...
end;

AußerDelphi 5 versucht nicht, sich selbst auszuloten und Dinge zu überdenken:

function TListColumn.GetWidth: TWidth;
begin
   if FWidth = 0 then
      FWidth := ListView_GetColumnWidth(TListColumns(Collection).Owner.Handle, Index);
   Result := FWidth;
end;

Wenn wir eine Breite haben, benutze sie.

Die Frage

Warum warTListColumn.GetWidth geändert? Welchen Fehler wollten sie lösen? Ich sehe, dass sie ihre Codeänderungen nicht kommentieren, so dass es unmöglich ist, aus der VCL-Quelle zu sagen, was die Begründung war.

Was noch wichtiger ist, wie behebe ich das? Ich kann den Code von nicht entfernenOnResize, aber ich kann einen erstellenTFixedListView Zollkontrolle; außer ich müsste alles von Grund auf neu schreiben, damit es a verwendetTFixedListViewColumn Klasse.

Das ist nicht gut.

Die wichtigste Frage: Wie behebt Embarcadero das Problem? Was ist das für ein Code?sollte in seinTListColumn.GetWidth den Fehler beheben?ComponentState ist leer. Es scheint, als müssten sie eine neue Variable einführen:

FAreColumnsInitialized: Boolean;

Oder sie könnten den Code so zurücksetzen, wie er war.

Worin würden Sie vorschlagen, dass sie den Code korrigieren?

Warum funktioniert es mit deaktivierten Themen?

Der Bug passiert nur mitVisuelle Stile aktiviert.

Windows hat eineWM_PARENTNOTIFY Nachricht, dass"benachrichtigt die Eltern über wichtige Ereignisse im Leben der Kontrolle". Im Falle derlistviewDies sendet das Handle desheader steuern, dass die Listenansicht intern verwendet. Delphi speichert dann diesen Headerhwnd:

procedure TCustomListView.WMParentNotify(var Message: TWMParentNotify);
begin
  with Message do
    if (Event = WM_CREATE) and (FHeaderHandle = 0) then
    begin
      FHeaderHandle := ChildWnd;

      //...
    end;
  inherited;
end;

Bei deaktivierten Designs sendet Windows das nichtWM_PARENTNOTIFY Nachricht bisspäter im Bauzyklus. Dies bedeutet, dass bei deaktivierten Themen dieTListColumn wird eines der Kriterien nicht erfüllen, das es ihm ermöglicht, mit der Listenansicht zu sprechen:

   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;

Aber wenn wir die neue Version von Windows verwendenlistview Kontrolle, Windows passiert das zu sendenWM_PARENTNOTIFY Nachricht früher in der Konstruktion:

   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;

Auch wenn das Header-Handle gültig ist, bedeutet dies nicht, dass die Spalten noch hinzugefügt wurden.

Die vorgeschlagene Lösung

Es scheint, als ob der VCL-Fix verwendet werden sollWM_PARENTNOTIY als die richtige Gelegenheit, die Spalten zur Listenansicht hinzuzufügen:

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

Mit Blick auf den Windows 2000-Quellcode weist ListView einige Kommentare auf, die erkennen, dass einige schlechte Apps vorhanden sind:

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

Antworten auf die Frage(0)

Ihre Antwort auf die Frage