Virtual Treeview step by step

von Sven H. (h.sven@gmx.at), Überarbeitung von Mike Lischke (public@lischke-online.de)
 
Zum Zeitpunkt als diese Beschreibung entstanden ist, hatte ich noch wenig Delphi-Kenntnisse und habe noch keines meiner 2 Delphibücher fertig gelesen. Aber natürlich war ich ungeduldig und wollte gleich einmal ausprobieren, was so geht.

Ich habe zwar etwas Ahnung von objektorientierter Programmierung und C++ (habe darüber auch einiges während meines Studiums gelernt), dieses Projekt ist jedoch mein erster Gehversuch in Delphi.

Es kann sein, dass meine Lösungen nicht die elegantesten sind und ich bin für Verbesserungsvorschläge immer zu haben. Aber alle Prinzipien, die ich hier demonstriere, funktionieren auch (zumindest bei mir ). Ich habe sie so in meinem ersten Projekt implementiert.

Diese Anleitung wendet sich in erster Linie an Programmierer, die mit dem Virtual Treeview noch nicht vertraut sind und so vielleicht einen leichteren Einstieg haben.

Für Fragen und Anregungen, die diese Kurzanleitung betreffen, bitte an h.sven@gmx.at wenden. Für sonstige Fragen seid ihr sicher bei Mike bzw. der dafür eingerichteten Yahoo Group (http://www.yahoogroups.com/group/VirtualTreeview) besser aufgehoben.

Ich bin weder ein Virtual Treeview- noch ein Delphi-Experte und habe mir alle Antworten mühsam (mit der Hilfe von Mike) erkämpft. Damit die im nachhinein relativ simplen Dinge keine große Hürde darstellen und jeder diese tolle Komponente rasch produktiv nutzen kann, habe ich diese Kurzanleitung geschrieben. Die 'richtigen' Probleme beginnen erst danach .

© 2001 Der über das Wiedergeben von Textstellen der Onlinehilfe hinausgehende Teil dieser Anleitung ist urheberrechtlich geschützt. Jede Veröffentlichung bedarf meiner Zustimmung.

Viel Spaß damit, Sven.

1	Vorbereitungen
 
Bevor wir beginnen, sind einige Vorbereitungen notwendig:

o	Die Komponente (Virtual Treeview) muss auf ein Formular gelegt werden.
o	Die Eigenschaften der Komponente sollten wie gewünscht festlegen werden.
o	Ein Record für die Knotendaten muss definiert werden.
 
Um die 'eigenen' Knotendaten zu speichern, müssen einige Überlegungen angestellt werden. Wie soll der Record aussehen?

a)	alle Knoten im Tree sind gleich
In diesem Fall kann ein einfacher Record die notwendige Daten-Struktur definieren. Also z. B.:

type
  rTreeData = record
    Text: WideString;
    URL: string[255];
    CRC: LongInt;
    isOpened: Boolean;
    ImageIndex: Integer;
  end;

b)	es gibt verschiedene Knoten im Tree (also z.B. Ordner die andere Unterknoten enthalten können)
Diesen Fall werde ich nun weiter verfolgen da mein Baum Ordner enthalten soll, die wiederum eigene Knoten enthalten können.

Da ich vorhabe erzeugte Bäume in einem File zu sichern, um den Baum später wieder rekonstruieren zu können, sind weitere Überlegungen notewendig:

Angenommen, ein Ordner-Knoten hat nur einen Namen und ein Blattknoten hat einen Namen und ein Textinfofeld. Möglicherweise möchte ich auch noch eine zweite Art von Blattknoten speichern, der dann z.B. anstelle des Testfeldes einen Zahlenwert aufnehmen kann usw.

Das Problem im Zusammenhang mit dem Einlesen von Daten aus einem Stream ist, ich muss wissen welche Daten in welcher Reihenfolge im Stream gespeichert wurden, weil ich sie ja genau in dieser Reihenfolge wieder auslesen muss. Daher muss ich als allererste Information aus dem Stream lesen können, welche Folgeinformationen da nun 'ankommen'. Also z.B. da kommt ein Knotenname, aber dann? Kommt da jetzt gar nichts mehr, eine Textinformation (String) oder vielleicht gar ein Integerwert. Ich denke der Punkt ist klar. Die erste Information, die ich einlese, muss mir darüber Auskunft geben.

