TreeView befüllen?

Für allgemeine Fragen zur Programmierung, welche nicht! direkt mit Lazarus zu tun haben.
Antworten
Patrix2911
Beiträge: 32
Registriert: So 30. Jul 2017, 13:53

TreeView befüllen?

Beitrag von Patrix2911 »

Hallo.

Das Thema TreeView war für mich schon immer ein Buch mit sieben Siegeln, Dokumentationen darüber eine Art heilige Schrift, derem Verständnis ich scheinbar nicht würdig bin. :D

Ich hoffe hier kann mir jemand helfen, Ich habe ein Memo, mit folgendem Inhalt:

SOLs 11380 l royal blue
SOLs 11380 l apple green
SOLs 11380 l deep black
SOLs 11380 m royal blue
SOLs 11380 m apple green
SOLs 11380 m deep black

Den Inhalt würde ich gern in ein TreeView bekommen, so das es am Ende so ausschaut:

SOLs
+-11380
+-l
+- royal blue
+- apple green
+- deep black
+-m
+- royal blue
+- apple green
+- deep black

Kann mir dabei jemand helfen?

Vorab vielen Dank für eure Hilfe, beste Grüße, Patrick.

Socke
Lazarusforum e. V.
Beiträge: 3158
Registriert: Di 22. Jul 2008, 19:27
OS, Lazarus, FPC: Lazarus: SVN; FPC: svn; Win 10/Linux/Raspbian/openSUSE
CPU-Target: 32bit x86 armhf
Wohnort: Köln
Kontaktdaten:

Re: TreeView befüllen?

Beitrag von Socke »

Woran liegt's denn? Im Wiki gibts zu TTreeView ein kleines Code-Beispiel, das alles Notwendige enthält.
MfG Socke
Ein Gedicht braucht keinen Reim//Ich pack’ hier trotzdem einen rein

Patrix2911
Beiträge: 32
Registriert: So 30. Jul 2017, 13:53

Re: TreeView befüllen?

Beitrag von Patrix2911 »

Hallo Socke,

so ziemlich an allem. Ich hab mir das Wiki dazu schon angesehen und das ganze auch mal auf ne Form gepackt... aber irgendwie komme ich da nicht dahinter, aus dem Grund hab ich wirklich mein leben Lang nen Bogen um TreeView's gemacht.

Wie schaffe ich es denn nun z.b. das nicht 6 ein Knoten "11380" erstellt wird, oder unter diesem dann 2 mal der Knoten "m" ?

wp_xyz
Beiträge: 4869
Registriert: Fr 8. Apr 2011, 09:01

Re: TreeView befüllen?

Beitrag von wp_xyz »

Patrix2911 hat geschrieben:derem Verständnis ich scheinbar nicht würdig bin. :D

Dann wird's Zeit, dem Verständinis auf die Sprünge zu helfen.

In deinem konkreten Fall würde ich das Memo Zeile für Zeile durchlaufen und jeden String in die einzelnen Wörter aufspalten, wobei die letzten beiden später wieder zusammengefasst werden müssen:

Code: Alles auswählen

var
  arr: TStringArray;
...
  arr := Memo1.Lines[index].Split;

Bei der fest vorgegebenen Struktur des Baums brauchst du dann 4 Variablen für Nodes: node1 für SOLs, node2 für 11380, node3 für l, m etc, node4 für "royal blue" etc.

Beim Durchlaufen der Strings erzeugst du den 1.Node mit dem Element arr[0] und Parent nil:

Code: Alles auswählen

  node1 := TreeView1.Items.AddChild(nil, arr[0]);

Die "11380" ist ein Kind von "SOLs", analog ist "l" ein Kind von "11380", usw. Also

Code: Alles auswählen

  node2 := TreeView1.Items.AddChild(node1, arr[1]);
  node3 := TreeView1.Items.AddChild(node2, arr[2]);
  node4 := TreeView1.Items.Addchild(node3, arr[3] + ' ' + arr[4]);

Das stimmt aber nicht ganz, weil nun für jede Memo-Zeile ein "SOLs" Node erzeugt wird. Daher musst du, bevor du die neuen Nodes erzeugst, suchen, ob es diese vielleicht schon gibt. Dazu prüft die folgende Funktion einen Node, ob sein "Text" dem Suchtext entspricht, also "SOLs" oder "11380" oder "m" oder "l" usw. Wenn das so ist, dann wird die Funktion verlassen, und der Node als Funktionswert übergeben. Wenn nicht, wird die Funktion mit dem ersten Kindknoten und den nächsten Geschwisterknoten aufgerufen. Dadurch wird effektiv der gesamte bereits angelegte Baum durchforstet. Erst wenn ganz am Ende nichts gefunden worden ist, wird das Funktionsergebnis nil.

Code: Alles auswählen

// Suche den Node im gesamten Tree, der den angegebenen Text als Caption hat
function TForm1.FindNode(ACaption: String; ANode: TTreeNode): TTreeNode;
var
  sibling, child: TTreeNode;
