2010-05-11 20 views
7

Tengo una aplicación que carga registros de un archivo de registro binario y los muestra en un TListView virtual. Hay potencialmente millones de registros en un archivo, y la pantalla puede ser filtrada por el usuario, por lo que no carga todos los registros en la memoria a la vez, y los índices de elementos ListView no son una relación de 1 a 1 con las compensaciones de registros de archivos (la lista de elementos 1 puede ser el registro de archivos 100, por ejemplo). Uso el evento OnDataHint de ListView para cargar registros solo para los elementos en los que ListView está realmente interesado. A medida que el usuario se desplaza, el rango especificado por OnDataHint cambia, lo que me permite liberar registros que no están en el nuevo rango y asignar nuevos registros según sea necesario.TVirtualStringTree - restauración de nodos visuales y consumo de memoria

Esto funciona bien, la velocidad es tolerable y la huella de memoria es muy baja.

Actualmente estoy evaluando TVirtualStringTree como reemplazo del TListView, principalmente porque deseo agregar la capacidad de expandir/colapsar registros que abarcan varias líneas (puedo mezclarlo con el TListView aumentando/disminuyendo dinámicamente el conteo de elementos, pero esto no es tan sencillo como usar un árbol real).

En su mayor parte, he sido capaz de portar la lógica TListView y hacer que todo funcione como lo necesito. Observo que el paradigma virtual de TVirtualStringTree es muy diferente. No tiene el mismo tipo de funcionalidad OnDataHint que TListView (puedo usar el evento OnScroll para simularlo, lo que permite que mi lógica de buffer de memoria continúe funcionando), y puedo usar el evento OnInitializeNode para asociar nodos con registros que están asignados .

Sin embargo, una vez que se inicializa un nodo de árbol, se ve que permanece inicializado durante la vida útil del árbol. Eso no es bueno para mi A medida que el usuario se desplaza y elimino registros de la memoria, necesito reiniciar esos nodos visuales sin eliminarlos del árbol por completo, o perder sus estados de expansión/colapso. Cuando el usuario los vuelva a visualizar, puedo volver a asignar los registros y reiniciar los nodos. Básicamente, quiero hacer que TVirtualStringTree actúe tanto como TListView como sea posible, en lo que respecta a su virtualización.

He visto que TVirtualStringTree tiene un método ResetNode(), pero encuentro varios errores cada vez que trato de usarlo. Debo usarlo mal. También pensé simplemente almacenar un puntero de datos dentro de cada nodo en mis búferes de registro, y asignar y liberar memoria, actualizar esos punteros en consecuencia. El efecto final tampoco funciona tan bien.

Peor aún, mi archivo de registro de prueba más grande tiene ~ 5 millones de registros en el mismo. Si inicializo el TVirtualStringTree con tantos nodos a la vez (cuando la pantalla de registro no está filtrada), la sobrecarga interna del árbol para sus nodos ocupa una friolera de 260 MB de memoria (sin ningún registro asignado aún). Mientras que con TListView, cargando el mismo archivo de registro y toda la lógica de la memoria detrás de él, puedo salirse con la suya usando solo unos pocos MB.

¿Alguna idea?

Respuesta

1

Probablemente no deba cambiar a VST a menos que tenga un uso para al menos algunas de las bonitas funciones de VST que un listbox/listview estándar no tiene. Pero, por supuesto, hay una gran sobrecarga de memoria en comparación con una lista plana de elementos.

No veo un beneficio real en el uso de TVirtualStringTree solo para poder expandir y contraer elementos que abarcan varias líneas. Usted escribe

principalmente porque quiero añadir la posibilidad de ampliar los registros/collapse que abarcan varias líneas (puedo eludir con el TListView mediante el incremento/decremento cuenta el elemento de forma dinámica, pero esto no es tan sencillo como usando un árbol real).

pero puede implementarlo fácilmente sin cambiar el número de elementos. Si configura el Style del cuadro de lista en lbOwnerDrawVariable e implementa el evento OnMeasureItem, puede ajustar la altura según sea necesario para dibujar solo la primera o todas las líneas. Dibujar el triángulo expansor o el pequeño símbolo más de una vista de árbol manualmente debería ser fácil. Las funciones API de Windows DrawText() o DrawTextEx() se pueden usar para medir y dibujar el texto (opcionalmente envuelto).

Editar:

Lo siento, me perdí por completo el hecho de que está utilizando una vista de lista en este momento, no es un cuadro de lista. De hecho, no hay forma de tener filas con diferentes alturas en una vista de lista, así que esa no es una opción. Todavía puede usar un cuadro de lista con un control de encabezado estándar en la parte superior, pero puede no ser compatible con todo lo que está utilizando ahora desde la funcionalidad de vista de lista, y puede ser tanto o más trabajo para obtenerlo que mostrar dinámicamente y ocultar filas de vista de lista para simular colapsar y expandir.

+0

Actualmente uso un TListView en el modo vsReport, no un TListBox. TListView no admite elementos de altura variable en vsReport. –

+0

Estoy marcando esta como la respuesta por ahora, pero solo porque me dice que no cambie a VST. –

0

No debe usar ResetNode porque este método invoca InvalidateNode e inicializa el nodo nuevamente, lo que genera un efecto contrario al esperado. No sé si es posible inducir a VST a liberar el tamaño de memoria especificado en NodeDataSize sin eliminar realmente el nodo. Pero, ¿por qué no establecer NodeDataSize al tamaño de Puntero (Delphi, VirtualStringTree - classes (objects) instead of records) y administrar los datos usted mismo? Solo una idea...

+0

