Delphi向ComboBox添加项目的速度

8

我有一个相当复杂和庞大的应用程序,处理大量数据。有没有一种快速的方法将项目添加到ComboBox中,不需要很长时间?在我的P3 3.2ghz上,以下代码段需要将近1秒钟的时间才能添加大约32,000个项目。(MasterCIList是一个StringList,其中包含通常为20-30字节的字符串)。

with LookupComboBox do
 begin
  Items.BeginUpdate;
  Items.Clear;
  for i := 0 to MasterCIList.Count - 1 do
   Items.Add(MasterCIList[i]);
  Items.EndUpdate;
 end;

深入研究VCL,看起来在TComboBoxStrings.Add中,有一个调用

Result := SendMessage(ComboBox.Handle, CB_ADDSTRING, 0, Longint(PChar(S)));

我猜想这个过程可能会占用很多时间(好吧,我知道确实是这样的)。有没有其他更快的方法来填充Items?是否有高速组合框可用?我有TMS组件,但它们似乎是TComboBox的扩展。
例如,我有PlusMemo,它似乎是TMemo的完全重写。我可以轻松地在一秒钟内向PlusMemo添加一百万行。对于TMemo,我不认为可以做到这一点。
非常感谢您的时间!

4
不是一个答案,但你为什么想要在组合框中放置32,000个项目?这是一种非常糟糕的存储如此多数据的方式。 - Ed S.
你怎么指望用户在这么多项目中进行排序以做出决策呢?自动完成功能可以帮助一些,但即使有大约270个项目(例如每个国家一个),他的问题仍然是有效的。将大约270个项目添加到TComboBox需要80毫秒,这简直荒谬。所以,大家都可以回答被提出的问题。这是一个有效的问题,适用于比这个具体场景更广泛的受众。没有理由TComboBox不能在小于10毫秒内添加大约100,000个项目。不要担心用户体验 - 这不是这个问题的一部分。回答这个问题就好。 - Ian Boyd
相关问题 - 有相同问题的人来自C#内部。 - Ian Boyd
10个回答

16

很抱歉如果我有些烦人,但我认为拥有32,000个选项的TComboBox甚至在可用性方面都不太好 -- 我会说它慢的原因是:它从来没有被设计成这样 :)

是否有可能过滤数据,只加载子集?更具体地说,在我正在处理的一个特定数据库应用程序中,用户可以搜索一个人。我们让用户输入至少3或4个字符的姓名,然后才开始在列表框中返回结果。这大大提高了搜索表单的可用性,也大大加快了整个过程。

你能否采用类似的方法呢?

或者,完全不同的做法是,也许你可以查看VirtualTreeView组件 --- 直接使用或作为灵感来源。


这是一个好的提示。也许可以在OnDropDown处理程序中添加字符串,过滤以与已输入的字符开头相匹配。这样用户就可以对延迟产生影响。无论如何,即使立即填充,包含32000个项目的查找列表也是无法使用的。 - mghie

3

我同意在下拉框中有32K个选项是荒谬的... 话虽如此,您可以尝试先将这些选项添加到TStringList中,然后使用Begin/EndUpdate和AddStrings的组合:

SL := TStringList.Create;
try
  // Add your items to the stringlist
  ComboBox.Items.BeginUpdate;
  try
    ComboBox.Items.AddStrings(YourStringList);
  finally
    ComboBox.Items.EndUpdate;
  end;
finally
  SL.Free;
end;

代码可以编译,但我没有进一步测试它;我从未觉得需要向组合框或列表框中添加超过几十个项目。如果需要更多的项目,我会在填充列表之前找到一种筛选方式,以便有更少的项目。
只是出于好奇,您希望用户如何浏览那么多项目并做出决定?

"你如何期望用户在那么多项目中进行筛选以做出决定?自动补全。这个问题在有200个项目时同样适用。" - Ian Boyd
调用AddStrings内部已经为您调用了BeginUpdate / EndUpdate。在TComboBox.Items上调用BeginUpdate会在控件上设置WM_SETREDRAW。问题在于AddStrings仍然在循环中调用AddObject,每次循环都会发送CB_SETITEMDATA消息。一个普通的TStringList填充需要0.2毫秒。而“组合框 TStrings”则慢约280倍。 - Ian Boyd

2
var
  Buffer: TStringList;