begin
  if ANode = nil then begin
    if TreeView1.Items.Count = 0 then begin
      Result := nil;
      exit;
    end else
      ANode := TreeView1.Items[0];
  end;
 
  // Prüfe den Node selbst
  if ANode.Text = ACaption then begin
    Result := ANode;
    exit;
  end;
 
  // Prüfe alle "Kinder" von ANode
  child := ANode.GetFirstChild;
  if child <> nil then begin
    Result := FindNode(ACaption, child)// child prüft seine eigenen Kinder und Geschwister und immer so weiter
    if Result <> nil then exit;
  end;
 
  // Prüfe Geschwister
  sibling := ANode.GetNextSibling;
  if sibling <> nil then begin
    Result := FindNode(ACaption, sibling)// sibling prüft seine eigenen Kinder und die folgenden Geschwister
    if Result <> nil then
      exit;
  end;
 
  // Nichts gefunden
  Result := nil;
end;

Um die bereits existierenden Nodes zu berücksichtigen, musst du also "FindNode" aufrufen. Das Ergebnis nimmst du als Parent für den nächsten Teilstring des Memos, bzw. falls der Node nicht gefunden wurde, musst du ihn wie oben erläutert neu erzeugen:

Code: Alles auswählen

procedure TForm1.MemoToTree;
var
  i: Integer;
  arr: TStringArray;
  node1, node2, node3, node4: TTreeNode;
begin
  for i:=0 to Memo1.Lines.Count-1 do begin
    arr := String(Memo1.Lines[i]).Split(' ');
 
    node1 := FindNode(arr[0], nil);
    if node1 = nil then
      node1 := TreeView1.Items.AddChild(nil, arr[0]);
 
    node2 := FindNode(arr[1], node1);
    if node2 = nil then
      node2 := TreeView1.items.AddChild(node1, arr[1]);
 
    node3 := FindNode(arr[2], node2);
    if node3 = nil then
      node3 := TreeView1.Items.AddChild(node2, arr[2]);
 
    node4 := FindNode(arr[3] + ' ' + arr[4], node3);
    if node4 = nil then
      node4 := TreeView1.Items.AddChild(node3, arr[3] + ' ' + arr[4]);
  end;
  TreeView1.FullExpand;
end;

Es gibt sicher noch andere Herangehensmöglichkeiten. Zwangsläufig wirst du immer auf eine rekursive Funktion, also eine Funktion, die sich immer wieder mit geänderten Parametern selbst aufruft. Bei rekursiven Funktionen habe ich immer das Gefühl, jemand würde mein Gehirn umdrehen... Aber diese Dinger sind sehr elegant und gerade bei Bäumen unerlässlich.

Achtzig
Beiträge: 90
Registriert: Mo 15. Okt 2007, 13:09
OS, Lazarus, FPC: Debian
CPU-Target: xxBit

Re: TreeView befüllen?

Beitrag von Achtzig »

Um einen Knoten mit einem bestimmten Text zu suchen, stellt TTreeView.Items bereits die Funktion FindNodeWithText bereit. Die Funktion geht auch einfacher zu Werke als die obige TForm1.FindNode, da die Knoten nicht verschachtelt gespeichert werden:

Code: Alles auswählen

function TTreeNodes.FindNodeWithText(const NodeText: string): TTreeNode;
begin
  Result := GetFirstNode;
  while Assigned(Result) and (Result.Text <> NodeText) do
    Result := Result.GetNext;
end;
 

wp_xyz
Beiträge: 4869
Registriert: Fr 8. Apr 2011, 09:01

Re: TreeView befüllen?

Beitrag von wp_xyz »

Achtzig hat geschrieben:Um einen Knoten mit einem bestimmten Text zu suchen, stellt TTreeView.Items bereits die Funktion FindNodeWithText bereit. Die Funktion geht auch einfacher zu Werke als die obige TForm1.FindNode, da die Knoten nicht verschachtelt gespeichert werden:

Code: Alles auswählen

function TTreeNodes.FindNodeWithText(const NodeText: string): TTreeNode;
begin
  Result := GetFirstNode;
  while Assigned(Result) and (Result.Text <> NodeText) do
    Result := Result.GetNext;
end;
 

Eben deswegen funktioniert sie auch nicht. Es wird immer, vom allersten Knoten ausgehend, solange gesucht, bis ein Knoten mit dem passenden Text gefunden wurde. Das ist falsch, denn derselbe Text kann ja in mehreren Teilbäumen vorkommen. Nimm diese Daten (angelehnt an die nach Vorgaben des OP):

SOLs 11380 l royal blue
SOLs 11381 l royal blue

Nach Bearbeiten der 1.Zeile gibt es einen Knoten "SOLs", einen Unterknoten "11380", davon einen Unterknoten "l", und dort hängt "royal blue". Wenn die 2.Zeile bearbeitet wird, wird in den Knoten "SOLs" ein zweiter Unterknoten "11381" angelegt. Wenn nun der Text "l" bearbeitet wird, sucht meine Funktion im bestehenden Teilzweig "SOLs"-"11381" weiter, findet nichts und hängt den neuen "l" Knoten bei "11381" ein. Das "FindNodeWithText" dagegen sucht stur von Anfang an, findet den Knoten "l" unter "SOLs" - "11380" und hängt den neuen "l" Knoten dort ein. Das heißt, der Baum wird völlig falsch aufgebaut.