Diese Überlegungen haben mich zu folgender Lösung geführt: ich speichere also nun im Stream [Kennung]->[Name]->[Folgedaten]
0  'Ordner'
1  'Infoknoten'  'Blabla'
2  'Zahlenknoten'  123

Ich weiß, aus dem Stream lese ich zuerst einen Integerwert. Je nachdem ob ich 1, 2 oder 3 auslese, muss ich noch - nun bekannte - Folgewerte einlesen. So weit so gut, jetzt zum Record.

type
  rTreeData = record
    Typ: Integer;
    Name: string[255];
    pNodeData: Pointer;
  end;

Aber hallo, jetzt ist da auf einmal ein Pointer im Record? Also, noch ein paar Kommentare dazu:
1)	Typ ist ein Integerwert, anhand dessen ich feststellen kann, um welche Art von Knoten es sich handelt. Also, in meinem Beispiel 1, 2 oder 3.

2)	Name ist der Name des Knoten - er wird relativ oft benötigt, weil er ja auch als Text im Baum zu sehen ist und ich möchte auf diese Information leicht zugreifen können (Mann, bin ich faul).

3)	Der Pointer ermöglicht es mir (ähnlich wie die Data-Eigenschaft des Baums) einen Record oder noch besser eine Klasseninstanz damit zu verknüpfen.

Jetzt habe ich nach wie vor die Freiheit eine Basisklasse von Knoten zu definieren. Sie enthält dann alle Eigenschaften und Methoden, die alle Klassen gemeinsam haben. Und von ihr kann ich dann die entsprechenden Subklassen ableiten (z.B. Textknoten, Wertknoten usw.). Ein weiterer Vorteil dieses Records ist seine fixe Größe. Daher kann man, falls der Baum beim Hinzufügen eines neuen Knotens nach der Größe der Knotendaten fragt (Eigenschaft 'NodeDataSize'), immer die selbe Größe zurückliefern. Aber mehr dazu später.

Noch eine Anmerkung:
Will man keine Klassen verwenden, könnte man auch einfach 3 Records definieren, die als erstes Element einen Typ definieren und dann je nach Typ unterschiedlich reagieren. 

alternative Lösung:
Ja, ich gebe es ja zu. Es wäre natürlich auch möglich, den Typ so wie oben in den Stream zu schreiben und aus dem Stream zu lesen, ohne dass der Typ als Teil des Records gespeichert wird. Der Typ der Knotenklasse ist ja indirekt bekannt, weil man eine Klasse ja fragen kann, welchen Klassennamen sie hat (siehe z.B. Klassenfunktion ClassName) bzw. weiß die Klasse das ja selbst.

Also, ich soll einen Knoten speichern, okay. Ich gebe den Stream weiter an die Methode Knoten.SaveToFile(Stream) und diese Methode schreibt, je nachdem, um welche Knotenklasse es sich tatsächlich handelt, automatisch als ersten Wert 1, 2 oder 3 in den Stream.

Beim Lesen aus dem Stream lese ich zuerst den Wert 1, 2, oder 3 aus dem Stream aus und entscheide daraufhin, um welche Klasse es sich handelt. Dann erzeuge ich eine Instanz dieser Klasse und rufe ihre Methode LoadFromFile auf.

So, diese Lösung gefällt mir am besten und bevor mir noch eine andere einfällt, werde ich sie nun gleich implementieren. (Hinweis: in Schritt 5 nehme ich noch eine Veränderung vor)

Also mache ich jetzt folgendes:

Wie man an der Deklaration des 'internen' Knotens eines Virtual Treeviews erkennen kann 

  TVirtualNode = packed record 
    Index,	// index of node with regard to its parent 
    ChildCount: Cardinal;	// number of child nodes 
    ... 
    ... 
    LastChild: PVirtualNode;	// link to the node's last child... 
    Data: record end;	// this is a placeholder, each node gets extra
// data determined by NodeDataSize 
  end; 

wird am Ende des Records wieder eine Record-Struktur gespeichert. Welche Struktur das genau ist wird 'indirekt' festgelegt.

