Virtual Treeview

Marcin Baszczewski

Virtual Treeview Wstęp

Virtual Treeview jest komponentem rozpowszechnianym na dwóch licencjach:

*Moziall Public License 1.1 (MPL 1.1)
*GNU Lesser General Public License
Posiada doskonałe możliwości i z czystym sumieniem możemy nim zastąpić inne kontrolki zapewniając sobie dużą elastyczność, oraz estetykę aplikacji

Instalator możemy pobrać <a href=http://www.delphi-gems.com/supplement/download.php?ID=28>tutaj</a>
W jego skład wchodzi komponent oraz przykładowe programy demonstrujące jego wykorzystanie.
Mimo wszystko analiza zamieszczonych źródeł dla niektórych użytkowników może się okazać nieco kłopotliwa, zatem postanowiłem wyjść z pomocną ręką pisząc ten artykuł.

<image src="http://www.delphi-gems.com/images/anivt.gif">
Virtual Treeview Gallery

Virtual Treeview Część1 (Rozbudowany listbox)

![Demo1.JPG](//static.4programmers.net/uploads/attachment/Demo1.JPG) Zacznijmy od prostego wykorzystania komponentu. Napiszemy efektywniejszy odpowiednik listboxa.
  1. Wstawiamy na formę komponent VirtualStringTree. RootNodeCount ustawiamy na 10
  2. Deklarujemy rekord - odpowiedzialny za przechowywanie danych związanych z wierszami listy:
type
  PWirtualnyRekord = ^TWirtualnyRekord;
  TWirtualnyRekord = record
    Caption : WideString;
    Size    : Int64;
end;
  1. W OnCreate formy przyporządkowujemy komponentowi nasz rekord:
VirtualStringTree1.NodeDataSize := SizeOf(TWirtualnyRekord);
  1. Treść procedury OnGetText komponentu VirtualStringTree:
var
  Data: PWirtualnyRekord; //Zauważ że odwołujemy się do rekordu przez nas napisanego.
begin
  Data := Sender.GetNodeData(Node);
  if Length(Data.Caption) = 0 then
  Data.Caption := 'Wiersz ' + IntToStr(Sender.AbsoluteIndex(Node)+1);
  CellText := Data.Caption;
end; 

Edycja wierszy

var
  Wskaznik : PVirtualNode;
  Data     : PWirtualnyRekord;
begin
  VirtualStringTree1.BeginUpdate;
  Wskaznik := VirtualStringTree1.GetFirst;//Zaznaczamy pierwszy element
  Data     := VirtualStringTree1.GetNodeData(Wskaznik);
  Data.Caption:='Nowa wartość';
  VirtualStringTree1.EndUpdate;
end;

Mile wskazane w tym miejscu będą wyjaśnienia.
Jak się zapewne domyślasz w konkretnym przypadku edytujemy pierwszy element.
Odwołanie się do innych nie jest niczym skomplikowanym a to za sprawą funkcji:

VirtualStringTree.GetNext(PVirtualNode);
VirtualStringTree.GetLast();
VirtualStringTree.GetPrevious(PVirtualNode)

Przykład edycji przedostatniego wiersza

var
  Wskaznik : PVirtualNode;
  Data     : PWirtualnyRekord;
begin
  VirtualStringTree1.BeginUpdate;
  Wskaznik := VirtualStringTree1.GetLast;
  Wskaznik := VirtualStringTree1.GetPrevious(Wskaznik);
  Data     := VirtualStringTree1.GetNodeData(Wskaznik);  Data.Caption:='Nowa wartość';
  VirtualStringTree1.EndUpdate;
end;

Dodanie nowego elementu z wybraną wartością

var
  Wskaznik : PVirtualNode;
  Data     : PWirtualnyRekord;
begin
  VirtualStringTree1.BeginUpdate;
  VirtualStringTree1.RootNodeCount:=VirtualStringTree1.RootNodeCount+1;
  Wskaznik := VirtualStringTree1.GetLast;
  Data     := VirtualStringTree1.GetNodeData(Wskaznik);
  Data.Caption:='Nowa wartość';
  VirtualStringTree1.EndUpdate;
end;

Usuwanie wierszy
Możemy odwołać się do zaznaczonych: VirtualStringTree1.DeleteSelectedNodes;
Co jest o tyle pomocne o ile umożliwimy komponentowi zaznaczanie wielu linii.
Standardowo jednakże posłużymy się procedurą DeleteNode której to argumentem jest wcześniej przez nas zaprezentowany wskaźnik (PVirtualNode).

Usunięcie pierwszego wiersza

VirtualStringTree1.DeleteNode(VirtualStringTree1.GetFirst);

Usunięcie zaznaczonego

VirtualStringTree1.DeleteNode(VirtualStringTree1.GetFirstSelected);

Numer zaznaczonego wiersza

ShowMessage(IntToStr(VirtualStringTree1.GetFirstSelected.Index));

Ilości zaznaczonych elementów

ShowMessage(IntToStr(VirtualStringTree1.SelectedCount));

Treść zaznaczonego

var
  Data: PWirtualnyRekord;
begin
  Data := VirtualStringTree1.GetNodeData(VirtualStringTree1.GetFirstSelected);
  ShowMessage(Data.Caption);
end;

VirtualStringTree - rozbudowa

Najwyższa pora by przejść do bardziej subtelnych możliwości komponentu, które zapewnią Cię, że dotychczasowa lektura nie była czasem straconym. Rozważymy wyświetlanie podpowiedzi (hintów).

Posłużymy się przykładowym programem który już utworzyliśmy.
Niezbędna będzie dla nas zmienna przechowująca treść podpowiedzi oraz kolor konkretnego wiersza.
Nanieśmy zatem poprawki w rekordzie TWirtualnyRekord.

type
  PWirtualnyRekord = ^TWirtualnyRekord;
  TWirtualnyRekord = record
    Caption : WideString;
    Hint    : WideString;
    Color   : TColor;
    Size    : Int64;
end;

Pora na wstępne ustawienia naszego komponentu:
ShowHint (True)
HintMode (hmHint)
TreeOptions->MiscOptions->toShowRoot (false)
TreeOptions->MiscOptions->toShowTreeLines (false)
TreeOptions->SelectionOptions->toFullRowSelect (True)
TreeOptions->PaintOptions->toUseBlendedSelection (True)
TreeOptions->PaintOptions->toHideFocusRect (True)
SelectionBlendFactor (128)

Zajmijmy się naszymi Hintami. W tym celu uzupełnijmy zdarzenie OnGetHint:

var
  Data : PWirtualnyRekord;
begin
  Data     := Sender.GetNodeData(Node);
  HintText := Data.Hint;
end;

W chwili obecnej możemy przyporządkowywać konkretnym hintom odpowiednie podpowiedzi. Powinieneś się domyśleć jak tego dokonać? Ale by tradycji stało się zadość?

var
  Data: PWirtualnyRekord;
begin
  Data := VirtualStringTree1.GetNodeData(VirtualStringTree1.GetFirstSelected);
  Data.Hint:='hint dla zaznaczonego elementu';
end;

Edytując nasz rekord dodaliśmy właściwość kolor.
Najwyższa pora przyjrzeć się jej bliżej.
Oprogramowanie zdarzenia OnBeforeCellPaint:

var
  Data : PWirtualnyRekord;
begin
  Data     := Sender.GetNodeData(Node);
  if Data.Color=TColor(nil) then //Nadanie domyslnej wartosci
  Data.Color:=clWindow;
  TargetCanvas.Brush.Color := Data.Color;
  TargetCanvas.FillRect(CellRect);
end;

Zmiana tła zaznaczonego elementu

var
  Data: PWirtualnyRekord;
begin
  VirtualStringTree1.BeginUpdate;
  Data := VirtualStringTree1.GetNodeData(VirtualStringTree1.GetFirstSelected);
  Data.Color:=clRed;
  VirtualStringTree1.EndUpdate;
end;

Estetyka

Ze względu na sporą ingerencję w ustawienie postanowiłem zamieścić fragment pliku *.dfm który to bezbłędnie prezentuje naniesione przeze mnie modyfikacje. Spokojnie możesz zmodyfikować u siebie to archiwum (*.dfm) kopiując i wklejając zamieszczone poniżej dane bądź je przeanalizować i manualnie ponanosić.
object VirtualStringTree1: TVirtualStringTree
    Left = 0
    Top = 0
    Width = 304
    Height = 185
    Align = alClient
    BevelInner = bvLowered
    BevelOuter = bvNone
    CheckImageKind = ckDarkTick
    ClipboardFormats.Strings = (
      'CSV'
      'HTML Format'
      'Plain text'
      'Rich Text Format'
      'Rich Text Format Without Objects'
      'Unicode text'
      'Virtual Tree Data')
    Colors.BorderColor = clBackground
    Colors.FocusedSelectionColor = clInactiveCaptionText
    Colors.FocusedSelectionBorderColor = 10526880
    Colors.HotColor = clBlack
    Colors.UnfocusedSelectionBorderColor = clBtnShadow
    DefaultNodeHeight = 24
    EditDelay = 100
    Header.AutoSizeIndex = -1
    Header.Background = clBtnShadow
    Header.Font.Charset = ANSI_CHARSET
    Header.Font.Color = clWindowText
    Header.Font.Height = -12
    Header.Font.Name = 'Arial'
    Header.Font.Style = [fsBold]
    Header.Height = 25
    Header.MainColumn = -1
    Header.Options = [hoColumnResize, hoDblClickResize, hoHotTrack, hoOwnerDraw, hoShowHint, hoShowImages, hoShowSortGlyphs]
    Header.Style = hsPlates
    HotCursor = crHandPoint
    Indent = 2
    LineMode = lmBands
    LineStyle = lsSolid
    Margin = 0
    ParentShowHint = False
    RootNodeCount = 5
    SelectionCurveRadius = 5
    ShowHint = True
    TabOrder = 0
    TreeOptions.AnimationOptions = [toAnimatedToggle]
    TreeOptions.AutoOptions = [toAutoDropExpand, toAutoScroll, toAutoScrollOnExpand, toAutoTristateTracking, toAutoDeleteMovedNodes]
    TreeOptions.MiscOptions = [toCheckSupport, toFullRepaintOnResize, toGridExtensions, toInitOnSave, toWheelPanning]
    TreeOptions.PaintOptions = [toHideFocusRect, toShowVertGridLines, toThemeAware, toUseBlendedImages]
    TreeOptions.SelectionOptions = [toDisableDrawSelection, toFullRowSelect]
    TreeOptions.StringOptions = [toSaveCaptions]
    OnBeforeCellPaint = VirtualStringTree1BeforeCellPaint
    OnGetText = VirtualStringTree1GetText
    OnGetHint = VirtualStringTree1GetHint
    Columns = <>
  End

Najwyższa pora na kolorystykę. Tą naniesiemy jednak z poziomu kodu.
Do tego celu będziemy potrzebowali funkcje odpowiedzialną za mieszanie kolorów.

Do procedury OnCreate dodaj:

VirtualStringTree1.Colors.FocusedSelectionColor:= MieszanieKolorow(clHighlight,clWindow,50,50);
VirtualStringTree1.Colors.FocusedSelectionBorderColor:= MieszanieKolorow(clHighlight,clWindow,80,20);
VirtualStringTree1.Colors.UnfocusedSelectionColor:= MieszanieKolorow(clHighlight,clWindow,60,40);
VirtualStringTree1.Colors.UnfocusedSelectionBorderColor:=MieszanieKolorow(clHighlight,clWindow,80,20);

Virtual Treeview Część2 (Drzewko)

<center>![Demo2.JPG](//static.4programmers.net/uploads/attachment/3963/1125)</center> Po bardzo długiej rozgrzewce najwyższa pora zająć się tworzeniem drzewek za pomocą tego komponentu. Nie jest to trudne ? a całość sprowadzi się do paru modyfikacji? Co prawda możemy zrezygnować z wizualnych zmian które niedawno dokonaliśmy ale nie jest to obowiązkowe jeśli przywrócimy domyślną wartość zmiennym: toShowRoot, toShowTreeLines. Ja mimo wszystko zacznę pracę z pozbawionym zmian komponentem co nie oznacza że zrezygnuje z naszego doczesnego dorobku. Zarówno rekord (PwirtualnyRekord) jak i oprogramowane zdarzenia będą nam bardzo potrzebne.

Zacznijmy od najprostszego przypadku:
Dodania elementów potomnych dla zaznaczonego wiersza

var
  Data     : PWirtualnyRekord;
  Wskaznik : PVirtualNode;
begin
  Wskaznik:=VirtualStringTree1.GetFirstSelected;
  Data := VirtualStringTree1.GetNodeData(Wskaznik);
  VirtualStringTree1.ChildCount[Wskaznik]:=VirtualStringTree1.ChildCount[Wskaznik]+1;
  Data.Caption:='Parent';
  Wskaznik := VirtualStringTree1.GetLastChild(Wskaznik);
  Data := VirtualStringTree1.GetNodeData(Wskaznik);
  Data.Caption:='Childer';
end;

Dotychczas wędrowaliśmy pomiędzy wierszami za pomocą instrukcji GetNext, GetLast?
Wraz z pojawieniem się elementów podrzędnych warto zapoznać się z dodatkowymi funkcjami: NextSibling, PrevSibling.
Znajdą zastosowanie wszędzie tam gdzie chcemy się odwołać do równoległego kolejnego elementu nie koniecznie zagłębiając się w ?korzenie? jak by to miało miejscy przy wykorzystaniu funkcji GetNext.

var
  Data     : PWirtualnyRekord;
  Wskaznik : PVirtualNode;
begin
  VirtualStringTree1.BeginUpdate;
  Wskaznik := VirtualStringTree1.GetFirst.NextSibling;
  Data := VirtualStringTree1.GetNodeData(Wskaznik);
  Data.Caption:='To ja jestem następny?';
  VirtualStringTree1.EndUpdate;
end;

Jesteśmy już w stanie zaprezentować dane w tego typu strukturze.
Warto jednak pójść o krok do przodu przyporządkowując checbkox-a elementom naszej konstrukcji. Zapewni nam to użyteczne narzędzie które poszerzy gamę zastosowań kontrolki.

TreeOptions->MiscOptions->toCheckSupport (true)
2)
Oprogramowanie zdarzenia OnInitNode:

var
  Poziom: Integer;
begin
  Poziom := Sender.GetNodeLevel(Node); 
  if Poziom < 1 then
    Include(InitialStates, ivsExpanded);
  if Poziom > 0 then
    Node.CheckType := ctCheckBox //Może być to równie dobrze RadioButton (ctRadioButton)
  else
    Node.CheckType := ctTriStateCheckBox;
end;

Jak pobrać pierwszy zaznaczony (dzióbkiem) element?

var
  Data     : PWirtualnyRekord;
  Wskaznik : PVirtualNode;
begin
  Wskaznik:=VirtualStringTree1.GetFirstChecked(csCheckedNormal);
  Data := VirtualStringTree1.GetNodeData(Wskaznik);
  ShowMessage(Data.Caption);
end;

Jak pobrać listę zaznaczonych (dzióbkiem) elementów?

var
  Data     : PWirtualnyRekord;
  Wskaznik : PVirtualNode;
  Temp     : TStringList;
begin
  Temp:=TStringList.Create();
  Wskaznik:=VirtualStringTree1.GetFirstChecked(csCheckedNormal);
  while Assigned(Wskaznik) do
  begin
    Data := VirtualStringTree1.GetNodeData(Wskaznik);
    Temp.Add(Data.Caption);
    Wskaznik := VirtualStringTree1.GetNextChecked(Wskaznik,csCheckedNormal);
  end;
  ShowMessage(Temp.Text);
  Temp.Free;
end;

Kolumny

Podstawy mamy za sobą. Tymczasem chciałem zaprezentować sposób wdrażania dodatkowych kolumn.
Te będą współistnieć wraz z powstałym drzewkiem.

Dodaj dwie kolumny (Header->Columns), ustaw ich szerokość oraz wartość Text.
2)
Header->Options->hoVisible (True)
3)
Zapewnienie niezależnych wartości drugiej kolumnie:

