TListView:如果您添加列,VCL会丢失列的顺序

6
我试图在TListView中添加一列。为此,我将新列添加到末尾,并通过将其索引设置为指定值来移动它。这有效,直到添加另一列。
我的操作步骤如下:将列添加到最后位置(Columns.Add),同时在最后位置添加子项(Subitems.Add)。然后,通过将其索引设置为正确的位置来移动该列。只要添加的是一个新列,就能正常工作。但是当添加第二列时,子项会出现问题。第一列的新子项会被移动到最后位置,例如:
0        |  1          |  new A       |  new B      | 3
Caption  |  old sub 1  |  old sub 3   |  new Sub B  | new sub A

我很乐意帮忙翻译!
例如,是否有一种命令或消息可以发送给ListView以刷新或保存其列->子项映射?这样,在添加第一个新列及其子项后,我可以使用该命令处理第二个新列,就像处理第一个新列一样。
还是这只是TListViews的列->子项处理或TListColumns的一个错误?
以下是一个VCL表单应用程序的示例代码(将Form1.OnCreate事件分配):
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls;

type
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
  private
    listview: TListView;
    initButton: TButton;
    addColumn: TButton;
    editColumn: TEdit;
    subItemCount: Integer;
    procedure OnInitClick(Sender: TObject);
    procedure OnAddClick(Sender: TObject);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  listview := TListView.Create(self);
  with listview do
  begin
    Left := 8;
    Top := 8;
    Width := self.Width - 30;
    Height := self.Height - 100;
    Anchors := [akLeft, akTop, akRight, akBottom];
    TabOrder := 0;
    ViewStyle := vsReport;
    Parent := self;
  end;

initButton := TButton.Create(self);
with initButton do
  begin
    left := 8;
    top := listview.Top + listview.Height + 20;
    Width := 75;
    Height := 25;
    TabOrder := 1;
    Caption := 'init';
    OnClick := OnInitClick;
    Parent := self;
  end;

  editColumn := TEdit.Create(self);
  with editColumn do
  begin
    left := initButton.Left + initButton.Width + 30;
    top := listview.Top + listview.Height + 20;
    Width := 120;
    Height := 25;
    TabOrder := 2;
    Parent := self;
    Caption := '';
  end;

  addColumn := TButton.Create(self);
  with addColumn do
  begin
    left := editColumn.Left + editColumn.Width + 10;
    top := listview.Top + listview.Height + 20;
    Width := 75;
    Height := 25;
    TabOrder := 1;
    Enabled := true;
    Caption := 'add';
    OnClick := OnAddClick;
    Parent := self;
  end;

end;

procedure TForm1.OnInitClick(Sender: TObject);
var col: TListColumn;
i, j: integer;
item: TListItem;
begin
  listview.Items.Clear;
  listview.Columns.Clear;

  // add items
  for I := 0 to 2 do
  begin
    col := ListView.Columns.Add;
    col.Caption := 'column ' + IntToStr(i);
    col.Width := 80;
  end;

  // add columns
  for I := 0 to 3 do
  begin
    item := ListView.Items.Add;
    item.Caption := 'ItemCaption';

    // add subitems for each column
    for j := 0 to 1 do
    begin
      item.SubItems.Add('subitem ' + IntToStr(j+1));
    end;
  end;

  subItemCount := 5;
end;

procedure TForm1.OnAddClick(Sender: TObject);
var number: integer;
col: TListColumn;
i: Integer;
ascii: char;
begin
  listview.Columns.BeginUpdate;

  number := StrToInt(editColumn.Text);
  ascii :=  Chr(65 + number);

  // create the new column
  col := TListColumn(ListView.Columns.add());
  col.Width := 80;
  col.Caption := ascii;

  // add the new subitems
  for I := 0 to ListView.Items.Count-1 do
  begin
    ListView.Items[i].SubItems.Add('subitem ' + ascii);
  end;

  // move it to the designated position
  col.Index := number;

  listview.Columns.EndUpdate;

  Inc(subItemCount);
end;

end.

谢谢!


编辑:Sertac Akyuz提出的建议修复方案很好,但我不能使用它,因为更改Delphi源代码对我的项目来说不是解决方案。已报告错误。

编辑:删除了无意中包含在第一个帖子中的第二个问题,并开了一个新问题(请参见链接的问题和问题修订)。

更新报告的错误现已在Delphi XE2 更新4中修复。


我猜测某处缺少了刷新/更新。不过我不确定是什么。话虽如此,这似乎又是一个虚拟模式列表视图能够发挥作用的案例。 - David Heffernan
但是它们只适用于 .Net,不是吗?我在等价的 C#.Net 项目中遇到了同样的问题,也许可以在那里使用它。 - torno
不错,Windows的列表视图支持虚拟模式,并且Delphi将其封装得非常好。如果您想在运行时操作列,那绝对是最好的选择。这里其他人可能会指向虚拟树状视图,但我个人喜欢使用原生控件。 - David Heffernan
浏览了一下VCL代码,似乎在更新列索引的过程中,VCL的数据与Windows列表视图数据在子项方面出现了不同步的情况。尝试使用Begin/EndUpdate进行调试有一定效果,但遗憾的是目前还不是期望的结果。像@David建议的那样,将ListView设置为虚拟模式可能是最好的选择。这样,您的应用程序始终会被要求提供每个单元格需要显示的数据,而VCL或Windows中没有隐藏的“复制”操作。 - Marjan Venema
听起来你应该停止使用列表视图,改用真正的网格。 - Warren P
显示剩余2条评论
1个回答