type
  rTreeData = record
    Name: string[255];	// the identifier of the node
    ImageIndex: Integer;	// the image index of the node
    pNodeData: Pointer;
  end;

soll die Struktur dieses Records sein. Der Virtual Treeview kennt die Struktur eigentlich nicht wirklich, aber er weiß, wie viel Platz dafür reserviert werden muss. Woher? Wir teilen ihm das durch 

myVirtualTree.NodeDataSize := SizeOf(rTreeData); 

mit.

Anmerkung: selbst wenn man nur einen einzigen Wert, wie z.B. einen Pointer als Knotendaten speichern will, wird einfach die Größe, die reserviert werden soll, angegeben.
 
2	Implementierung
2.1	Ein leerer Baum
 
Ich beginne mit einem leeren Baum (zur Entwurfszeit werden noch keine Top-Level-Knoten erzeugt)

o	entweder wird ein vorhandener Baum aus einem File eingelesen oder
o	es wird ein Top-Level-Knoten erzeugt

Bevor Knoten erzeugt werden, muss nun die Größe für die eigenen Knotendaten festgelegt werden. Laut Dokumentation gibt es dafür 3 Möglichkeiten:
o	im Objektinspektor
o	beim OnGetNodeDataSize-Event
o	oder beim Erzeugen eines Formulars

Ich entscheide mich für die letzte Variante und werde nun beim Erzeugen des Formulars folgendes tun:

procedure TMyForm.FormCreate(Sender: TObject);
var
  Node: PVirtualNode;
begin
  ...
  // create tree
  MyTree.NodeDataSize := SizeOf(TTreeData);
  if MyForm.filename = '' then begin // if there is no tree to load
    // create tree with top level node
    Node := BookmarkForm.BookmarkTree.AddChild(nil); // adds a top level node
  end
  else 
  begin
    // load tree
    ....
  end;
....

end;	
 
2.2	Daten für den Knoten
 
Nach dem Aufruf von AddChild können nun Daten für den Knoten zur Verfügung gestellt werden. Dazu wird ein Pointer auf den von uns definierten Record erzeugt und über die Funktion GetNodeData mit der richtigen Adresse belegt. Über diesen Pointer können wir nun auf die Elemente des Records zugreifen und diese mit Werten belegen.

var
  ...
  NodeData: ^rTreeData;
begin
  ...
  // determine data for node
  NodeData := BookmarkForm.BookmarkTree.GetNodeData(Node);
  NodeData.Name := 'new project';
  NodeData.ImageIndex := 0;
   ...
 
2.3	Den Knotennamen anzeigen
 
Der Name des Knoten soll nun als Knotenbezeichnung im Baum erscheinen. Alle Daten über den Knoten sind, wie auch der Name, dem VirtualTreeview unbekannt und er muss sie erst erfragen. 

Jedes Mal, wenn der Bezeichner des Knotens benötigt wird, wird das Ereignis OnGetText ausgelöst. In der Ereignisbehandlungsroutine geben wir den Namen des Knotens in der Variable Text zurück, mehr ist nicht zu tun.

procedure TBookmarkForm.BookmarkTreeGetText(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Column: Integer; TextType: TVSTTextType; var Text: WideString);
var
   NodeData: ^rTreeData;
begin
  NodeData := Sender.GetNodeData(Node);

  // return identifier of the node
  Text := NodeData.Name;
end;
 
2.4	Das Icon für den Knoten
 
Weil ich es gerne bunt habe, will ich auch gleich ein Icon für den Top-Level-Knoten zur Verfügung stellen. Dafür sind folgende Schritte notwendig:
o	ein TImageList auf die Form ziehen und mit Bildern laden
o	die Eigenschaft Images des VirtualTreeviews auf die TImageList zeigen lassen
o	das Ereignis OnGetImageIndex implementieren

Mit dem Ereignis OnGetImageIndex kann der Index festgelegt werden der wiederum bestimmt das wievielte Bild aus der Liste angezeigt werden soll.

Da die Methode auch für die Zustandsicons aufgerufen wird und ich momentan noch kein Zustandsicon anzeigen will (ich aber bereits eine ImageList mit der Eigenschaft StateImages verknüpft habe), muss für diesen Fall (Kind -> ikState) als Index -1 zurückgegeben werden.

