2010-05-11 3 views
7

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

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

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

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

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

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

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

Любые идеи?

ответ

1

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

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

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

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

Edit:

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

+0

В настоящее время я использую TListView в режиме vsReport, а не TListBox. TListView не поддерживает элементы переменной высоты в vsReport. –

+0

Я отмечаю это как ответ на данный момент, но только потому, что он говорит мне не переключиться на VST. –

0

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

+0

Это то, что я сейчас делаю. Фактически, я в настоящее время использую NodeDataSize = 0, но приложение по-прежнему использует до 200 + МБ памяти. Это связано с тем, что минимальный размер самого TVirtualNode составляет 44 байта, поэтому умноженное на 5 миллионов узлов (44 x 5000000) = 220000000 = 214843.75 Kb = 209.81 МБ, и это только для памяти TVirtualNode, не считая накладных расходов, которые вместо этого используется менеджер памяти VCL. –

+0

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

1

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

nodecount * (SizeOf (TVirtualNode) + YourNodeDataSize + DWORD-ALIGN-набивка)

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

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

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

+0

Фактически, я уже использую файл с отображением памяти, но из-за возможного размера файла (может быть до 1+ ГБ) и способа реализации фильтрации экрана я не сопоставляю весь файл в памяти в одно время. Что касается реализации древовидной структуры в коде, это не очень поможет, так как большинство элементов файла журнала не будут иметь дочерние узлы. Вот почему ListView в настоящее время используется для отображения. Поддержка дочерних узлов заключается в том, чтобы разместить функцию, в которой записи многострочного журнала хранятся в файле журнала как отдельные записи, которые я бы объединил в памяти для упрощения управления. –

0

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

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

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

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

+0

DeleteChildren() не решает проблемы, которые у меня есть. У большинства узлов не было бы никаких дочерних узлов. Я надеялся добавить поддержку тем немногим, которые были бы. Мне нужно освободить память для узлов верхнего уровня, которые становятся невидимыми при прокрутке. –

+0

Все видимые узлы в TVirtualTree требуют TVirtualNode, потому что все события, используемые для получения информации, связанной с узлами (например: CellText), ожидают, что PVirtualNode будет единственным способом идентифицировать узел. Если это проблема для вас, вы можете захотеть заглянуть в TVTNodeMemoryManager (и, возможно, взломать его под резервную копию файла с памятью) ИЛИ вернуться к исходному решению ListView, как это было предложено mghie. И всегда есть решение «100% custom control», учитывая ваши особые потребности! Создание собственного может быть быстрее, чем взломать альтернативы. –

+0

Хотя пользовательский контроль был бы приятным, у меня нет такого свободного времени для его разработки. Я посмотрю на TVTNodeMemoryManager, но я не думаю, что это поможет, так как все физические элементы TVirtualNode все равно должны быть выделены для того, чтобы дерево функционировало. –

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. 

+0

Вы настраиваете RowCount сетки на основе того, сколько строк нужно добавить/удалить. В своем первоначальном сообщении я сказал, что уже знал об этом методе: «Я могу использовать его с помощью TListView, увеличивая/уменьшая количество элементов динамически». Это маршрут, который мне придется принять. –

+0

@Remy: но вы также сказали, что это было «не прямо вперед». с drawgrid, развернуть/свернуть примерно так же просто, как вы можете получить, плюс у вас никогда не будет загрузки/выделения/освобождения/узлов/родителей/детей/и т.д., о которых нужно беспокоиться. если listview работает, но запутан, а drawgrid работает и прост, идите с drawgrid. не забудьте попробовать образец кода. если вы сравниваете его бок о бок с кодом, необходимым для той же функции в списке, я думаю, что большинство кодеров найдут решение drawgrid легче отслеживать/поддерживать/изменять/и т. д. –

+0

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

 Смежные вопросы

  • Нет связанных вопросов^_^