Eso es lo que hago en este momento. De hecho, actualmente estoy usando NodeDataSize = 0, pero la aplicación aún usa más de 200 MB de memoria. Esto se debe a que el tamaño mínimo de TVirtualNode es de 44 bytes, por lo que multiplicado por 5 millones de nodos es (44 x 5000000) = 220000000 = 214843.75 Kb = 209.81 MB, y solo para la memoria de TVirtualNode, sin incluir la sobrecarga el administrador de memoria VCL además de eso. –

+0

Creo que es posible en VST que los nodos secundarios se vean exactamente como su nodo principal, por lo que el usuario no puede ver que son nodos secundarios. Podría agregar cinco mil nodos de nivel superior que cada uno tenga mil nodos secundarios. –

1

Si he entendido bien, el requisito de memoria de TVirtualStringTree debe ser:

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

Para reducir al mínimo el consumo de memoria, usted podría quizás inicialice los nodos con solo punteros para desplazar a un archivo mapeado en memoria. Restablecer nodos que ya se han inicializado no parece ser necesario en este caso: la huella de memoria debe ser nodecount * (44 + 4 + 0) para 5 millones de registros, unos 230 MB.

En mi humilde opinión no se puede mejorar con el árbol, pero el uso de un archivo mapeado en memoria le permitirá leer los datos directamente desde el archivo sin asignar aún más memoria y copiar los datos a la misma.

También podría considerar el uso de una estructura de árbol en lugar de una vista plana para presentar los datos. De esta forma, podría inicializar los nodos secundarios de un nodo padre a petición (cuando el nodo padre se expande) y restablecer el nodo padre cuando se colapsó (liberando así todos sus nodos secundarios). En otras palabras, trate de no tener demasiados nodos en el mismo nivel.

+0

De hecho, ya uso un archivo mapeado de memoria, pero debido al tamaño posible del archivo (puede ser de hasta 1, 2 GB) y la forma en que se implementa el filtrado de pantalla, no mapeo todo el archivo en la memoria de una sola vez. En cuanto a la implementación de una estructura de árbol en el código, eso no ayudará mucho ya que la mayoría de los elementos del archivo de registro no tendrán nodos secundarios. Es por eso que ListView se está utilizando actualmente para la pantalla. Soportar nodos secundarios es acomodar una función donde las entradas de registro de varias líneas se almacenan en el archivo de registro como registros separados que combinaría en la memoria para una administración más fácil –

0

Prueba "DeleteChildren". Esto es lo que dice el comentario de este procedimiento:

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

nunca utilizado, pero como lo leí, se puede utilizar en caso de que OnCollapsed para liberar la memoria asignada a los nodos que sólo se convirtió en invisible. Y luego, vuelva a generar esos nodos en OnExpading para que el usuario nunca sepa que el nodo se fue de la memoria.

Pero no puedo estar seguro, nunca tuve la necesidad de tal comportamiento.

+0

DeleteChildren() no soluciona las inquietudes que tengo. La mayoría de los nodos no tendrían ningún nodo hijo para comenzar. Esperaba agregar soporte para los pocos que lo harían. Lo que necesito es liberar memoria para los nodos de nivel superior que se vuelven invisibles mientras se desplazan. –

+0

Todos los nodos visibles en TVirtualTree requieren un TVirtualNode porque todos los eventos utilizados para obtener cosas relacionadas con los nodos (por ejemplo, CellText) esperan un PVirtualNode como única forma de identificar el Nodo. Si eso es un problema para usted, entonces puede que desee examinar el TVTNodeMemoryManager (y tal vez hackearlo para que esté respaldado por un archivo asignado a la memoria) O volver a su solución inicial ListView como lo sugirió mghie. Y siempre existe la solución de "control 100% personalizado", teniendo en cuenta sus necesidades muy especiales. Hacer tu propio puede ser más rápido que piratear las alternativas. –

+0

Aunque un control personalizado sería bueno, no tengo ese tipo de tiempo libre para desarrollar uno. Miraré TVTNodeMemoryManager, pero no creo que sea de mucha ayuda, ya que todos los elementos físicos de TVirtualNode todavía tienen que asignarse para que el árbol funcione. –

1

Para cumplir con su requisito "para expandir/contraer registros que abarcan varias líneas", simplemente usaría una cuadrícula de dibujo. Para verificarlo, arrastre una cuadrícula de dibujo en un formulario, luego conecte el siguiente código de Delphi 6. Puede colapsar y expandir 5,000,000 registros de líneas múltiples (o la cantidad que desee) sin esencialmente gastos indirectos. Es una técnica simple, no requiere mucho código y funciona sorprendentemente bien.


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

Está ajustando el RowCount de la cuadrícula según el número de filas que se deben agregar o eliminar. Dije en mi mensaje original que ya sabía sobre esa técnica: "Puedo mezclarlo con el TListView aumentando/disminuyendo dinámicamente el conteo de elementos". Esa es la ruta que tendré que tomar. –

+0

@Remy: pero también dijiste que no era "directo". con un drawgrid, expand/collapse es lo más simple que puedes obtener, además de que nunca tienes que cargar/allocating/freeing/nodes/parents/children/etc de los que preocuparte. si la vista de lista funciona, pero está desordenada, y drawgrid funciona y es simple, vaya con la cuadrícula. asegúrese de probar el código de muestra. si lo comparas lado a lado con el código necesario para la misma funcionalidad en la vista de lista, creo que la mayoría de los programadores encontrarán que la solución drawgrid es más fácil de seguir/mantener/modificar/etc. –

+0

Desafortunadamente, debido a la naturaleza dinámica de mi carga de datos y administración de memoria, no es una solución simple si uso un ListView o un DrawGrid (también, como un control personalizado, un DrawGrid tiene una sobrecarga interna adicional que un ListView estándar no) –

Cuestiones relacionadas