procedure TBookmarkForm.BookmarkTreeGetImageIndex(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Kind: TVTImageKind; Column: Integer; var Index: Integer);
var
   NodeData: ^rTreeData;
begin
  NodeData := Sender.GetNodeData(Node);

  case Kind of
    ikState: // for the case the state icon has been requested
      Index := -1;
    ikNormal, ikSelected: // normal or the selected icon is required
      Index := NodeData.ImageIndex;
  end;

end;

Es soll, je nachdem, ob der Knoten selektiert wurde oder nicht, ein anderes Icon angezeigt werden (siehe Schritt 6).
 
2.5	Nur eine Knotenklasse im Record
 
Da ich nun diese Mischung von Daten im Record und später dann Daten in der Knotenklasse vermeiden möchte, habe ich mich entschieden diesen Record

type
  TTreeData = record
    Name: string[255]; // the identifier of the node
    ImageIndex: Integer; // the image index of the node
    pNodeData: Pointer;
  end;

in einen Record umzuwandeln, der nur einen Zeiger auf eine Knotenklasse beinhaltet.
Ich deklariere daher zuerst eine Knotenklasse

   TBasicNodeData = class
      ...
   end;

und dann eine Struktur der Form:

  rTreeData = record
    BasicND: TBasicNodeData;
  end;
   
Dieses Record belegt also immer 4 Bytes für den Zeiger auf die Klasse.

Dabei ist besonders Augenmerk auf das Ereignis OnGetText zu lenken. Dieses Ereignis wird schon beim Erzeugen des Knotens mit Tree.AddChild(nil) aufgerufen, um über einen eventuell vergebenen Text den grafischen Platzbedarf des Knotens zu ermitteln (jedoch nur, wenn keine Spalten erzeugt wurden). Zu diesem Zeitpunkt konnte die Knotenklasse jedoch noch nicht initialisiert werden (noch kein Konstruktor Aufruf) und daher muss für diesen Fall 

  if NodeD.BasicND = nil then 
    Text := '' 

zurückgegeben werden oder man packt die gesamte Initialisierung in einen BeginUpdate/EndUpdate Block und initialisiert die neuen Knoten bevor EndUpdate ausgeführt wurde (z.B. mittels ValidateNode(Node)).

Ohne diese Vorkehrung würde man sonst eine Zugriffsverletzung auslösen.

2.5.1	Beispiel Klassendeklaration
 
unit TreeData;

interface 

//===========================================

type
  // declare common node class
  TBasicNodeData = class
  protected
    cName: ShortString;
    cImageIndex: Integer;
  public
    constructor Create; overload;
    constructor Create(vName: ShortString; vIIndex: Integer = 0); overload; 

    property Name: ShortString read cName write cName;
    property ImageIndex: Integer read cImageIndex write cImageIndex;
  end;

  // declare new structure for node data
  rTreeData = record
    BasicND: TBasicNodeData;
  end;

implementation

constructor TBasicNodeData.Create;
begin
  { not necessary
   cName := '';
   cImageIndex := 0;
  }
end;

constructor TBasicNodeData.Create(vName: ShortString; vIIndex: Integer = 0);
begin
  cName := vName;
  cImageIndex := vIIndex;
end;

end.
 
2.5.2	Beispiel: Erzeugen des Baumes
 
// Tree will be created when the form is created.
procedure TMyForm.FormCreate(Sender: TObject);
var
   Node: PVirtualNode;
   NodeD: ^rTreeData;
begin
  ....
  // create tree
  MyTree.NodeDataSize := SizeOf(rTreeData);
  if MainControlForm.filename = '' then 
  begin
    // create tree with top level node
    Node := MyTree.AddChild(nil); // adds a node to the root of the tree
    // assign data for this node
    NodeD := MyTree.GetNodeData(Node);
    NodeD.BasicND := TBasicNodeData.Create('new project');
  end
  else 
  begin
    // load tree
  end;
  ...
end;

{______________________________MyTree_________________________________}

// returns the text (the identification) of the node
procedure TMyForm.MyTreeGetText(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Column: Integer; TextType: TVSTTextType;
  var Text: WideString);
var
  NodeD: ^rTreeData;
begin
  NodeD := Sender.GetNodeData(Node);

  // return the identifier of the node
  if NodeD.BasicND = nil then 
    Text:=''
  else 
    Text := NodeD.BasicND.Name;
