我有一个应用程序,它从二进制日志文件中加载记录并将其显示在虚拟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即可。
有什么想法吗?
答案 0 :(得分:1)
如果我理解正确,TVirtualStringTree
的内存要求应为:
nodecount *(SizeOf(TVirtualNode)+ YourNodeDataSize + DWORD-align-padding)
为了最小化内存占用,您可能只使用指向内存映射文件的偏移量的指针来初始化节点。在这种情况下,重置已经初始化的节点似乎没有必要 - 内存占用量应该是nodecount *(44 + 4 + 0) - 对于500万条记录,大约230 MB。
恕我直言,你无法用树获得更好的效果,但使用内存映射文件可以直接从文件中读取数据,而无需分配更多内存并将数据复制到文件中。
您还可以考虑使用树结构而不是平面视图来显示数据。这样,您可以根据需要初始化父节点的子节点(扩展父节点时),并在父节点折叠时重置父节点(从而释放其所有子节点)。换句话说,尽量不要在同一级别拥有太多节点。
答案 1 :(得分:1)
除非您至少使用标准列表框/列表视图所没有的VST的一些优秀功能,否则您可能不应该切换到VST。但是,与平面的项目列表相比,当然会有大量的内存开销。
我认为使用TVirtualStringTree
只能扩展和折叠跨越多行的项目并不会带来真正的好处。你写了
主要是因为我想添加扩展/折叠跨越多行的记录的能力(我可以通过动态递增/递减项目计数来使用TListView来捏造它,但这并不像使用真实树那样直接)
但您可以轻松实现,而无需更改项目数。如果您将列表框的Style
设置为lbOwnerDrawVariable
并实施OnMeasureItem
事件,则可以根据需要调整高度,以仅绘制第一行或所有行。手动绘制扩展器三角形或树形视图的小加号应该很容易。 Windows API函数DrawText()
或DrawTextEx()
可用于测量和绘制(可选的自动换行)文本。
修改强>
抱歉,我完全错过了您正在使用listview的事实,而不是列表框。实际上,在列表视图中没有办法让行具有不同的高度,所以这是没有选择的。您仍然可以在顶部使用带有标准标题控件的列表框,但这可能不支持您现在使用的列表视图功能中的所有内容,并且它本身可能比动态显示和隐藏列表视图行更好或更多工作模拟崩溃和扩展。
答案 2 :(得分:1)
为满足您的要求“展开/折叠跨越多行的记录”,我只需使用绘图网格。要检查它,将drawgrid拖到窗体上,然后插入以下Delphi 6代码。您可以折叠和展开5,000,000多行记录(或任何您想要的数量),基本上没有开销。这是一种简单的技术,不需要太多代码,而且效果非常好。
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, Grids, StdCtrls;
type
TForm1 = class(TForm)
DrawGrid1: TDrawGrid;
procedure DrawGrid1DrawCell(Sender: TObject; ACol, ARow: Integer; Rect: TRect; State: TGridDrawState);
procedure DrawGrid1SelectCell(Sender: TObject; ACol, ARow: Integer; var CanSelect: Boolean);
procedure DrawGrid1TopLeftChanged(Sender: TObject);
procedure DrawGrid1DblClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
procedure AdjustGrid;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
// Display a large number of multi-line records that can be expanded or collapsed, using minimal overhead.
// LinesInThisRecord() and RecordContents() are faked; change them to return actual data.
const TOTALRECORDS = 5000000; // arbitrary; a production implementation would probably determine this at run time
// keep track of whether each record is expanded or collapsed
var isExpanded: packed array[1..TOTALRECORDS] of boolean; // initially all FALSE
function LinesInThisRecord(const RecNum: integer): integer;
begin // how many lines (rows) does the record need to display when expanded?
result := (RecNum mod 10) + 1; // make something up, so we don't have to use real data just for this demo
end;
function LinesDisplayedForRecord(const RecNum: integer): integer;
begin // how many lines (rows) of info are we currently displaying for the given record?
if isExpanded[RecNum] then result := LinesInThisRecord(RecNum) // all lines show when expanded
else result := 1; // show only 1 row when collapsed
end;
procedure GridRowToRecordAndLine(const RowNum: integer; var RecNum, LineNum: integer);
var LinesAbove: integer;
begin // for a given row number in the drawgrid, return the record and line numbers that appear in that row
RecNum := Form1.DrawGrid1.TopRow; // for simplicity, TopRow always displays the record with that same number
if RecNum > TOTALRECORDS then RecNum := 0; // avoid overflow
LinesAbove := 0;
while (RecNum > 0) and ((LinesDisplayedForRecord(RecNum) + LinesAbove) < (RowNum - Form1.DrawGrid1.TopRow + 1)) do
begin // accumulate the tally of lines in expanded or collapsed records until we reach the row of interest
inc(LinesAbove, LinesDisplayedForRecord(RecNum));
inc(RecNum); if RecNum > TOTALRECORDS then RecNum := 0; // avoid overflow
end;
LineNum := RowNum - Form1.DrawGrid1.TopRow + 1 - LinesAbove;
end;
function RecordContents(const RowNum: integer): string;
var RecNum, LineNum: integer;
begin // display the data that goes in the grid row. for now, fake it
GridRowToRecordAndLine(RowNum, RecNum, LineNum); // convert row number to record and line numbers
if RecNum = 0 then result := '' // out of range
else
begin
result := 'Record ' + IntToStr(RecNum);
if isExpanded[RecNum] then // show line counts too
result := result + ' line ' + IntToStr(LineNum) + ' of ' + IntToStr(LinesInThisRecord(RecNum));
end;
end;
procedure TForm1.AdjustGrid;
begin // don't allow scrolling past last record
if DrawGrid1.TopRow > TOTALRECORDS then DrawGrid1.TopRow := TOTALRECORDS;
if RecordContents(DrawGrid1.Selection.Top) = '' then // move selection back on to a valid cell
DrawGrid1.Selection := TGridRect(Rect(0, TOTALRECORDS, 0, TOTALRECORDS));
DrawGrid1.Refresh;
end;
procedure TForm1.DrawGrid1DrawCell(Sender: TObject; ACol, ARow: Integer; Rect: TRect; State: TGridDrawState);
var s: string;
begin // time to draw one of the grid cells
if ARow = 0 then s := 'Data' // we're in the top row, get the heading for the column
else s := RecordContents(ARow); // painting a record, get the data for this cell from the appropriate record
// draw the data in the cell
ExtTextOut(DrawGrid1.Canvas.Handle, Rect.Left, Rect.Top, ETO_CLIPPED or ETO_OPAQUE, @Rect, pchar(s), length(s), nil);
end;
procedure TForm1.DrawGrid1SelectCell(Sender: TObject; ACol, ARow: Integer; var CanSelect: Boolean);
var RecNum, ignore: integer;
begin
GridRowToRecordAndLine(ARow, RecNum, ignore); // convert selected row number to record number
CanSelect := RecNum <> 0; // don't select unoccupied rows
end;
procedure TForm1.DrawGrid1TopLeftChanged(Sender: TObject);
begin
AdjustGrid; // keep last page looking good
end;
procedure TForm1.DrawGrid1DblClick(Sender: TObject);
var RecNum, ignore, delta: integer;
begin // expand or collapse the currently selected record
GridRowToRecordAndLine(DrawGrid1.Selection.Top, RecNum, ignore); // convert selected row number to record number
isExpanded[RecNum] := not isExpanded[RecNum]; // mark record as expanded or collapsed; subsequent records might change their position in the grid
delta := LinesInThisRecord(RecNum) - 1; // amount we grew or shrank (-1 since record already occupied 1 line)
if isExpanded[RecNum] then // just grew
else delta := -delta; // just shrank
DrawGrid1.RowCount := DrawGrid1.RowCount + delta; // keep rowcount in sync
AdjustGrid; // keep last page looking good
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Caption := FormatFloat('#,##0 records', TOTALRECORDS);
DrawGrid1.RowCount := TOTALRECORDS + 1; // +1 for column heading
DrawGrid1.ColCount := 1;
DrawGrid1.DefaultColWidth := 300; // arbitrary
DrawGrid1.DefaultRowHeight := 12; // arbitrary
DrawGrid1.Options := DrawGrid1.Options - [goVertLine, goHorzLine, goRangeSelect] + [goDrawFocusSelected, goThumbTracking]; // change some defaults
end;
end.
答案 3 :(得分:0)
您不应该使用ResetNode,因为此方法会调用InvalidateNode并再次初始化节点,从而导致与预期相反的效果。 我不知道是否可以诱导VST释放NodeDataSize中指定的内存大小而不实际删除节点。但是为什么不将NodeDataSize设置为Pointer(Delphi, VirtualStringTree - classes (objects) instead of records)的大小并自己管理数据呢?只是一个想法...
答案 4 :(得分:0)
尝试“DeleteChildren”。以下是此程序的评论所说的内容:
// Removes all children and their children from memory without changing the vsHasChildren style by default.
从未使用它,但在我阅读它时,您可以在OnCollapsed事件中使用它来释放分配给刚刚变得不可见的节点的内存。然后在OnExpading中重新生成这些节点,以便用户永远不会知道节点离开内存。
但我无法确定,我从未需要这样的行为。