a)

type
  PWirtualnyRekord = ^TWirtualnyRekord;
  TWirtualnyRekord = record
    Caption1: WideString;
    Caption2: WideString; //Dodaliśmy zmienną przechowującą tekst w drugiej kolumnie
    Hint    : WideString;
    Color   : TColor;
    Size    : Int64;
end;

b)
Modyfikacja zdarzenia OnGetText:

var
  Data: PWirtualnyRekord;
begin
  Data := Sender.GetNodeData(Node);
  if Length(Data.Caption1) = 0 then
  begin
    Data.Caption1 := 'Wiersz ' + IntToStr(Sender.AbsoluteIndex(Node)+1);
    Data.Caption2 := Data.Caption1;
  end;
  if Column=0 then
  CellText := Data.Caption1 else
  CellText := Data.Caption2;
end;

Virtual Treeview Część3 (Proste tabele)

![Demo3.JPG](//static.4programmers.net/uploads/attachment/3963/1126) By zapewnić sobie szybszy start posłużymy się tym co zdołaliśmy napisać w pierwszej części artykułu. Wszelkie modyfikacje zamieszczam poniżej:
type
  PWirtualnyRekord = ^TWirtualnyRekord;
  TWirtualnyRekord = record
    Caption : array [0..3] of WideString;
    Color   : TColor;
    Size    : Int64;
end;

Zdarzenie OnGetText

var
  Data: PWirtualnyRekord;
begin
  Data := Sender.GetNodeData(Node);
  if Length(Data.Caption[0]) = 0 then
  begin
    Data.Caption[0] := 'Wiersz ' + IntToStr(Sender.AbsoluteIndex(Node)+1);
    Data.Caption[1] := Data.Caption[0];
    Data.Caption[2] := Data.Caption[0];
    Data.Caption[3] := IntToStr(Random(100))+'%';
  end;
  CellText := Data.Caption[Column];
end;

Zdarzenie OnColumnClick

begin
  VirtualStringTree1.Header.MainColumn:=Column;
end;

Headrer->Columns (dodaj 4 kolumny)
Dla 4 kolumny: Aligment (taCenter)
Headrer->Options->hoVisible (true)
TreeOptions->PaintOptions->toShowHorzGridLines (true)
TreeOptions->toFullRowSelect (false)

Pasek postępu w komórce
OnBeforeCellPaint udostępnia nam bezpośredni dostęp do płótna komórki.
Wykorzystamy ten fakt do wstawiania słupków postępu.

Kod zdarzenia:

var
  Data       : PWirtualnyRekord;
  Temp, I    : Integer;
  Temp2      : String;
  Temp3      : TRect;
  R,G,B:Byte;
begin
  Data     := Sender.GetNodeData(Node);
  if Data.Color=TColor(nil) then
  Data.Color:=clWindow;
  TargetCanvas.Brush.Color := Data.Color;
  TargetCanvas.FillRect(CellRect);
  if Pos('%', Data.Caption[Column]) > 0 then
  begin
    Temp3:=VirtualStringTree1.Header.Columns[Column].GetRect;
    TargetCanvas.Pen.Color:=clBlack;
    TargetCanvas.Brush.Color:=clHighlight;
    Temp2:=Data.Caption[Column];
    Delete(Temp2, Pos('%', Temp2), 1);
    Temp:=Round((Temp3.Right-Temp3.Left-4)*StrToInt(Temp2)/100);
    if Temp<4 then
    Temp:=4;
    TargetCanvas.Rectangle(Temp3.Left,Temp3.Top,Temp3.Left+Temp,Temp3.Bottom-1);
    Temp:=Temp-1;
    for I:=0 to (Temp3.Bottom-Temp3.Top-4) do
    begin
      R:=255-Round(i*i/2);
      G:=255-Round(i*i/2);
      B:=255-Round(i*i/2);
      TargetCanvas.Pen.Color:= MieszanieKolorow(RGB(R, G, B),clHighlight,40,60);
      TargetCanvas.MoveTo(Temp3.Left+1,Temp3.Top+1+I);
      TargetCanvas.LineTo(Temp3.Left+Temp,Temp3.Top+1+I);
    end;
  end;
end;

5 komentarzy

UWAGA: w tym przykladzie (artykul) tez trzeba 'zwolnic' rekord, a raczej 'posprzatac' go. Zawiera on WideString, ktory od-tak sam sie nie zwolni!
Nie bedzie widac mem-leaka w np. FastMM, bo WideStringi sa alokowane nie przez Delphi/FastMM, a przez system (SysAllocStringLen/SysReAllocStringLen/SysFreeString). Aby zobaczyc memleak'i w FastMM, wystarczy do tego rekordu TWirtualnyRekord dodac jakies pole typu String/AnsiString i wpisac do niego jakiegos dynamicznie wygenerowanego stringa (nie moze to byc: lRec.MyStr := 'aaa', tylko np: lRec.MyStr := TimeToStr(Now); ), wtedy na pewno bedzie widoczny leak :)

procedure TfrmMain.VTFreeNode(Sender: TBaseVirtualTree;
Node: PVirtualNode);
var
lRec: PWirtualnyRekord;
begin
lRec := Sender.GetNodeData(Node);
Finalize(lRec^); // Wszystkie dynamiczne typy zostana zwolnione/zmienjszony licznik referencji (AnsiString/String/UnicodeString, dyn. tablice, interfejsy a takze WideStringi). Obietky nalezy zwolnic 'recznie'. Jezeli rekord zawiera kolejne rekordy, to one tez zostana 'posprzatane'
end;

Ten rekord TWirtualnyRekord jest 'doklejany' na koniec rekordu TVirtualNode. Pamiec przydzielona na TWirtualnyRekord jest zwalniana przy zwalnianu TVirtualNode, ale juz obiekty/klasy lub dynamiczne typy trzeba zwolic samemu. VT nic nie 'wie' o 'doklejanej' strukturze :)

