代码之家  ›  专栏  ›  技术社区  ›  Remy Lebeau

Tvirtualstringtree-重置非可视节点和内存消耗

  •  8
  • Remy Lebeau  · 技术社区  · 15 年前

    我有一个应用程序,它从一个二进制日志文件中加载记录,并将它们显示在一个虚拟的tlistview中。一个文件中可能有数百万条记录,用户可以过滤显示,因此我不会一次将所有记录加载到内存中,并且ListView项索引与文件记录偏移量没有1对1关系(例如,列表项1可能是文件记录100)。我使用ListView的OnDataHint事件仅为ListView实际感兴趣的项加载记录。当用户滚动时,OnDataHint指定的范围将更改,允许我释放不在新范围内的记录,并根据需要分配新记录。

    这工作得很好,速度可以忍受,内存占用非常低。

    我目前正在评估Tvirtualstringtree作为tlistview的替换,主要是因为我想添加扩展/折叠跨多行记录的功能(我可以通过动态增加/减少项目计数来使用tlistview来篡改它,但这并不像使用真正的树那样直接)。

    在很大程度上,我已经能够移植tlistview逻辑,并且可以根据需要工作。不过,我注意到Tvirtualstringtree的虚拟范式有很大的不同。它不具有与tlistview相同的OnDataHint功能(我可以使用OnScroll事件来伪造它,这允许我的内存缓冲区逻辑继续工作),并且我可以使用OnInitializeNode事件将节点与分配的记录相关联。

    但是,一旦一个树节点被初始化,它就会看到它在树的生命周期内保持初始化状态。那对我不好。当用户滚动并从内存中删除记录时,我需要重置这些非可视节点,而不将它们完全从树中删除,或者丢失它们的展开/折叠状态。当用户将它们滚动回视图时,我可以重新分配记录并重新初始化节点。基本上,我想让tvirtualstringtree尽可能地像tlistview,就其虚拟化而言。

    我已经看到tvirtualstringtree有resetnode()方法,但每当我尝试使用它时,都会遇到各种错误。我一定是用错了。我还考虑在每个节点中存储一个数据指针到我的记录缓冲区,然后分配和释放内存,相应地更新这些指针。最终效果也不太好。

    更糟糕的是,我最大的测试日志文件中有大约500万条记录。如果我一次用那么多节点初始化Tvirtualstringtree(当日志显示未被过滤时),那么该树的节点内部开销将占用260MB的内存(还没有分配任何记录)。然而,使用tlistview,加载同一个日志文件以及它背后的所有内存逻辑,我可以不必使用几个mbs。

    有什么想法吗?

    5 回复  |  直到 15 年前
        1
  •  1
  •   mghie    15 年前

    您可能不应该切换到VST,除非您至少使用了标准列表框/列表视图不具备的VST的一些好功能。当然,与简单的项目列表相比,内存开销很大。

    我看不到真正的好处在使用 TVirtualStringTree 只能展开和折叠跨多行的项。你写

    主要是因为我想添加扩展/折叠跨多行记录的功能(我可以通过动态增加/减少项目计数来使用tlistview来篡改它,但这不像使用真正的树那样直接)。

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

    编辑:

    抱歉,我完全忽略了您现在使用的是列表视图,而不是列表框。事实上,在ListView中没有不同高度的行,所以这是没有选择的。您仍然可以使用一个顶部带有标准头控件的列表框,但这可能不支持您现在从ListView功能中使用的所有内容,而且与其动态显示和隐藏ListView行以模拟折叠和展开,还不如使用更多的工作来纠正错误。

        2
  •  1
  •   Ondrej Kelle    15 年前

    如果我理解正确,记忆的需求 TVirtualStringTree 应该是:

    节点计数*(sizeof(tvirtualnode)+yournodedatasize+dword对齐填充)

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

    imho您无法更好地使用树,但使用内存映射文件将允许您直接从文件中读取数据,而无需分配更多的内存并将数据复制到该文件中。

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

        3
  •  1
  •   joe snyder    15 年前

    满足你的要求 “展开/折叠跨多行的记录” 我只需要使用一个网格。若要检查它,请将DrawGrid拖到表单上,然后插入以下Delphi6代码。您可以折叠和扩展5000000条多行记录(或者您想要的任何数量),基本上没有开销。这是一种简单的技术,不需要太多代码,而且工作得非常好。

    
    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) &lt (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 &lt> 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.
    
    
        4
  •  0
  •   Community CDub    8 年前

    您不应该使用resetnode,因为此方法调用invalidatenode并再次初始化节点,从而产生与预期相反的效果。 我不知道是否可以在不实际删除节点的情况下诱导vst释放nodedatasize中指定的内存大小。但是为什么不把nodedatasize设置为指针的大小呢( Delphi, VirtualStringTree - classes (objects) instead of records )自己管理数据?只是一个想法…

        5
  •  0
  •   Cosmin Prund    15 年前

    尝试“删除子项”。以下是该程序的注释:

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

    从未使用过它,但在我阅读它时,您可以在oncollapsed事件中使用它来释放分配给刚刚变为不可见的节点的内存。然后在一次扩展中重新生成这些节点,这样用户就永远不会知道节点离开了内存。

    但我不能确定,我从来就不需要这样的行为。