在 Delphi 中有效地填充组合框

Efficiently populate combobox in Delphi

需要在 TComboBox 中添加很多项目(超过 10k)(我知道 TComboBox 不应该容纳很多项目,但我不能更改它)而不添加重复项。 所以我需要在添加之前搜索完整列表。我想避免 TComboBox.items.indexof 因为我需要二进制搜索但是二进制查找在 TStrings 中不可用。

所以我创建了一个临时的 Tstringlist,将 sorted 设置为 true 并使用 find。但是现在将临时 Tstringlist 分配回 TComboBox.Items

(myCB.Items.AddStrings(myList)) 

复制整个列表真的很慢。有没有办法移动列表而不是复制它?或者任何其他有效填充我的 TComboBox 的方法?

无法将列表 "move" 放入组合框中,因为组合框的存储属于内部 Windows 控件实现。它不知道有什么方法可以直接使用您的 Delphi TStringList 对象。它所提供的只是一个将一项添加到列表中的命令,TComboBox 然后使用该命令将字符串列表中的每一项逐一复制到系统控件中。避免将数千个项目复制到组合框中的唯一方法是完全避免此问题,例如使用不同类型的控件或减少需要添加的项目数。

列表视图有一个 "virtual" 模式,您只需告诉它 它应该有多少 个项目,然后它会在需要时回调到您的程序了解有关屏幕上可见内容的详细信息。不可见的项目在列表视图的实现中不占用任何 space,因此您可以避免复制。然而,system combo boxes don't have a "virtual" mode。您或许可以找到提供该功能的第三方控件。

减少您需要放入组合框中的项目数量是您的下一个最佳选择,但只有您和您的同事具备找出最佳方法所需的领域知识。

尽管将 10k 项保存在 TComboBox 中是疯狂的,但这里的有效策略是将缓存保存在单独的对象中。例如,声明:

    { use a TDictionary just for storing a hashmap }        
    FComboStringsDict : TDictionary<string, integer>;

其中

procedure TForm1.FormCreate(Sender: TObject);
var
  i : integer;
  spw : TStopwatch;
begin
  FComboStringsDict := TDictionary<string, integer>.Create;
  spw := TStopwatch.StartNew;
  { add 10k random items }
  for i := 0 to 10000 do begin
    AddComboStringIfNotDuplicate(IntToStr(Floor(20000*Random)));
  end;
  spw.Stop;
  ListBox1.Items.Add(IntToStr(spw.ElapsedMilliseconds));
end;

function TForm1.AddComboStringIfNotDuplicate(AEntry: string) : boolean;
begin
  result := false;
  if not FComboStringsDict.ContainsKey(AEntry) then begin
    FComboStringsDict.Add(AEntry, 0);
    ComboBox1.Items.Add(AEntry);
    result := true;
  end;
end;

通过这种方式最初添加 10k 项大约需要 0.5 秒。

{ test adding new items }
procedure TForm1.Button1Click(Sender: TObject);
var
  spw : TStopwatch;
begin
  spw := TStopwatch.StartNew;
  if not AddComboString(IntToStr(Floor(20000*Random))) then
    ListBox1.Items.Add('Did not add duplicate');
  spw.Stop;
  ListBox1.Items.Add(IntToStr(spw.ElapsedMilliseconds));
end;

但是添加每个后续项目非常快 <1ms。这是一个笨拙的实现,但您可以轻松地将此行为包装到自定义 class 中。这个想法是让你的数据模型尽可能地与可视化组件分开——在添加或删除项目时让它们保持同步,但在查找速度很快的字典上进行大量搜索。删除项目仍然依赖于 .IndexOf.

正如 Rudy Velthuis 在评论中提到的,假设您使用的是 VCLCB_INITSTORAGE 消息可能是一个选项:

SendMessage(myCB, CB_INITSTORAGE, myList.Count, 20 * myList.Count*sizeof(Integer));

其中 20 是您的平均字符串长度。

结果(在 i5-7200U 和 20K 项目上,随机长度在 1 到 50 个字符之间):

  • 没有 CB_INITSTORAGE:~ 265 毫秒
  • CB_INITSTORAGE:~215 毫秒

因此,虽然您可以通过预分配内存稍微加快速度,但更大的问题似乎是糟糕的用户体验。用户如何在包含如此多项目的组合框中找到正确的元素?