7

在你排列好列之后,请调用UpdateItems方法。例如:

..
col.Index := number;
listview.UpdateItems(0, MAXINT);
..



更新:

在我的测试中,我仍然发现某些情况下需要以上调用。但真正的问题是,“Delphi列表视图控件存在一个 bug”。

使用简单项目复制该问题:

  • Place a TListView control on a VCL form, set its ViewStyle to 'vsReport' and set FullDrag to 'true'.
  • Put the below code to the OnCreate handler of the form:
    ListView1.Columns.Add.Caption := 'col 1';
    ListView1.Columns.Add.Caption := 'col 2';
    ListView1.Columns.Add.Caption := 'col 3';
    ListView1.AddItem('cell 1', nil);
    ListView1.Items[0].SubItems.Add('cell 2');
    ListView1.Items[0].SubItems.Add('cell 3');
    
  • Place a TButton on the form, and put the below code to its OnClick handler:
    ListView1.Columns.Add.Caption := 'col 4';
  • Run the project and drag the column header of 'col 3' to in-between 'col 1' and 'col 2'. The below picture is what you'll see at this moment (everything is fine):

    list view after column drag

  • Click the button to add a new column, now the list view becomes:

    list view after adding column

    Notice that 'cell 2' has reclaimed its original position.

Bug:

TListView (TListColumn)的列在其FOrderTag字段中保存其排序信息。每当您更改列的顺序(通过设置Index属性或拖动标题),此FOrderTag会相应地更新。

现在,当您向TListColumns集合添加列时,集合首先添加新的TListColumn,然后调用TListColumns中的UpdateCols方法。以下是D2007 VCL中TListColumnsUpdateCols方法的代码:

procedure TListColumns.UpdateCols;
var
  I: Integer;
  LVColumn: TLVColumn;
begin
  if not Owner.HandleAllocated then Exit;
  BeginUpdate;
  try
    for I := Count - 1 downto 0 do
      ListView_DeleteColumn(Owner.Handle, I);

    for I := 0 to Count - 1 do
    begin
      with LVColumn do
      begin
        mask := LVCF_FMT or LVCF_WIDTH;
        fmt := LVCFMT_LEFT;
        cx := Items[I].FWidth;
      end;
      ListView_InsertColumn(Owner.Handle, I, LVColumn);
      Items[I].FOrderTag := I;
    end;
    Owner.UpdateColumns;
  finally
    EndUpdate;
  end;
end;


上述代码从底层API列表视图控件中删除所有列,然后重新插入它们。请注意,代码将每个插入的列的FOrderTag赋值为索引计数器:

      Items[I].FOrderTag := I;

这是在创建时从左到右的列顺序。如果在调用此方法时,列的顺序与创建时不同,则会丢失该排序。由于项目不会相应地更改其位置,因此所有内容都会混乱。
修复方法: 以下对该方法的修改似乎在我的少量测试中有效,您需要进行更多测试(显然,此修复方法并不涵盖所有可能情况,请参见下面“torno的评论”以了解详情)。
procedure TListColumns.UpdateCols;
var
  I: Integer;
  LVColumn: TLVColumn;
  ColumnOrder: array of Integer;
begin
  if not Owner.HandleAllocated then Exit;
  BeginUpdate;
  try
    SetLength(ColumnOrder, Count);
    for I := Count - 1 downto 0 do begin
      ColumnOrder[I] := Items[I].FOrderTag;
      ListView_DeleteColumn(Owner.Handle, I);
    end;

    for I := 0 to Count - 1 do
    begin
      with LVColumn do
      begin
        mask := LVCF_FMT or LVCF_WIDTH;
        fmt := LVCFMT_LEFT;
        cx := Items[I].FWidth;
      end;
      ListView_InsertColumn(Owner.Handle, I, LVColumn);
    end;
    ListView_SetColumnOrderArray(Owner.Handle, Count, PInteger(ColumnOrder));

    Owner.UpdateColumns;
  finally
    EndUpdate;
  end;
end;

如果您没有使用包,可以将修改过的'comctrls.pas'放到项目文件夹中。否则,您可能需要进行运行时代码修补,或者提交错误报告并等待修复。

@torno - 不,我只测试了插入一个列的代码片段。应该仔细阅读问题... - Sertac Akyuz
谢谢你,Sertac!这是一个很好的观点。我会尝试验证你的修复,如果成功的话,我会报告这个错误。 - torno
插入甚至不调用UpdateCols方法...但现有子项的顺序是正确的(所以你修复的错误只出现在调用columns.add时)。在正确位置插入列后,添加新项目及其子项似乎存在问题 :/ - torno
这个屏幕截图或许能让你理解我的问题:http://imageshack.us/photo/my-images/46/newitem.png/。与子项索引1不同,新项目的子项具有子项索引2。 - torno
好的,你是对的。我的例子和子项文本选择得不好 :) 我总是为新列“添加”一个子项,所以它在最后。抱歉让你感到困惑。所以我可能不必为每个项目保存索引,而是为每个列保存。这可能是可行的。谢谢你指出这一点。我会尝试找到解决方案,如果成功,我会关闭这篇文章。再次感谢 :) - torno
显示剩余12条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接