PS. Odnosnie samego rekordu TWirtualnyRekord, obecnie duzo lepiej jest uzywac typu UnicodeString, zamiast WideString. w Delphi < 2009 bedzie to tylko alias na WideString, a w Delphi >= 2009 bedzie to juz typ String=Unicodestring ---> oszczednosc pamieci + predkosc dzialania (tylko w przypadku D2009+)

Dlaczego przy kolorowaniu Node w ViirtualTree za pomoca np OnBeforeCellPaint lub innych procedur, obciazenie CPU wskakuje na 100% ( jesli uzywamy 2 roznych kolorów, np co drugi wiersz inny kolor czcionki)

@entek
Noda? Ciekawa nazwa czegoś, co po polsku nazywa się po prostu węzłem. A co do VirtualStringTree - ciężko mi sobie wyobrazić kontrolkę, której nie byłby w stanie zastąpić.

Autor mógłby napisać o zwalnianiu pamięci i sprzątaniu po "Virtual Treeview ", a szczególnie gdy zamiast rekordu tworzymy jakąs klasę "TWirtualnaKlasa".

Tydzień się męczyłem zanim znalazłem, iż tworząc nową nodę należy dodać:

Node.States := Node.States +[vsInitialUserData];

Inaczej, przy kończeniu programu nie zostanie wywołana funkcja:

procedure TForm1.mytreeFreeNode(Sender: TBaseVirtualTree;
Node: PVirtualNode);
begin
//zwalnianie obszaru który zjmujemy przy tworzeniu nody
end;

Pozdrawiam.

Oooo coś jak w tych programach p2p, że kilka źródeł do jednego pliku można pokazać, i takie tam ;]