end;

// returns the index for image display
procedure TMyForm.MyTreeGetImageIndex(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Kind: TVTImageKind; Column: Integer; var Index: Integer);
var
   NodeD: ^rTreeData;
begin
  NodeD := Sender.GetNodeData(Node);

  case Kind of
    ikState: // for the case the state index has been requested
      Index := -1;
    ikNormal, ikSelected: // normal icon case
      Index := NodeD.BasicND.ImageIndex;
  end;

end;
 
2.6	Icons für selektierte Knoten
 
Wenn ein Knoten selektiert ist, soll ein anderes Symbol angezeigt werden: 

Dazu implementiere ich eine neue Methode 

  function GetImageIndex(focus: Boolean): Integer; virtual;

die in Abhängigkeit davon ob der Knoten den Fokus hat oder nicht, den normalen Bildindex oder den für den fokussierten Knoten zurückliefert.

Aufruf:
   Index := NodeD.BasicND.GetImageIndex(Node = Sender.FocusedNode);

Implementierung der Methode:

function TBasicNodeData.GetImageIndex(focus: Boolean): Integer;
begin
  if focus then 
    Result := cImageIndexFocus
  else 
    Result := cImageIndex;
end;

wobei cImageIndex immer den normalen Index hält und cImageIndexFocus den für fokussierte Knoten. Dabei gehe ich davon aus, dass dieser Index immer 1 über cImageIndex liegt.
Damit das auch der Fall ist wird der Konstruktor folgendermaßen geändert:

constructor TBasicNodeData.Create(vName: ShortString; vIIndex: Integer = 0);
begin
  cName := vName;
  cImageIndex := vIIndex;
  cImageIndexFocus := vIIndex + 1;
end;
 
2.7	Knoten hinzufügen und löschen
 
So, damit ich weitere Funktionen implementieren und testen kann, will ich nun endlich die Möglichkeit haben, den Baum aufzubauen. Über ein Kontextmenü soll es möglich sein, Knoten hinzuzufügen und wieder zu entfernen.

Also definiere ich ein Popup-Menü mit 2 Einträgen: [Hinzufügen] und [Entfernen]. Damit bei einem Rechtsklick auch der darunter liegende Knoten den Fokus erhält, muss noch die entsprechende Option toRightClickSelect auf True gesetzt werden.

Wenn also Hinzufügen gewählt wird, wird für den Knoten mit dem Fokus ein Kind-Knoten erzeugt:

procedure TMyForm.addClick (Sender: TObject);
var
  Node: PVirtualNode;
  NodeD: ^rTreeData;
begin
  // Ok, a node must be added.
  Node := MyTree.AddChild(MyTree.FocusedNode); // adds a node as the last child     
  // determine data of node
  NodeD := MyTree.GetNodeData(Node);
  NodeD.BasicND := TBasicNodeData.Create('Child');
end;

ACHTUNG: Was tun, wenn noch kein Knoten den Fokus hat?
 z.B. den neuen Knoten als Kind eines Top-Level-Knotens einfügen.

  if BookmarkTree.FocusedNode = nil then 
  begin
    // insert as child of the first top level node
    Node := BookmarkTree.AddChild(BookmarkTree.RootNode.FirstChild);
    // determine data for node
    NodeD := BookmarkTree.GetNodeData(Node);
    NodeD.BasicND := TFolderNodeData.Create('new folder');
  end
  else 
  begin
    // Ok, a new node must be added.
    Node := BookmarkTree.AddChild(BookmarkTree.FocusedNode);
    // determine data of the node
    NodeD := BookmarkTree.GetNodeData(Node);
    NodeD.BasicND := TFolderNodeData.Create('new folder');
  end;

Wenn der Knoten mit dem Fokus gelöscht werden soll geschieht folgendes:

procedure TMyForm.delClick (Sender: TObject);
begin
  // The focused node should be removed. The top level must not be deleted however.
  if MyTree.FocusedNode = nil then
    MessageDlg('There was no node selected.', mtInformation, [mbOk], 0)
  else
    // Note: RootNode is the internal (hidden) root node and parent of all top 
    // level nodes. To determine whether a node is a top level node you also use
    // GetNodeLevel which returns 0 for top level nodes.
  if MyTree.FocusedNode.Parent = MyTree.RootNode then
    MessageDlg('The project node must not be deleted.', mtInformation, [mbOk], 0)
  else 
    MyTree.DeleteNode(MyTree.FocusedNode);
