TVirtualStringTree - 重置非可视节点和内存消耗
我有一个应用程序,可以从二进制日志文件加载记录并将其显示在虚拟 TListView 中。一个文件中可能有数百万条记录,并且显示可以由用户过滤,所以我不会一次性加载内存中的所有记录,并且ListView项索引与文件记录偏移量(例如,列表项 1 可能是文件记录 100)。我使用 ListView 的 OnDataHint 事件仅加载 ListView 实际感兴趣的项目的记录。当用户滚动时,OnDataHint 指定的范围会发生变化,允许我释放不在新范围内的记录,并分配新记录根据需要。
这工作正常,速度可以接受,并且内存占用非常低。
我目前正在评估 TVirtualStringTree 作为 TListView 的替代品,主要是因为我想添加扩展/折叠跨多行记录的功能(我可以通过动态递增/递减项目计数来使用 TListView 来捏造它,但这不是就像使用真正的树一样简单)。
在大多数情况下,我已经能够移植 TListView 逻辑并让一切按照我的需要工作。不过,我注意到 TVirtualStringTree 的虚拟范例有很大不同。它不具有与 TListView 相同的 OnDataHint 功能(我可以使用 OnScroll 事件来伪造它,这允许我的内存缓冲区逻辑继续工作),并且我可以使用 OnInitializeNode 事件将节点与分配的记录关联起来。
然而,一旦树节点被初始化,它就会在树的生命周期内保持初始化状态。这对我来说不好。当用户滚动并从内存中删除记录时,我需要重置这些非可视节点,而不将它们完全从树中删除,或丢失它们的展开/折叠状态。当用户将它们滚动回视图时,我可以重新分配记录并重新初始化节点。基本上,就虚拟化而言,我想让 TVirtualStringTree 尽可能像 TListView 那样工作。
我已经看到 TVirtualStringTree 有一个 ResetNode() 方法,但是每当我尝试使用它时都会遇到各种错误。一定是我用错了。我还想到只将每个节点内的数据指针存储到我的记录缓冲区,然后分配和释放内存,相应地更新这些指针。最终的效果也不是那么好。
更糟糕的是,我最大的测试日志文件中有大约 500 万条记录。如果我一次用那么多节点初始化 TVirtualStringTree(当日志显示未过滤时),树的节点的内部开销会占用高达 260MB 的内存(尚未分配任何记录)。而使用 TListView,加载相同的日志文件及其背后的所有内存逻辑,我只需使用几 MB 就可以了。
有什么想法吗?
I have an app that loads records from a binary log file and displays them in a virtual TListView. There are potentially millions of records in a file, and the display can be filtered by the user, so I do not load all of the records in memory at one time, and the ListView item indexes are not a 1-to-1 relation with the file record offsets (List item 1 may be file record 100, for instance). I use the ListView's OnDataHint event to load records for just the items the ListView is actually interested in. As the user scrolls around, the range specified by OnDataHint changes, allowing me to free records that are not in the new range, and allocate new records as needed.
This works fine, speed is tolerable, and the memory footprint is very low.
I am currently evaluating TVirtualStringTree as a replacement for the TListView, mainly because I want to add the ability to expand/collapse records that span multiple lines (I can fudge it with the TListView by incrementing/decrementing the item count dynamically, but this is not as straight forward as using a real tree).
For the most part, I have been able to port the TListView logic and have everything work as I need. I notice that TVirtualStringTree's virtual paradigm is vastly different, though. It does not have the same kind of OnDataHint functionality that TListView does (I can use the OnScroll event to fake it, which allows my memory buffer logic to continue working), and I can use the OnInitializeNode event to associate nodes with records that are allocated.
However, once a tree node is initialized, it sees that it remains initialized for the lifetime of the tree. That is not good for me. As the user scrolls around and I remove records from memory, I need to reset those non-visual nodes without removing them from the tree completely, or losing their expand/collapse states. When the user scrolls them back into view, I can re-allocate the records and re-initialize the nodes. Basically, I want to make TVirtualStringTree act as much like TListView as possible, as far as its virtualization is concerned.
I have seen that TVirtualStringTree has a ResetNode() method, but I encounter various errors whenever I try to use it. I must be using it wrong. I also thought of just storing a data pointer inside each node to my record buffers, and I allocate and free memory, update those pointers accordingly. The end effect does not work so well, either.
Worse, my largest test log file has ~5 million records in it. If I initialize the TVirtualStringTree with that many nodes at one time (when the log display is unfiltered), the tree's internal overhead for its nodes takes up a whopping 260MB of memory (without any records being allocated yet). Whereas with the TListView, loading the same log file and all the memory logic behind it, I can get away with using just a few MBs.
Any ideas?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
您可能不应该切换到 VST,除非您至少需要使用 VST 的一些标准列表框/列表视图所没有的优秀功能。但与扁平的项目列表相比,当然会有很大的内存开销。
我认为仅使用
TVirtualStringTree
能够展开和折叠跨多行的项目并没有真正的好处。你写但您可以轻松实现这一点,而无需更改项目计数。如果将列表框的
Style
设置为lbOwnerDrawVariable
并实现OnMeasureItem
事件,则可以根据需要调整高度以仅绘制第一个或所有线路。手动绘制扩展三角形或树视图的小加号应该很容易。 Windows API 函数DrawText()
或 <代码> DrawTextEx()
可用于测量和绘制(可选自动换行)文本。编辑:
抱歉,我完全忽略了您现在使用的是列表视图而不是列表框的事实。事实上,没有办法在列表视图中拥有不同高度的行,所以这是没有选择的。您仍然可以使用顶部带有标准标题控件的列表框,但这可能不支持您现在从列表视图功能中使用的所有内容,并且它本身可能比动态显示和隐藏列表视图行要完成更多甚至更多的工作。模拟折叠和扩展。
You probably shouldn't switch to VST unless you have a use for at least some of the nice features of VST that a standard listbox / listview don't have. But there is of course a large memory overhead compared to a flat list of items.
I don't see a real benefit in using
TVirtualStringTree
only to be able to expand and collapse items that span multiple lines. You writebut you can implement that easily without changing the item count. If you set the
Style
of the listbox tolbOwnerDrawVariable
and implement theOnMeasureItem
event you can adjust the height as required to draw either only the first or all lines. Drawing the expander triangle or the little plus symbol of a tree view manually should be easy. The Windows API functionsDrawText()
orDrawTextEx()
can be used both to measure and draw the (optionally word-wrapped) text.Edit:
Sorry, I completely missed the fact that you are using a listview right now, not a listbox. Indeed, there is no way to have rows with different heights in a listview, so that's no option. You could still use a listbox with a standard header control on top, but that may not support everything you are using now from listview functionality, and it may itself be as much or even more work to get right than dynamically showing and hiding listview rows to simulate collapsing and expanding.
如果我理解正确,
TVirtualStringTree
的内存要求应该是:nodecount * (SizeOf(TVirtualNode) + YourNodeDataSize + DWORD-align-padding)
为了最小化内存占用,您也许可以使用以下命令初始化节点仅指向内存映射文件的偏移量的指针。在这种情况下,重置已经初始化的节点似乎没有必要 - 内存占用应该是 nodecount * (44 + 4 + 0) - 对于 500 万条记录,大约 230 MB。
恕我直言,您无法对树做出任何更好的处理,但使用内存映射文件将允许您直接从文件读取数据,而无需分配更多内存并将数据复制到其中。
您还可以考虑使用树结构而不是平面视图来呈现数据。这样,您可以根据需要初始化父节点的子节点(当父节点展开时),并在父节点折叠时重置父节点(从而释放其所有子节点)。换句话说,同一层级的节点尽量不要过多。
If I understand it correctly, the memory requirement of
TVirtualStringTree
should be:nodecount * (SizeOf(TVirtualNode) + YourNodeDataSize + DWORD-align-padding)
To minimize the memory footprint, you could perhaps initialize the nodes with only pointers to offsets to a memory-mapped file. Resetting nodes which have already been initialized doesn't seem necessary in this case - the memory footprint should be nodecount * (44 + 4 + 0) - for 5 million records, about 230 MB.
IMHO you can't get any better with the tree but using a memory-mapped file would allow you to read the data directly from the file without allocating even more memory and copying the data to it.
You could also consider using a tree structure instead of a flat view to present the data. That way you could initialize child nodes of a parent node on demand (when the parent node is expanded) and resetting the parent node when it's collapsed (therefore freeing all its child nodes). In other words, try not to have too many nodes at the same level.
为了满足您的要求“展开/折叠跨多行的记录”,我只需使用绘图网格即可。要检查它,请将绘图网格拖到窗体上,然后插入以下 Delphi 6 代码。您可以折叠和展开 5,000,000 条多行记录(或任何您想要的数量),基本上没有任何开销。这是一种简单的技术,不需要太多代码,而且效果出奇的好。
To meet your requirement "to expand/collapse records that span multiple lines", I'd simply use a drawgrid. To check it out, drag a drawgrid onto a form, then plug in the following Delphi 6 code. You can collapse and expand 5,000,000 multiline records (or whatever quantity you want) with essentially no overhead. It's a simple technique, doesn't require much code, and works surprisingly well.
您不应该使用 ResetNode,因为此方法会调用 InvalidateNode 并再次初始化节点,从而导致与预期相反的效果。
我不知道是否可以诱导 VST 释放 NodeDataSize 中指定的内存大小,而无需实际删除节点。但为什么不将 NodeDataSize 设置为指针的大小(Delphi,VirtualStringTree - 类(对象)而不是记录)并自己管理数据?只是一个想法...
You shouldn't use ResetNode because this method invokes InvalidateNode and initializes node again, leading to opposite effect than expected.
I don't know if it's possible to induce VST to free memory size specified in NodeDataSize without actually removing node. But why not set NodeDataSize to size of Pointer ( Delphi, VirtualStringTree - classes (objects) instead of records ) and manage data yourself? Just an idea...
尝试“删除儿童”。该过程的注释如下:
从未使用过它,但正如我所读到的,您可以在 OnCollapsed 事件中使用它来释放分配给刚刚变得不可见的节点的内存。然后在 OnExpading 中重新生成这些节点,这样用户永远不知道节点已从内存中消失。
但我不能确定,我从来不需要这种行为。
Give "DeleteChildren" a try. Here's what this procedure's comment says:
Never used it, but as I read it, you can use that in the OnCollapsed event to free the memory allocated to nodes that just became invisible. And then re-generate those nodes in OnExpading so the user never knows the node went away from memory.
But I can't be sure, I never had a need for such behaviour.