Und so nebenbei würde die Funktion "FindNodeWithText" den OP davon abhalten, rekursiv zu denken. Ich hatte einmal einen englischen Kollegen, der sagte manchmal: "An apple a day keeps the doctor away". Ich würde das umformulieren in "A recursive function a day keeps Parkinson away".

P.S.
Es gibt auch eine Method FindNode von TTreeNode:

Code: Alles auswählen

function TTreeNode.FindNode(const NodeText: string): TTreeNode;
begin
  Result:=GetFirstChild;
  while (Result<>nil) and (Result.Text<>NodeText) do
    Result:=Result.GetNextSibling;
end;

Diese durchsucht aber nur die direkten Kinder des Ausgangsknotens, nicht aber die Kindeskinder etc - dazu müsste sie sich rekursiv selbst aufrufen.

Benutzeravatar
theo
Beiträge: 10468
Registriert: Mo 11. Sep 2006, 19:01

Re: TreeView befüllen?

Beitrag von theo »

wp_xyz hat geschrieben:Ich hatte einmal einen englischen Kollegen, der sagte manchmal: "An apple a day keeps the doctor away".


Ein schottischer Kollege sagt: "A whisky a day keeps the apple away". :mrgreen:

Patrix2911
Beiträge: 32
Registriert: So 30. Jul 2017, 13:53

Re: TreeView befüllen?

Beitrag von Patrix2911 »

Sorry für meine späte Antwort, ich kam leider nicht eher dazu. Also die Variante von wp_xyz reicht für meine Zwecke vollkommen aus, an der Stelle vielen Dank für deine Mühe und Hilfe!!!

Patrix2911
Beiträge: 32
Registriert: So 30. Jul 2017, 13:53

Re: TreeView befüllen?

Beitrag von Patrix2911 »

Kaum schrieb ich dass diese Variante vollkommen genügt für mich, kam mir eine Idee das ganze noch zu Erweitern. Kann man denn den einzelnen Einträgen noch ein Hint zuordnen?

Also z.B.

SOLs 11380 m apple green
SOLs 11380 m deep black

wird im TreeView angezeigt, dann soll "apple green" einen Dateinamen als HINT haben ... und "deep black" soll natürlich einen anderen Dateinamen als Hint haben ... nun scheint es aber so das TTreeNode keine Hint Eigenschaft hat ... gibt es dafür eine andere Lösung?

wp_xyz
Beiträge: 4869
Registriert: Fr 8. Apr 2011, 09:01

Re: TreeView befüllen?

Beitrag von wp_xyz »

Das bedeutet einiges an Bastelei. Zunächst mal muss man dem Node beibringen, überhaupt den Pfad zu speichern. Das geht mit einer Zusatzklasse, z.B. TNodeData, die man ins Feld Data des TTreeNode einhängt, indem man die Add*-Aufrufe durch Add*Object ersetzt und dort die TNodeData-Instanz als zusätzlichen Parameter angibt:

Code: Alles auswählen

type
  TNodeData = class
    Pfad: String;
  end;
 
procedure TForm1.MemoToTree;
var
  data: TNodeData;
 ...
begin
  ...
    node1 := FindNode(arr[0], nil);
    if node1 = nil then begin
      data := TNodeData.Create;
      data.Pfad := PfadFuerNode(arr[0]);   // Diese Funktion musst du selbst zur Verfügung stellen.
      node1 := TreeView1.Items.AddChildObject(nil, arr[0], data);   // statt AddChild
    end
  ...

Die in Node.Data angelegten Instanzen von TNodeData muss man natürlich wieder aufräumen. Dafür ist das OnDeletion-Ereignis geeignet:

Code: Alles auswählen

procedure TForm1.TreeView1Deletion(Sender: TObject; Node: TTreeNode);
begin
  TNodeData(Node.Data).Free;
end;

Und schließlich muss man berücksichtigen, dass TTreeView keine Möglichkeit eingebaut hat, für jeden Node einen eigenen Hint anzuzeigen - das muss man nachrüsten, indem man einen Handler für OnShowHint schreibt:

Code: Alles auswählen

procedure TForm1.TreeView1ShowHint(Sender: TObject; HintInfo: PHintInfo);
var
  P: TPoint;
  node: TTreeNode;
begin
  P := TreeView1.ScreenToClient(Mouse.CursorPos);
  node := TreeView1.GetNodeAt(P.X, P.Y);
  if node <> nil then
    HintInfo^.HintStr := 'Pfad: ' + TNodeData(node.Data).Pfad;
end;

Wenn ich jetzt nichts vergessen habe, müsste es funktionieren.

Patrix2911
Beiträge: 32
Registriert: So 30. Jul 2017, 13:53

Re: TreeView befüllen?

Beitrag von Patrix2911 »

wp_xyz Vielen Dank! Ich werd das nachher gleich mal ausprobieren. :)

Antworten