JackTrapper / DelphiBugs

Bug tracker for Delphi
7 stars 2 forks source link

TListColumns internal FOrderTag gets out of sync when columns are deleted #15

Open JackTrapper opened 5 years ago

JackTrapper commented 5 years ago

Tested

The ListView internal class TListColumns maintains a mapping between the columns TCollection and the column index in the underlying windows LISTVIEW class.

Each LVCOLUMN is represented in Delphi as a TListColumn object, and the TListColumn maintains an FOrderTag, which indicates which "real" column this TListColumn maps to:

TListColumn = class(TCollectionItem)
private
   FOrderTag: Integer;
end;

This tag is used internally in methods get TListColumn.GetWidth:

function TListColumn.GetWidth: TWidth;
var
   lvColumn : TLVColumn;
begin
   lvColumn.mask := LVCF_WIDTH;
   if ListView_GetColumn(LOwner.Handle, FOrderTag, {var}lvColumn) then
      FWidth := lvColumn.cx;
end;

Deleting columns breaks the FOrderTag

If a column from the TListColumns collection is deleted, then the FOrderTag of all subsequent columns is not recalculated. Ideally TListColumns would override the Notify method, and if the Action is cnDeleting, it would fix all the order tags:

procedure TListColumns.Notify(Item: TCollectionItem; Action: TCollectionNotification);
begin
   case Action of
   cnDeleting: FixOrderTags(Item as TListColumn);
   end;

   inherited;
end;

procedure TListColumnsFixed.FixOrderTags(Item: TListColumn);
var
   nIndex : Integer;
   listColumn: TListColumn;
begin
   // The Windows ListView adjusts the LVColumn.iOrder when a column is deleted.
   // FOrderTag is not adjusted in the original TListColumns collection so it gets out of sync.
   // This class fixes that.

   for nIndex := 0 to Count - 1 do
   begin
      listColumn := Items[nIndex];
      // if FOrderTag > FOrderTag of the item being deleted then adjust it
      if ListColumn.FOrderTag > Item.FOrderTag then
         Dec(ListColumn.FOrderTag);
   end;
end;

The VCL source fix

That works great when you're Imprise, because then you can just fix the Vcl source. But we're not allowed to make any changes that affect the interface section of a unit. Because of that we instead create our own TListColumns descendant hidden in the implementation section:

type
  TListColumnsFixed = class(TListColumns)
  private
    procedure FixOrderTags(Item : TListColumn);
  protected
    procedure Notify(Item: TCollectionItem; Action: TCollectionNotification); override;
  end;

{ TListColumnsFixed }

procedure TListColumnsFixed.FixOrderTags(Item: TListColumn);
var
    nIndex : Integer;
    listColumn: TListColumn;
begin
    // The Windows ListView adjusts the LVColumn.iOrder when a column is deleted.
    // FOrderTag is not adjusted in the original TListColumns collection so it gets out of sync.
    // This class fixes that.

    for nIndex := 0 to Count - 1 do
    begin
        listColumn := Items[nIndex];
        // if FOrderTag > FOrderTag of the item being deleted then adjust it
        if ListColumn.FOrderTag > Item.FOrderTag then
            Dec(ListColumn.FOrderTag);
    end;
end;

procedure TListColumnsFixed.Notify(Item: TCollectionItem; Action: TCollectionNotification);
begin
    case Action of
      cnDeleting: FixOrderTags(Item as TListColumn);
   end;

  inherited;
end;

And now we just have the TListView use our correctly working TListColumns descendant:

constructor TCustomListView.Create(AOwner: TComponent);
begin
   inherited Create(AOwner);
   //...snip...
// FListColumns := TListColumns.Create(Self);
   FListColumns := TListColumnsFixed.Create(Self); // Create the version that fixes the FOrderTag issue.
   //...snip...
end;