end;

Ich will jedoch verhindern, das der Top-Level-Knoten gelöscht werden kann. Daher überprüfe ich mit dem Vergleich MyTree.FocusedNode.Parent = MyTree.RootNode, ob der Knoten mit dem Fokus nicht auch der Top-Level-Knoten ist. Hier muss man jedoch beachten, dass die Eigenschaft RootNode den (versteckten) internen Rootknoten zurückgibt, welcher der gemeinsame Vaterknoten aller Top-Level Knoten ist.

Da wir schon beim Löschen von Knoten sind: 
Alle Daten im Record werden automatisch freigegeben, sobald das erforderlich ist. In diesem Fall reicht es uns jedoch nicht, wenn der Speicher, den der Pointer auf die Klasse (Objektinstanz) belegt, freigegeben wird, sondern es ist auch erforderlich den Speicherbereich, den die Objektinstanz selbst benötigt freizugeben. Dies geschieht durch Aufruf des Destruktors beim OnFreeNode Ereignis:

procedure TMyForm.MyTreeFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
begin
  // Free here the node data (Note: type PtreeData = ^rTreeData).
  PTreeData(Sender.GetNodeData(Node)).BasicND.Free;
end;
 
2.8	Ordner und Blätter hinzufügen
 
Jetzt bin ich soweit, um auf der einen Seite Ordner dem Baum hinzuzufügen und auf der anderen Seite auch Endknoten, die keine weiteren Kinder haben dürfen. Dafür werde ich nun 2 neue Knotenklassen von der Basisklasse ableiten.

TFolderNodeData = class(TBasicNodeData)
TItemNodeData = class(TBasicNodeData)

Je nachdem, welche Art von Knoten der Benutzer über das Kontextmenü  jetzt erzeugen will, speichere ich im Record eine entsprechende Instanz der Klasse.

NodeD.BasicND := TFolderNodeData.Create('new folder');
NodeD.BasicND := TItemNodeData.Create('new node');
   
Die Klassen erhalten eine neue Eigenschaft ChildrenAllowed. Anhand dieser Eigenschaft kann nun unterschieden werden, ob der Knoten mit dem Fokus Kindknoten aufnehmen darf (Ordner) oder nicht (Items).
 
2.9	Baum speichern
 
Jetzt kann ich endlich das Speichern des Baums implementieren. Über diesen Schritt habe ich mir schon zu Beginn viele Gedanken gemacht. Mal sehen ob das was gefruchtet hat.

Noch mal ein Teil aus Vorbereitung:
Ich will einen Knoten speichern, okay. Ich gebe den Stream weiter an die Methode MeineKnotenklasse.SaveToFile und diese Methode schreibt, je nachdem um welche Knotenklasse es sich tatsächlich handelt, automatisch als ersten Wert 0, 1 oder 2 als eine Art Klassen ID in den Stream (alternative bietet sich auch eine Enumerationstyp an).

Beim Laden lese ich zuerst den Wert 0, 1 oder 2 aus dem Stream aus und entscheide darauf hin, um welche Klasse es sich handelt. Dann erzeuge ich eine Instanz dieser Klasse und rufe ihre Methode LoadFromFile auf.

Hinweis:
Es wäre auch möglich, anstatt einer ID für die Klasse gleich den Klassennamen zu speichern. Beim Lesen und Erzeugen der Instanzen könnte man die Klassenreferenz und virtuelle Konstruktoren nutzen und so die Case-Anweisung einsparen, wie ich sie im Ereignis OnLoadNode verwende, um zu entscheiden, von welcher Klasse eine Instanz erzeugt werden soll. (Beispiel siehe Delphi 5, von Elmar Warken, Addison-Wesley Verlag - Kapitel 4.3.3, Seite 439)

Bevor etwas gelesen werden kann, muss es geschrieben werden. Daher werde ich zuerst die notwendigen Prozeduren zum Speichern des Baums implementieren. Da wir uns selbst darum kümmern, dass die Bezeichnung des Knotens gespeichert wird, werde ich die StringOptions auf toSaveCaption = False setzen. So werden die Daten nicht doppelt gespeichert.

