8

バイナリ ログ ファイルからレコードを読み込み、それらを仮想 TListView に表示するアプリがあります。ファイルには数百万のレコードが含まれている可能性があり、ユーザーが表示をフィルター処理できるため、すべてのレコードを一度にメモリにロードすることはなく、ListView アイテムのインデックスは 1 対 1 の関係ではありません。ファイル レコード オフセット (たとえば、リスト項目 1 はファイル レコード 100 の場合があります)。ListView の OnDataHint イベントを使用して、ListView が実際に関心を持っている項目だけのレコードを読み込みます。ユーザーがスクロールすると、OnDataHint で指定された範囲が変化するため、新しい範囲にないレコードを解放し、新しいレコードを割り当てることができます。必要に応じて。

これは問題なく動作し、速度は許容範囲内であり、メモリ フットプリントは非常に小さいです。

私は現在、TListView の代わりとして TVirtualStringTree を評価しています。主な理由は、複数行にまたがるレコードを展開/折りたたむ機能を追加したいからです (項目数を動的に増減することで、TListView でそれをごまかすことができますが、これはそうではありません実際のツリーを使用するのと同じくらい簡単です)。

ほとんどの場合、TListView ロジックを移植して、必要に応じてすべてを機能させることができました。ただし、TVirtualStringTree の仮想パラダイムは大きく異なることに気付きました。これには、TListView と同じ種類の OnDataHint 機能はありません (OnScroll イベントを使用してそれを偽造することができます。これにより、メモリ バッファー ロジックは引き続き機能します)。また、OnInitializeNode イベントを使用して、割り当てられたレコードにノードを関連付けることができます。 .

ただし、ツリー ノードが初期化されると、ツリーの存続期間中は初期化されたままになります。それは私には良くありません。ユーザーがスクロールしてメモリからレコードを削除すると、これらの非表示ノードをツリーから完全に削除したり、展開/折りたたみ状態を失ったりせずにリセットする必要があります。ユーザーがそれらをスクロールして表示に戻すと、レコードを再割り当てしてノードを再初期化できます。基本的に、仮想化に関する限り、TVirtualStringTree をできるだけ TListView と同じように動作させたいと考えています。

TVirtualStringTree に ResetNode() メソッドがあることは確認しましたが、使用しようとするとさまざまなエラーが発生します。使い方が間違っているに違いない。また、各ノード内のデータ ポインターをレコード バッファーに格納することも考えました。メモリを割り当てて解放し、それに応じてそれらのポインターを更新します。エンドエフェクトもあまりうまくいきません。

さらに悪いことに、私の最大のテスト ログ ファイルには、約 500 万件のレコードが含まれています。一度に多くのノードで TVirtualStringTree を初期化すると (ログ表示がフィルタリングされていない場合)、そのノードのツリーの内部オーバーヘッドが 260MB ものメモリを消費します (レコードはまだ割り当てられていません)。一方、TListView を使用すると、同じログ ファイルとその背後にあるすべてのメモリ ロジックを読み込むため、数 MB を使用するだけで済みます。

何か案は?

4

5 に答える 5

1

「複数行にまたがるレコードを展開/折りたたむ」という要件を満たすには、単純に drawgrid を使用します。それを確認するには、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.

于 2010-06-22T04:52:50.923 に答える
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 に答える
0

このメソッドは InvalidateNode を呼び出してノードを再度初期化するため、ResetNode を使用しないでください。予想とは逆の結果になります。実際にノードを削除せずに、VST に NodeDataSize で指定されたメモリ サイズを解放させることが可能かどうかはわかりません。しかし、NodeDataSize を Pointer のサイズ ( Delphi、VirtualStringTree - レコードではなくクラス (オブジェクト) ) に設定して、自分でデータを管理してみませんか? ただのアイデア...

于 2010-05-12T09:11:39.710 に答える
0

「DeleteChildren」を試してみてください。この手順のコメントは次のとおりです。

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

使用したことはありませんが、私が読んだように、それを OnCollapsed イベントで使用して、非表示になったばかりのノードに割り当てられたメモリを解放できます。次に、これらのノードを OnExpading で再生成して、ノードがメモリから離れたことをユーザーが認識しないようにします。

しかし、私はそのような行動の必要性がなかったので、確信が持てません.

于 2010-05-12T11:06:56.643 に答える