begin
  Buffer := TStringList.Create;

  try
    // --> Add items to Buffer here <--

    ComboBox.Items := Buffer;
  finally
    FreeAndNil(Buffer);
  end;
end;

这是我们发现的最快的更新可视化控件的方法。
VCL在内部执行BeginUpdate、Clear和EndUpdate操作。
如果您不相信我,请进行分析。

1

使用BackgroundWorker添加MasterCIList项目。完成添加项目后,仅使用AddStrings。

procedure TForm2.BackgroundWorker1Work(Worker: TBackgroundWorker);
var
  I: Integer;
begin
  MasterCIList.BeginUpdate;
  try
    MasterCIList.Capacity := 32 * 1024; // if derminate  count of items
    for I := 1 to 32 * 1024 do
    begin
      MasterCIList.Add(IntToStr(I));
      { if need progess }
      if I mod 300 = 0 then
        Worker.ReportProgress((I * 100) div MasterCIList.Capacity);
      { if need cancelable }
      if (I mod 100 = 0) and Worker.CancellationPending then
        Break;

    end;

  finally
    MasterCIList.EndUpdate;
  end;

end;

procedure TForm2.BackgroundWorker1WorkComplete(Worker: TBackgroundWorker;
  Cancelled: Boolean);
begin

  LookupComboBox.Items.AddStrings(MasterCIList );


// AddStrings use beginupdate..endupdate in itself

 end;

1

或许 cmb.Items.Assign(myStringList) 可以帮助解决问题。

这里有个疯狂的想法:我没试过,但你可以看一下是否有虚拟加载combobox的方法,设置项目数量,然后进行自定义绘制。请原谅我的疯狂想法,但我听说某种方式是可用的。 无关紧要: 在Palm OS中就是这样做的... 加载combobox的更快方式是不加载... ;-)

不是答案,但为什么你需要在组合框中放32,000个项目?那是一种非常糟糕的存储大量数据的方式。

我同意; 这是一个不好的做法 ...


1

又是我。我正在添加32,000个项目,因为我需要这么多。这是我的应用程序中许多控件之一,其中有很多项目。我需要有那么多项目。查找东西时它的工作非常好。实际上完美无缺。我只是试图优化一些东西。用户可以很容易地找到他们需要的东西,因为它们按照某种逻辑顺序排列。

到目前为止,我所看到的关于Assign和AddStrings的所有内容最终都会在SendMessage调用中以Add的形式出现。所以我会继续寻找。

感谢您的反馈。


1
首先,像这样发布一个“答案”是错误的。请编辑您的原始帖子。其次,我已经编程20多年了,Win应用程序15+年,做过一些非常复杂的应用程序,甚至从未发现需要在组合框中使用1K个项目。但祝你好运(和你的应用程序用户)。 - Ken White
同意Ken的观点。明确一点 - 如果你的回答真的是对你问题的答案,那么发表回答是可以的。如果你只是有评论,那么请编辑你的问题。 - Argalatyr

1

0

我以不同的方式实现了这个功能。首先,我移除了组合框控件,取而代之的是文本框控件,并将其分配给自定义源的自动完成,其中自定义源字符串集合有32k项。我从控件验证的新查询中获取所选值。

因此,它可以替换组合框功能。对于大约32k项的情况,人们不会滚动,但他们会继续输入按键,并被我们的自定义自动完成源捕获。


0
也许你可以在后端使用数据库引擎并使用数据感知组件。这样事情会更快速和可用。如果您尝试描述您想要实现的内容,我们可以为您提供进一步的帮助。无论如何,您的用户界面设计有点奇怪。也许Embarcadero论坛可以更好地帮助您。

0
没有人有解决速度问题的解决方案。但是让我们来看一下各种可以稍微加快速度的答案(尽管远远不足以认为问题已经解决)。
我们的测试将添加250个项目。
测试1 - 添加到原始TStrings - 每秒120,000个项目
首先,我们将向一个TStringList添加250个项目。在没有任何其他开销或成本的情况下,我们将看到它如何发生。
var
   i: Integer;
   sl: TStrings;
begin
   sl := TStringList.Create;
   sl.BeginUpdate;
   for i := 1 to 250 do
      sl.Items.Add('Test');
   sl.EndUpdate;
end;

这在我的3.5 GHz i7-2700上以0.0081毫秒的速度添加了250个项目(每秒120,000个项目)。