Zum Speichern des Baumes wird die Prozedur

procedure TBaseVirtualTree.SaveToFile(const FileName: TFileName);

aufgerufen. Dadurch wird die Struktur des Baums automatisch gespeichert. Um zusätzlich unsere Daten mit abzuspeichern existiert das Ereignis OnSaveNode, in dessen Behandlung wir unsere Daten einfach mitspeichern können.

property OnSaveNode: TVTSaveNodeEvent read FOnSaveNode write FOnSaveNode;

Wird OnSaveNode ausgelöst, wird die Methode SaveNode der entsprechenden Knotendatenklasse aufgerufen:

procedure TMyForm.MyTreeSaveNode(Sender: TBaseVirtualTree; Node: PVirtualNode;
  Stream: TStream);
begin
  PTreeData(Sender.GetNodeData(Node)).BasicND.SaveToFile(Stream);
end;

In der SaveNode Methode der Klasse werden die Felder wie Knotenname, Image Index usw. im Stream abgelegt:

procedure TBasicNodeData.SaveNode(Stream: TStream);
var 
  size: Integer;
begin
  // save type of the node
  Stream.Write(Art, SizeOf(Art));

  // store cName
  Size := Length(cName) + 1; // include terminating #0
  Stream.Write(Size, SizeOf(Size)); // store length of the string
  Stream.Write(PChar(cName)^, Size); // now the string itself

  // store cImageIndex
  Stream.Write(cImageIndex, SizeOf(cImageIndex));

  // store cImageIndexFocus
  Stream.Write(cImageIndexFocus, SizeOf(cImageIndexFocus));

  // store cChildrenAllowed 
  Stream.Write(cChildrenAllowed, SizeOf(cChildrenAllowed));
end;

Jetzt können wir den Baum, den wir gespeichert haben, auch wieder laden. Dieser Vorgang könnte etwa so aussehen:

try 
  // load tree
  MyTree.LoadFromFile(MainControlForm.Filename);
except
  on E: Exception do 
  begin
    Application.MessageBox(PChar(E.Message), PChar('Error while loading.'), MB_OK);
    MainControlForm.Filename := '';

    // create tree with top level node (since loading failed)
    Node := MyTree.AddChild(nil);
    NodeD := MyTree.GetNodeData(Node);
    NodeD.BasicND := TBasicNodeData.Create('new project');
  end;
end;
     
Durch den Aufruf von LoadFromFile wird das Ereignis OnLoadNode ausgelöst und somit wird die Methode LoadNode aufgerufen:
   
procedure TBasicNodeData.LoadNode(Stream: TStream);
var 
  Size: Integer;
  StrBuffer: PChar;
begin
  // load cName
  Stream.Read(Size, SizeOf(Size)); // length of the string

  StrBuffer := AllocMem(Size); // get temporary memory
  Stream.Read(StrBuffer^, Size); // read the string
  cName := StrBuffer; 
  FreeMem(StrBuffer);
  // Alternatively you can simply use: 
  // SetLength(cName, Size);
  // Stream.Read(PChar(cName)^, Size);

  // load cImageIndex 
  Stream.Read(cImageIndex, SizeOf(cImageIndex));

  // load cImageIndexFocus
  Stream.Read(cImageIndexFocus, SizeOf(cImageIndexFocus));

  // load cChildrenAllowed
  Stream.Read(cChildrenAllowed, SizeOf(cChildrenAllowed));
end;
  
2.10	Zwei Spalten in der Baumansicht
 
Ich möchte jetzt 2 Spalten im Treeview anzeigen. Dafür lege ich zunächst im Objektinspektor die neuen Eigenschaften des Baums fest.

Über Header.Columns können durch Doppelklick die gewünschten Spalten erzeugt werden. Danach muss nur mehr Header.Options.hoVisible = True gesetzt werden und die Spalten erscheinen im Treeview.

Nachdem alle sonst gewünschten Einstellungen vorgenommen worden sind, kann jetzt der Text bzw. das Icon für die jeweilige Spalte angegeben werden. Das geschieht in den schon vorhandenen Ereignissen OnGetText und OnGetImageIndex, wo nun auch noch auf den übergebenen Spaltenindex Rücksicht genommen werden muss.

