TVirtualStringTree - сброс невидимых узлов и потребления памяти

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

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

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

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

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

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

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

Какие-либо идеи?

8
задан mghie 12 May 2010 в 09:56
поделиться

5 ответов

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

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

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

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

Edit:

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

1
ответ дан 6 December 2019 в 00:54
поделиться

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

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

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

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

0
ответ дан 6 December 2019 в 00:54
поделиться

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

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

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

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

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

1
ответ дан 6 December 2019 в 00:54
поделиться

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

0
ответ дан 6 December 2019 в 00:54
поделиться

Чтобы выполнить ваше требование «разворачивать / сворачивать записи, охватывающие несколько строк» ​​, я бы просто использовал 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.

1
ответ дан 6 December 2019 в 00:54
поделиться
Другие вопросы по тегам:

Похожие вопросы: