TVirtualStringTree - сброс невизуальных узлов и потребление памяти - PullRequest
8 голосов
/ 12 мая 2010

У меня есть приложение, которое загружает записи из двоичного файла журнала и отображает их в виртуальном TListView.В файле потенциально содержатся миллионы записей, и отображение может быть отфильтровано пользователем, поэтому я не загружаю все записи в память за один раз, и индексы элемента ListView не являются отношением 1 к 1 ссмещения записи файла (например, элементом 1 списка может быть запись 100 файла).Я использую событие OnDataHint в ListView для загрузки записей только для тех элементов, которые на самом деле интересуют ListView. По мере прокрутки пользователь меняет диапазон, указанный в OnDataHint, что позволяет мне освобождать записи, не входящие в новый диапазон, и выделять новые записи.при необходимости.

Это работает нормально, скорость терпима, и объем памяти очень низкий.

В настоящее время я оцениваю TVirtualStringTree как замену для TListView, главным образом потому, что я хочу добавитьвозможность разворачивать / сворачивать записи, которые занимают несколько строк (я могу выдумать это с помощью TListView, динамически увеличивая / уменьшая количество элементов, но это не так просто, как при использовании реального дерева).

Для большинствачастично я смог портировать логику TListView и заставить все работать так, как мне нужно.Я замечаю, что виртуальная парадигма TVirtualStringTree сильно отличается.Он не имеет такой же функциональности OnDataHint, как у TListView (я могу использовать событие OnScroll, чтобы подделать его, что позволяет продолжить работу логики буфера памяти), и я могу использовать событие OnInitializeNode, чтобы связать узлы с выделенными записями.

Однако, как только узел дерева инициализирован, он видит, что он остается инициализированным в течение всего времени жизни дерева.Это не хорошо для меня.Когда пользователь прокручивает и я удаляю записи из памяти, мне нужно сбросить эти невизуальные узлы, не удаляя их полностью из дерева и не теряя их состояния развертывания / свертывания.Когда пользователь прокручивает их обратно в поле зрения, я могу перераспределить записи и повторно инициализировать узлы.По сути, я хочу, чтобы TVirtualStringTree действовал так же, как и TListView, насколько это было возможно с точки зрения его виртуализации.

Я видел, что TVirtualStringTree имеет метод ResetNode (), но я сталкиваюсь с различными ошибками всякий раз, когда пытаюсьиспользуй это.Я должен использовать это неправильно.Я также подумал о том, чтобы просто хранить указатель данных внутри каждого узла в моих буферах записи, и я выделяю и освобождаю память, соответственно обновляю эти указатели.Конечный эффект тоже не очень хорошо работает.

Хуже того, мой самый большой файл журнала испытаний содержит ~ 5 миллионов записей.Если я инициализирую TVirtualStringTree с таким количеством узлов за один раз (когда отображение журнала не отфильтровано), внутренние издержки дерева для его узлов занимают колоссальные 260 МБ памяти (без выделения каких-либо записей).Принимая во внимание, что с TListView, загружая тот же файл журнала и всю логику памяти за ним, я могу избежать использования всего лишь нескольких МБ.

Есть идеи?

Ответы [ 5 ]

1 голос
/ 22 июня 2010

Чтобы удовлетворить ваше требование "развернуть / свернуть записи, которые занимают несколько строк" , я бы просто использовал сетку. Чтобы проверить это, перетащите сетку на форму, а затем вставьте следующий код 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) &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.

1 голос
/ 12 мая 2010

Если я правильно понимаю, требование к памяти TVirtualStringTree должно быть:

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

Чтобы минимизировать объем памяти, вы, возможно, могли бы инициализировать узлы только указателями на смещения в отображенном в памяти файле. Сброс уже инициализированных узлов не кажется необходимым в этом случае - объем памяти должен составлять количество узлов * (44 + 4 + 0) - для 5 миллионов записей, около 230 МБ.

ИМХО, с деревом лучше не получится, но использование отображенного в память файла позволит вам считывать данные непосредственно из файла, не выделяя еще больше памяти и не копируя в него данные.

Вы также можете рассмотреть возможность использования древовидной структуры вместо плоского представления для представления данных. Таким образом, вы можете инициализировать дочерние узлы родительского узла по требованию (когда родительский узел разворачивается) и сбрасывать родительский узел, когда он свернут (следовательно, освобождаются все его дочерние узлы). Другими словами, старайтесь не иметь слишком много узлов на одном уровне.

1 голос
/ 12 мая 2010

Вы, вероятно, не должны переключаться на VST, если у вас нет хотя бы некоторых приятных функций VST, которых нет в стандартном списке / просмотре списка. Но, конечно, есть большие накладные расходы памяти по сравнению с простым списком элементов.

Я не вижу реальной пользы в использовании TVirtualStringTree только для возможности разворачивать и сворачивать элементы, которые занимают несколько строк. Вы пишете

главным образом потому, что я хочу добавить возможность разворачивать / сворачивать записи, которые занимают несколько строк (я могу выдумать это с помощью TListView, динамически увеличивая / уменьшая количество элементов, но это не так просто, как при использовании реального дерева) .

но вы можете легко реализовать это, не меняя количество предметов. Если вы установите Style в списке на lbOwnerDrawVariable и реализуете событие OnMeasureItem, вы можете отрегулировать высоту по мере необходимости, чтобы нарисовать только первую или все линии. Рисование треугольника расширителя или небольшого плюса в виде дерева вручную должно быть простым. Функции Windows API DrawText() или DrawTextEx() могут использоваться как для измерения, так и для рисования текста (необязательно, завернутый в слова).

Edit:

Извините, я полностью упустил тот факт, что вы сейчас используете просмотр списка, а не список. Действительно, в списке нет способа иметь строки разной высоты, так что это не вариант. Вы все еще можете использовать список со стандартным элементом управления заголовка сверху, но он может не поддерживать все, что вы используете сейчас, от функциональности просмотра списка, и само по себе может потребоваться столько же или даже больше работы, чтобы получить права, чем динамическое отображение и скрытие строк списка просмотра для симулировать свертывание и расширение.

0 голосов
/ 12 мая 2010

Попробуйте "DeleteChildren". Вот что говорит комментарий этой процедуры:

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

Никогда не использовал его, но, как я читал, вы можете использовать его в событии OnCollapsed, чтобы освободить память, выделенную узлам, которые только что стали невидимыми. А затем заново сгенерируйте эти узлы в OnExpading, чтобы пользователь никогда не знал, что узел ушел из памяти.

Но я не могу быть уверен, мне никогда не нужно было такое поведение.

0 голосов
/ 12 мая 2010

Не следует использовать ResetNode, поскольку этот метод вызывает InvalidateNode и снова инициализирует узел, что приводит к эффекту, противоположному ожидаемому. Я не знаю, возможно ли заставить VST освободить объем памяти, указанный в NodeDataSize, без фактического удаления узла. Но почему бы не установить для NodeDataSize размер Pointer ( Delphi, VirtualStringTree - классы (объекты) вместо записей ) и управлять данными самостоятельно? Просто идея ...

...