8

我有一个应用程序,它从二进制日志文件加载记录并将它们显示在虚拟 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 就可以逃脱。

有任何想法吗?

4

5 回答 5

1

如果我理解正确,内存要求TVirtualStringTree应该是:

nodecount * (SizeOf(TVirtualNode) + YourNodeDataSize + DWORD-align-padding)

为了最大限度地减少内存占用,您也许可以仅使用指向内存映射文件的偏移量的指针来初始化节点。在这种情况下,似乎不需要重置已经初始化的节点 - 内存占用应该是 nodecount * (44 + 4 + 0) - 对于 500 万条记录,大约 230 MB。

恕我直言,您无法更好地使用树,但使用内存映射文件可以让您直接从文件中读取数据,而无需分配更多内存并将数据复制到其中。

您还可以考虑使用树结构而不是平面视图来呈现数据。这样,您可以按需初始化父节点的子节点(当父节点展开时)并在父节点折叠时重置父节点(因此释放其所有子节点)。换句话说,尽量不要在同一级别有太多的节点。

于 2010-05-12T09:13:43.543 回答
1

您可能不应该切换到 VST,除非您至少使用了标准列表框/列表视图所没有的 VST 的一些不错的功能。但是与平面的项目列表相比,当然会有很大的内存开销。

我没有看到TVirtualStringTree仅使用能够展开和折叠跨越多行的项目的真正好处。你写

主要是因为我想添加扩展/折叠跨越多行的记录的能力(我可以通过动态增加/减少项目计数来使用 TListView 捏造它,但这不像使用真正的树那么简单)。

但是您可以在不更改项目数的情况下轻松实现。如果Style将列表框设置为lbOwnerDrawVariable并实现OnMeasureItem事件,则可以根据需要调整高度以仅绘制第一行或所有行。手动绘制扩展三角形或树视图的小加号应该很容易。Windows API 函数DrawText()DrawTextEx()可用于测量和绘制(可选自动换行)文本。

编辑:

抱歉,我完全错过了您现在使用的是列表视图,而不是列表框的事实。事实上,没有办法在列表视图中拥有不同高度的行,所以这是没有选择的。您仍然可以使用顶部带有标准标题控件的列表框,但这可能不支持您现在从列表视图功能中使用的所有内容,并且它本身可能与动态显示和隐藏列表视图行一样多甚至更多的工作模拟折叠和展开。

于 2010-05-12T09:53:16.897 回答
1

为了满足您“展开/折叠跨越多行的记录”的要求,我只需使用绘图网格。要检查它,请将绘图网格拖到表单上,然后插入以下 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.

于 2010-06-22T04:52:50.923 回答
0

您不应该使用 ResetNode,因为此方法会调用 InvalidateNode 并再次初始化节点,从而导致与预期相反的效果。我不知道是否可以在不实际删除节点的情况下诱导 VST 释放 NodeDataSize 中指定的内存大小。但是为什么不将 NodeDataSize 设置为指针的大小(Delphi、VirtualStringTree - 类(对象)而不是记录)并自己管理数据呢?只是一个想法...

于 2010-05-12T09:11:39.710 回答
0

试试“删除儿童”。以下是此程序的评论内容:

// Removes all children and their children from memory without changing the vsHasChildren style by default.

从未使用过它,但当我阅读它时,您可以在 OnCollapsed 事件中使用它来释放分配给刚刚变得不可见的节点的内存。然后在 OnExpading 中重新生成这些节点,这样用户就永远不会知道节点从内存中消失了。

但我不能确定,我从来不需要这种行为。

于 2010-05-12T11:06:56.643 回答