这是我们能合理期望的最快速度。

测试2 - 向TComboBox.Items TStrings添加项目 - 每秒8个项目(比原来慢99.993%)

现在,我们的测试函数将实际向TComboBox.Items中添加内容。

var
   i: Integer;
begin
   for i := 1 to 250 do
      AComboBox.Items.Add('Test');
end;

这将在124.9毫秒内添加250个项目(每秒8.00个项目)。

测试3 - 关闭重绘 - 每秒24个项目(慢99.98%)

接下来,我们通过调用WM_SETREDRAW来关闭控件的绘制。

var
   i: Integer;
begin
   AComboBox.Perform(WM_SETREDRAW, 0, 0); // turn OFF redraw
   for i := 1 to 250 do
      AComboBox.Items.Add('Test');
   AComboBox.Perform(WM_SETREDRAW, 1, 0); // turn ON redraw
end;

这将在41.3397毫秒内添加250个项目(每秒24.2个项目)。

测试4 - Items.BeginUpdate - 每秒24个项目(比原速度慢99.98%)

您应该注意到,TCustomComboBoxStrings类(这是TComboBox内部使用的TStrings派生类)在您调用BeginUpdate / EndUpdate时已经为您调用了WM_SETREDRAW

procedure TCustomComboBoxStrings.SetUpdateState(Updating: Boolean);
begin
  SendMessage(ComboBox.Handle, WM_SETREDRAW, Ord(not Updating), 0);
  if not Updating then ComboBox.Refresh;
end;

这意味着你可以省去对 WM_SETREDRAW 的调用,直接使用经典的 Delphi 模式。
var
   i: Integer;
begin
   AComboBox.Items.BeginUpdate; // turn OFF redraw
   for i := 1 to 250 do
      AComboBox.Items.Add('Test');
   AComboBox.Items.EndUpdate; // turn ON redraw
end;

这样添加250个项目需要42.1127毫秒(每秒23.74个项目)。调用TStrings.BeginUpdate包装器的额外开销可以忽略不计。

测试5 - CB_INITSTORAGE - 每秒24个项目(慢99.98%)

然后我们来到神秘的CB_INITSTORAGE方法。意图是告诉ComboBox你将要添加多少个项目,以及预计将占用多少内存(以字节为单位)。这使得ComboBox能够预先分配内存。

var
   i: Integer;
begin
   AComboBox.Perform(CB_INITSTORAGE, 250, 250*6*2); // 12 bytes for "Test\r\n"
   AComboBox.Items.BeginUpdate; // turn OFF redraw
   for i := 1 to 250 do
      AComboBox.Items.Add('Test');
   AComboBox.Items.EndUpdate; // turn ON redraw
end;

这意味着在41.8486毫秒内添加了250个项目(每秒23.9个项目)。

这意味着它没有任何改进。事实上,我无法让CB_INITSTORAGE在任何地方提供任何改进。也许在2004年的Windows XP中,COMBOBOX控件受益于预分配内存。但现在看来,该控件已经在内部解决了重复递增内存分配的问题,并且现在以块的形式分配内存,因此CB_INITSTORAGE的用处已不复存在。

总结

测试 添加250个项目所需时间(毫秒) 速度(项目/秒)
添加到ComboBox 124.9 毫秒 8
设置重绘 41.3397 毫秒 24.2
开始更新 42.1127 毫秒 23.8
初始化存储 41.8486 毫秒 23.9
原始TStringList 0.0081 毫秒 120000

enter image description here

使用上述描述的方法,您可以提升性能:
- 在之前:比应有速度慢99.9933% - 在之后:比应有速度慢99.9809%
这将带来0.01%的显著加速!
额外话题:
有些人可能会建议调用`.AddStrings`(或根据您的情况调用`.AddObjects`):
begin
   AComboBox.Items.AddStrings(SourceList);
end;

他们没有意识到的是,AddStrings与上述的Case 4是完全相同的:

procedure TStrings.AddStrings(Strings: TStrings);
var
  I: Integer;
begin
  BeginUpdate;
  try
    for I := 0 to Strings.Count - 1 do
      AddObject(Strings[I], Strings.Objects[I]);
  finally
    EndUpdate;
  end;
end;

所以我们不再谈论AddStrings(或AddObjects),因为它们与我们的讨论无关。

额外阅读


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