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.- Wstawiamy na formę komponent VirtualStringTree. RootNodeCount ustawiamy na 10
- Deklarujemy rekord - odpowiedzialny za przechowywanie danych związanych z wierszami listy:
type
PWirtualnyRekord = ^TWirtualnyRekord;
TWirtualnyRekord = record
Caption : WideString;
Size : Int64;
end;
- W OnCreate formy przyporządkowujemy komponentowi nasz rekord:
VirtualStringTree1.NodeDataSize := SizeOf(TWirtualnyRekord);
- 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;
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 ;]