צפייה בעץ חלק שני

עבר הרבה זמן מאז שיצרתי את החלק הראשון, והגיע הזמן להשלים חוסרים.

Program using VirtualTreeView

תוכנה בשם precisionhelper המשתמשת ברכיב

יש רכיב צד שלישי הנקרא VirtualTreeView אשר נכתב במקור לדלפי ומגיע כקוד פתוח (LGPL).
הרכיב לא ניתן לעבודה בברירת מחדל עם לזרוס), והוא עובר מספר שינויים על מנת לקבל תמיכה בלזרוס, כאשר השינויים נמצאים ב Lazarus-CCR.

הרכיב מאפשר ליצור שליטה מלאה בעבודה אשר בברירת מחדל זהה לרכיב העץ (עליו דיברתי בחלק הקודם), אבל מספקת המון יכולות ותכונות אשר לא קיימות ברכיב המקורי, כדוגמת היכולת לייצג מידע אמיתי בצורה של עץ, שינוי תפיסה וויזואלית של הדברים, עבודה בסגנון Grid, אנימציה בתצוגת המידע, כולל תמיכה בהאצת חומרה, ציור של מידע בצורה מותאמת אישית, תוכנן במקור לתמוך בדו כיווניות (עברית וערבית) וזה אפילו לא מכסה את פסגת קצה הקרחון.

העבודה איתו אבל, היא כמעט כולה בקוד, ולא בצורה וויזואלית (למעט כיוונונים שונים שניתן לבצע), דבר האומר כי צריך להבין מה רוצים להשיג עם הרכיב בעצם, אחרת לא ניתן לתכנת איתו כלום. כך שהתחלת עבודה למי שאינו מכיר את הרכיב, יכולה להיות מאוד כואבת.

בשביל ליצור למשל עץ בעל 3 עמודות, וכותרת לעמודות, נגדיר לצורך העניין רשומה כזו (שימו לב כי בגרסה המקורית עובדים עם WideString, אבל בגרסת לזרוס, עובדים עם String – אם תשתמשו ב WideString הוא לא יציג את הטקסט שלכם):

type
  PTreeData = ^TTreeData;
  TTreeData = record
    Column1,
    Column2,
    Column3 : String;
  end;

הגדרתי גם מצביע לרשומה, אשר הוא יהיה בשימוש העיקרי שלנו, כפי שתראו בהמשך.
עכשיו נזרוק לטופס שלנו פקד TPanel ונעשה אותו עם align לalBottom
עליו נזרוק 3 כפתורים:
הראשון יקבל כותרת של Add Root. אתן לו את השם btnAddRoot
השני יקבל את הכותרת Add Child. אתן לו את השם btnAddChild
והשלישי יקבל את הכותרת Delete. אתן לו את השם btnDelete
עכשיו נזרוק על הטופס (לא על TPanel) את TVirtualTreeView שהתקנו (לפי הקישור למעלה ששמתי), ונגדיר אותו עם Align ל alClient. הוא קיבל אצלי את השם VST (קיצור לVirtual String Tree ).

הטופס יקבל הגדרה בעת היצירה שלו (אירוע OnCreateForm) כח VST יכיל כותרות, עם כותרות שנרצה:

procedure TfrmMain.FormCreate ( Sender : TObject ) ;
var
  Column : TVirtualTreeColumn;
  i : integer;
begin
  Randomize;
  VST.Header.Options := VST.Header.Options +[hoVisible];
  VST.Header.Style := hsFlatButtons;
  for i := 1 to 3 do
   begin
    Column := VST.Header.Columns.Add;
    Column.Text := 'column #' + IntToStr(i);
    Column.Options := Column.Options + [coAllowClick, coResizable];
    Column.Width := UTF8Length(Column.Text) + 100;
   end;

  VST.TreeOptions.MiscOptions      := VST.TreeOptions.MiscOptions +
                                      [toEditable,toGridExtensions];
  VST.TreeOptions.SelectionOptions := VST.TreeOptions.SelectionOptions +
                                      [toExtendedFocus, toMultiSelect];

