【问题标题】:Virtual TListView Item->SubItems->Assign() during OnData triggers refresh and hence never ending updatesOnData 期间的 Virtual TListView Item->SubItems->Assign() 触发刷新,因此更新永无止境
【发布时间】:2025-12-31 11:50:07
【问题描述】:

使用 c++ Builder 2009 维护旧项目

TListView 实例 (ViewStyle = vsReport) 中,设置为运行虚拟 (OwnerData = true) 我想尝试尽可能提高速度。每一点都有帮助。在OnData 事件中,我注意到Item->SubItems->Capacity = 0 一开始,随着子项目的添加,它每增加4。我在文档中读到 Capacity 是只读的,但我想尽可能避免 TStrings' 内部重新分配。由于我还需要进行缓存,我想我会使用TStringList 作为已经增长到所需容量的缓存。我假设 TStrings Assign() 然后会立即分配一个足够大的数组来存储所需数量的字符串?

Item->SubItems->Assign(Cache.SubItems) ;

虽然这行得通,但我注意到这会触发 ListView 再次调用 OnData,然后......导致它永远不会停止。

这样做很容易再次修复:

for (int x = 0 ; x < Cache.SubItems->Count ; x++)
    Item->SubItems->Add(Cache.SubItems->Strings[x]) ;

当然,重点是能够从一开始就告诉SubItems 字符串的数量。

我意识到我可能遇到了旧的 VCL 问题?那早就解决了?还是我现在不明白这种行为有什么意义?

有没有办法“启用”Capacity 接受输入?以便为要添加的字符串分配足够的空间?

【问题讨论】:

  • 我现在无权访问任何 C++ Builder,但是否有 BeginUpdate();EndUpdate(); 可用于在更新时禁用触发事件?像obj-&gt;BeginUpdate(); ...; Item-&gt;SubItems-&gt;Assign(Cache.SubItems); ...; obj-&gt;EndUpdate(); 这样的东西。如果没有,也许将OnData 事件处理程序存储在一个变量中,并在Assign() 之前将其设置为NULL,然后在您更新后将其设置回来一切都可以工作。
  • 优秀的建议 Ted,但它没有帮助,也许我需要重写我的问题,因为在 Assign() 期间没有调用 OnDataOnData 像往常一样完成,为所有显示的对象调用它,但所有对象的序列随后再次开始,一次又一次。通常在没有活动时它会停止,现在它永远不会停止
  • 进行更多测试时,似乎当我设置Capacity 时,值会保持不变(当我再次读出它时)。文档说没有,但我没有想到 TListViewItem SubItems 实现是一个继承自 TStrings 的类。
  • 自从我在虚拟模式下使用TListView 已经有一段时间了,但我从来没有遇到过你所描述的问题。我有 CB2009 的 VCL 源代码,所以我明天会查看它并发布答案。但与此同时,CB2009 文档说TStrings::Capacity 是读/写的,也许您将它与只读的TStrings::Count 混淆了? TStrings::Assign() 在内部调用 (Begin|End)Update()。而 IIRC,TStrings::Assign() 只是运行一个 Add() 循环。
  • @Peter 我现在已经添加了答案。

标签: c++builder vcl


【解决方案1】:

只要 ListView 需要给定列表项的数据,例如(但不限于)绘图操作,就会触发 TListView::OnData 事件。

请注意,当OnData 事件被触发时,TListItem::SubItems 已经预先被Clear()'ed。 TStringList::Clear()Capacity 设置为 0,释放其当前的 string 数组。这就是为什么CountCapacity 在进入OnData 处理程序时始终为0。

SubItems 属性实现为TSubItems 对象,该对象派生自TStringListTStrings::Capacity 属性设置器在TStringList 中实现,并执行您期望预分配string 数组的操作。但这就是它所做的一切——为数组分配 VCL 内存,仅此而已。仍然存在在 Win32 API 层更新 ListView 子项本身的方面,并且必须单独完成,因为每个 string 都添加到 SubItems

当您的OnData 处理程序调用SubItems-&gt;Assign() 时,您正在调用TStrings::Assign()(因为TStringListTSubItems 不要覆盖它)。然而,TStrings::Assign()DOES NOT 预分配string 数组到源TStrings 对象的大小,正如人们所期望的那样(至少在CB2009 中不是,我不知道如果现代版本有或没有)。在内部,Assign() 仅调用Clear(),然后调用TStrings::AddStrings()TStringListTSubItems 均未覆盖以预分配数组)。 AddStrings() 只是在循环中调用 TStrings::AddObject()TStringListTSubItems 都会覆盖)。

所有这些清除和添加逻辑都包含在一对TStrings::(Begin|End)Update() 调用中。这一点很重要,因为TSubItems 会对更新计数器做出反应。当计数器降到 0 时,TSubItems 触发 TListView 进行一些内部更新,其中包括对自身调用 Invalidate(),这会触发整个重绘,从而为列表项触发一系列新的 OnData 事件需要重新绘制。

另一方面,当您在自己的手动循环中调用 SubItems-&gt;Add() 并省略 (Begin|End)Update() 调用时,您将跳过整个 ListView 的重绘。 TSubItems 覆盖 TStrings::Add/Object() 以(除其他外)仅更新它链接到的特定 ListView 项目。它不会重绘整个 ListView。

所以,如果你真的想的话,你应该可以在进入手动循环之前设置Capacity

Item->SubItems->Capacity = Cache.SubItems->Count;
for (int x = 0; x < Cache.SubItems->Count; ++x)
    Item->SubItems->Add(Cache.SubItems->Strings[x]);

在这种情况下,您可以使用AddStrings() 而不是手动循环:

Item->SubItems->Capacity = Cache.SubItems->Count;
Item->SubItems->AddStrings(Cache.SubItems);

【讨论】:

  • AHA ..“TSubItems 对更新计数器做出反应。当计数器降至 0 时,TSubItems 触发 TListView 进行一些内部更新,其中包括对其自身调用 Invalidate()”解释了这一切,谢谢