procedure TMyForm.MyTreeGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; 
  Column: Integer; TextType: TVSTTextType; var Text: WideString);
var
  NodeD: ^rTreeData;
begin
  NodeD := Sender.GetNodeData(Node);

  // return the the identifier of the node
  if NodeD.BasicND = nil then 
    Text := ''
  else 
  begin
    case Column of
      -1,
      0: // main column, -1 if columns are hidden, 0 if they are shown
        Text := NodeD.BasicND.Name;
      1: 
        Text := 'This text appears in column 2.';
    end;
  end;
end;

procedure TMyForm.MyTreeGetImageIndex(Sender: TBaseVirtualTree; Node: PVirtualNode;
  Kind: TVTImageKind; Column: Integer; var Index: Integer);
var
  NodeD: ^rTreeData;
begin
  NodeD := Sender.GetNodeData(Node);

  if Column = 0 then // icons only in the first column
    case Kind of
      ikState:
        Index := -1;
      ikNormal, ikSelected: 
        Index := NodeD.BasicND.GetImageIndex(Node = Sender.FocusedNode);
      ikOverlay: // e.g. to mark a node whose content changed, 
                 // Note: don’t forget to call ImageList.Overlay for the image.
        if NodeD.BasicND.ImageIndex = 4 then 
          Index := 6;
    end;
end;
 
2.11	Zugriff auf die Spalten
 
Den Zugriff auf die Spalten eines TVirtualStringTrees möchte ich anhand eines Beispiels demonstrieren. Um wie im Punkt 2.12 globale Einstellungen im Tree zu speichern will ich die Weite einer Spalte erfahren. Diese Information aktualisiere ich immer, wenn es ein OnColumnResize Ereignis gibt:

procedure TBookmarkForm.BookmarkTreeColumnResize(Sender: TBaseVirtualTree;
  Column: Integer);
var
  NodeD: PTreeData;
begin
  NodeD := Sender.GetNodeData(Sender.RootNode.FirstChild);

  // Keep the new size of the column in the project node.
  TProjectNodeData(NodeD.BasicND).SetHColumnsWidth(
    TVirtualStringTree(Sender).Header.Columns.Items[Column].Width,Column);
end;

Der spannende Teil ist die Typumwandlung des Sender-Objekts. Im TBaseVirtualTree ist die Header-Property noch protected und erst durch die Umwandlung in TVirtualStringTree kann auf sie zugegriffen werden.

2.12	Globale Baum-Einstellungen
 
Globale Einstellungen, wie z.B. die Größen der Spalten, die im Projekt eingestellt waren, werden als Eigenschaften des einzigen Top-Level-Knotens gespeichert. Er enthält somit alle projektbezogenen Einstellungen.

Damit nicht alle abgeleiteten Klassen diese Felder erben wird der Top-Level-Knoten jetzt aus einer neuen Projektknotenklasse gebildet, die von der Basisknotenklasse abgeleitet wird.

Die neue Hierarchie sieht nun so aus:
»	Basis-Knotenklasse...	vereint Eigenschaften aller Knoten
	»	Projekt-Knotenklasse...	bereichert die Basis vor allem um die Verwaltung projektbezogener Einstellungen
	»	Ordner-Knotenklasse...	bereichert die Basis um Default-Eigenschaften für alle Blattknoten
		»	Blatt-Knotenklasse...	die eigentliche Knotenklasse (spezielle Eigenschaften)

Da es sich hier schon um sehr anwendungsspezifische Programmdetails handelt, will ich nur kurz einige Anmerkungen dazu machen.

Die Basis-Knotenklasse besitzt bereits die Fähigkeit Knotendaten zu speichern. Diese Methoden müssen als virtual deklariert werden. Genau diese Methoden werden dann durch die Deklaration override in der Projekt-Knotenklasse überschrieben, um das Speichern der Projektdaten zu realisieren.

So, jetzt bin ich soweit, um mit dem VirtualTreeview arbeiten zu können. Interessant wird es später noch einmal, wenn ich versuche Daten aus anderen Anwendungen in den Tree zu ziehen. Aber das ist eine andere Geschichte...