end;

עכשיו הזמן לעבוד עם מספר אירועים של VST, אשר חשובים עבור ניהול זיכרון המידע שלנו:
נלך דבר ראשון לאירוע של OnChange, ונגיד לו לבצע Refresh בכל שינוי שיתבצע:

procedure TfrmMain.VSTChange ( Sender : TBaseVirtualTree; Node : PVirtualNode ) ;
begin
  VST.Refresh;
end;

באירוע של שינוי פוקוס גם שם נגדיר לעץ לעשות עדכון:

procedure TfrmMain.VSTFocusChanged ( Sender : TBaseVirtualTree;
    Node : PVirtualNode; Column : TColumnIndex ) ;
begin
  VST.Refresh;
end;

חשוב להבין, כי במידה ו2 קריאות ה callbacks האלו, במידה והיו זהות במבנה שלהן, היה ניתן לאחד לאחת, ולהגיד לעץ לקרוא לcallback אחד עבור 2 האירועים האלו.
אירוע מאוד חשוב, הוא אירוע שיתרחש כאשר העץ ירצה לשחרר אלמנט בתוכו והוא נקרא OnFreeNode:

procedure TfrmMain.VSTFreeNode ( Sender : TBaseVirtualTree; Node : PVirtualNode) ;
var
  Data : PTreeData;
begin
  Data := VST.GetNodeData(Node);
  if Assigned(Data) then
   begin
    Data^.Column1 := '';
    Data^.Column2 := '';
    Data^.Column3 := '';
  end;
end;

אנחנו בעצם מסבירים לו כיצד לשחרר את הזיכרון, ובכך מונעים דליפת זיכרון. הסיבה לכך היא שאנחנו בעצם יצורים את סוג המידע, ולכן העץ אינו יודע לשחרר לבד את התוכן (למעט המצביע עצמו).

נלך לאירוע של GetNodeDataSize. תפקיד האירוע הוא לדעת כיצד לבצע הקצאת זיכרון למידע:

procedure TfrmMain.VSTGetNodeDataSize ( Sender : TBaseVirtualTree;
                                        var NodeDataSize : Integer ) ;
begin
  NodeDataSize := SizeOf(TTreeData);
end;

אירוע נוסף שמומלץ להשתמש בו הוא OnGetText:

procedure TfrmMain.VSTGetText ( Sender : TBaseVirtualTree; Node : PVirtualNode;
  Column : TColumnIndex; TextType : TVSTTextType; var CellText : String ) ;
var
  Data : PTreeData;
begin
  Data := VST.GetNodeData(Node);
  case Column of
    0 : CellText := Data^.Column1;
    1 : CellText := Data^.Column2;
    2 : CellText := Data^.Column3;
  end;
end;

כל איבר ואיבר בעץ, כאשר צריך להיות מוצג, צריך לדעת בעצם מה להציג. ככה אפשר לשנות את המראה בהתאם להרבה דברים. במקרה הזה, מציגים תמיד את אותו המידע.

האירוע האחרון של העץ שנשתמש בו, הוא OnNewText אשר בפעם הרשונה יציג טקסט מסויים לכל איבר בעץ:

procedure TfrmMain.VSTNewText ( Sender : TBaseVirtualTree; Node : PVirtualNode;
                                Column : TColumnIndex; NewText : String ) ;
var
  Data : PTreeData;
begin
  Data := VST.GetNodeData(Node);
  case Column of
    0 : Data^.Column1 := NewText;
    1 : Data^.Column2 := NewText;
    2 : Data^.Column3 := NewText;
  end;
end;

עכשיו לחלק האינטרקטיבי של המשתמש, נממש את הכפתורים שיצרנו.
ובכן צריך להבין מושגים קצת לפני כן. Root זהו בעצם איבר "שורש" כלשהו בעץ. שאין לו הורים, הוא עומד בפני עצמו, עם ילדים, ואולי גם יש לו אחים. בעוד ש Child הוא בן של איבר. הוא יכול להיות בן של איבר "שורש" או איבר אחר.

המימוש עבור הוספת איבר שורש הוא כזה (ההסבר יגיע מתחת לקוד):

procedure TfrmMain.btnAddRootClick ( Sender : TObject ) ;
Var
  Data : PTreeData;
  XNode : PVirtualNode;
  Rand : Integer;
begin
  Rand := Random(99);
  XNode := VST.AddChild(nil);

  if VST.AbsoluteIndex(XNode) > -1 then
  Begin
   Data := VST.GetNodeData(XNode);
   Data^.Column1 := 'One ' + IntToStr(Rand);
   Data^.Column2 := 'Two ' + IntToStr(Rand + 10);
   Data^.Column3 := 'Three ' + IntToStr(Rand - 5);
  End;

end;

אנחנו מבקשים מהעץ ליצור לנו "ילד", שהוא בעצם root.
עכשיו אנחנו בודקים כי האיבר אכן נוצר ברשימה ובמידה וכן (המתודה בעצם שואלת מה האינדקס של האיבר, ובמידה והוא לא נוצר, הערך שיחזור יהיה מינוס אחד). לאחר מכן, נחנו מבקשים את המצביע של המידע עבורו. במקרה הזה, מקבל ייצוג על ידי הרשומה שיצרנו בהתחלה. ואנחנו מזינים את המידע לתוך מצביע של הרשומה.
אנחנו מזינים את המצביע במידע לפי כל שדה, והיות וזה מצביע גלובלי (כלומר שייך לפקד), אין צורך להגיד פקודה של "שמור" את המידע.

תיאורטית, הכפתור של btnAddChild צריך להראות מאוד דומה (זוכרים כי root הוא האיבר הראשון, לא מעבר), אבל מימשתי אותו מעט אחרת:

procedure TfrmMain.btnAddChildClick ( Sender : TObject ) ;
var
  XNode : PVirtualNode;
  Data : PTreeData;

begin
  if not Assigned(VST.FocusedNode) then
    exit;

  XNode := VST.AddChild(VST.FocusedNode);
  Data := VST.GetNodeData(Xnode);

  Data^.Column1 := 'Ch 1';
  Data^.Column2 := 'Ch 2';
  Data^.Column3 := 'Ch 3';

 VST.Expanded[VST.FocusedNode] := True;
end;

בקוד הזה, אני בודק דבר ראשון, האם בכלל נעשתה בחירה של איבר. היות ואנחנו צריכים ליצור בן לאיבר, ולא ליצור איבר "ראשי".
אנחנו הפעם מוסיפים בן לעץ עבור האיבר אשר נבחר כרגע, ומקבלים את האיבר חזרה. וכמו מקודם גם כאן יוצרים מידע. ובסוף אומרים לעץ "להרחיב" את הענף שנוצר, כלומר שהוא יהיה גלוי ולא סגור.

הכפתור האחרון הוא כפתור המחיקה:

procedure TfrmMain.btnDeleteClick ( Sender : TObject ) ;
begin
  VST.DeleteSelectedNodes;
end;

הכפתור מוחק את האיבר שכרגע מסומן, כאשר במידה ויש לו ילדים, הם ימחקו גם הם עבורינו.

ככה נראה התוצר הסופי:  virtualtreeview_exampleאת קוד המקור ניתן למצוא כאן.

כתיבת תגובה

הזינו את פרטיכם בטופס, או לחצו על אחד מהאייקונים כדי להשתמש בחשבון קיים:

הלוגו של WordPress.com

אתה מגיב באמצעות חשבון WordPress.com שלך. לצאת מהמערכת / לשנות )

תמונת Twitter

אתה מגיב באמצעות חשבון Twitter שלך. לצאת מהמערכת / לשנות )

תמונת Facebook

אתה מגיב באמצעות חשבון Facebook שלך. לצאת מהמערכת / לשנות )

תמונת גוגל פלוס

אתה מגיב באמצעות חשבון Google+ שלך. לצאת מהמערכת / לשנות )

מתחבר ל-%s