Entwickler-Ecke

Datenbanken - DBTreeView ?!? bzw. Treeview speichern


cyberax - Sa 10.02.07 19:31
Titel: DBTreeView ?!? bzw. Treeview speichern
Hallo,

benutze mysql und Zeos Komponenten.
ich habe eine Artikel tabelle. Nun sollen die Artikel verschiedenen Kategorien zugeordnet werden. Dafür ist sicher ein Treeview passend.
Die Artikel sollen dann so dargestellt werden.

+Hauptgruppe 1
-Hauptgruppe 2
+Untergruppe 1
+Untergruppe 2
-Untergruppe 3
- Artikel 1
- Artikel X
....

Es sollen beliebig viele Gruppen und Untergruppen bzw. Einträge definiert werden können.
Ich dachte mir das so, wie es in vielen Online - Shops der Fall ist.
Wie kann ich nun am besten hinterlegen/speichern wo welcher Artikel liegt, bzw. wie der Baum aussieht?
Bei dem Treeview kann man ja einfach den Inhalt speichern, aber ich möchte nicht den dargestellten Text speichern, sondern etwas eindeutiges - wie die ID des Artikels.

Gibt es für so etwas eine Komponente? Oder wie könnte ich das am einfachsten machen?
Gruß cyberax

Moderiert von user profile iconChristian S.: Titel geändert.


stifflersmom - Sa 10.02.07 20:15

Ich weiß nicht ob es die beste Komponente für Dein Vorhaben ist,
aber was Bäume angeht ist wohl das VirtualTreeView von Mike Lischke
die Komponente überhaupt. Deer Umgang damit ist zwar definitiv aufwendiger
als mit einem "normalen" TreeView, dsfür hast Du allerdings dievesre Möglichkeiten
Deine Anforderungen zu befriedigen.
Beim VirtualTreeView kannst Du (ja mußt Du sogar) jedem Node einen record zuordnen
und der kann natürlich auch eine ID aus Deiner Tabelle aufnehmen.

By The way, hast Du vor, eine Verwaltung für die datenbank vom Oscommerce oder sogar
Xtcommerce zu entwickeln?

Moin


Delete - Sa 10.02.07 21:16

dafür baut man sich normal eine rekursive tabelle auf. dort stehen dann einfach die zuordnungen drin. wenn du möchtest, kannst ja zusätzliche informationen hinterlegen. tabellenaufbau ist so

t{@key; SubKey; ...}

die musst dann nur noch auswerten und deinen tree entsprechend aufbauen. sollte kein problem sein.


HelgeLange - Sa 10.02.07 21:16

man kann bei dem "normalen" TTreeView auch Daten zuordnen, jeder Knoten hat einen Data-pointer ;)

Was Du brauchst, ist nicht das speichern eines Treeviews, sondern eine vernünftige Daten-Definition in deiner Datenbank.

zum bsp:

Table1=Hauptgruppe1
ID : integer; (unique!)
Name : String;
... (was du sonst noch brauchst)

Table2=Untergruppe
ID : Integer; (unique!)
ID_Hauptgruppe : Integer; (Verweis auf die ID der Hauptgruppe)
Name : String;
... (was du sonst noch brauchst)

Table3=Artikel
ID : Integer; (unique!)
ID_Untegruppe: Integer; (Verweis auf die ID der Untergruppe, damit hast du automatisch die Hauptgruppe dazu)
Name : String;
...

somit hast du für alle artikel den Baum.
Im programm erzeugst du für alle Hauptgruppen einen Knoten, dann Lädst du die Untergruppen und legst Kindknoten in den Hauptgruppen an für die untergruppen. Dann gehst zu deinen Artikeln und machst das gleiche dort, allerdings als kinder der untergruppen. Aus der Tabelle weisst Du ja, zu welche Untergruupe welcher Artikel gehört.

Du kannst es natürlich auch so machen, dass Du statt 2 Tabellen für Gruppen nur 1 machst, die noch ein ID feld bekommmt, wo Du auf die Hauptgruppe verweist. Ist es 0, dann ist der Gruppeneintrag eine Hauptgruppe. Ist es grösser als 0, dann eine UNtergruppe und die ID ist der verweis auf die Hauptgruppe.


cyberax - Do 15.02.07 23:21

Also vielen Dank für eure Hilfe!
@stifflersmom - habe das mit der Komponente auf die "Schnelle" nicht hinbekommen. Habe nur die für Lazarus gefunden.. :roll: ..

werde es nun so machen, wie ihr es mir geraten habt. Eine tabelle
"gruppe" mit:
id
gr_id
name

In der Tabelle "artikel" gibt es eine "gr_id" wo der Verweis auf die nächste Gruppe steht. In der nächsten Gruppe steht der Verweis per "gr_id" auf die nächste und so weiter, bis die "gr_id" = 0 ist. Das wäre dann eine Hauptgruppe.

Also vielen Dank. Muss es nun nur noch umsetzen :D


cyberax - Fr 16.02.07 00:45

Zitat:

die musst dann nur noch auswerten und deinen tree entsprechend aufbauen. sollte kein problem sein


...stellt doch ein kleines Problem dar. Habe schon im Forum gesucht aber leider nix gefunden, was mit hilft. Also ich habe nun eine tablle "gruppe" wie beschrieben.
Ich hatte mir das mit dem Füllen des Trees so gedacht, dass ich aus der tabelle erst alle Einträge abfrage, die zur Hauptgruppe gehören ("gr_id"=0) und diese in das Tree schreibe. dann wollte ich die nächsten einlesen und so weiter. Nur habe ich keine Ahnung, wie ich das am besten umsetze bzw scheint mir das recht umständlich.. :roll:

@Grenzgaenger: meintest du, ich solle in die artikel tabelle ein Feld haben in dem der vollständige Pfad steht (zum Beispiel:"Hauptgruppe1/Untergruppe8/Artikelx")?


Delete - Fr 16.02.07 01:00

also das ist 'n rekursiver algo. musst mal bei deiner datenbank schaun, vielleicht unterstützt sie das ja per sql. :-) . bpw. oracle konnte zumindest in version 7 entsprechende auflösungen in sql direkt auflösen. bei mySQL kenn ich mich gar nicht aus. ansonsten musst ihn halt per software nachbilden.

der ist eigentlich recht einfach, musst nur aufpassen, dass sich der algo durch sich selbst definiert. und er macht ja da auch nichts anderes wie sich den knoten herauszuhohlen, sich selbst einzuknüpfen und zu gucken ob er unterknoten hat, wenn ja, fängt das ganze wieder von vorne an... bis in die 'zig'ste ebene.. wichtig dabei ist, dass die tabellendefiniton auch rekursiv aufgebaut ist, wie in meinen ersten post aufgezeigt, sonst brichst du dir die finger bei der implemtation.

wünsch dir noch viel erfolg.

ich meinte, in der tabelle sollten die primärschlüssel stehen. die tabelle selbst braucht nur aus zwei feldern zu bestehen, einen schlüssel und einen schlüssel, der auf den schlüssel verweisst. bspw.

t{@k;@s}

Quelltext
1:
2:
3:
4:
5:
6:
1    3
3    4
3    5
5    6
5    7
7    9


das würde den folgenden baum ergeben

Quelltext
1:
2:
3:
4:
1
 3 4
   5  6
      7 9


UGrohne - Fr 16.02.07 15:17

Ich hatte dasselbe Problem auch schon mal, allerdings mit C++ und Firebird.

In diesem Artikel [http://www.delphi-forum.de/viewtopic.php?p=96763#96763] steht meine damalige Lösung dazu, mit einer Stored Procedure, die die Daten entsprechend aufbereitet und einer Prozedur, die das TreeView füllt. Ich denke, es sollte nicht so schwierig sein, beides in Delphi-code zu übersetzen und noch die Verwendung der Data-Eigenschaft einzubinden.


cyberax - Sa 17.02.07 13:34

Also ich habe nun die tabelle "gruppe" mal wie folgt gefüllt.


Quelltext
1:
2:
3:
4:
5:
6:
id           gr_id       name
1             0          Autos
2             0          Fahrräder
3             1          Verbrenner
4             1          Elektro
5             3          1:8


wollte also im Tree folgendes Bild:


Quelltext
1:
2:
3:
4:
5:
 - Autos
    - Verbrenner
      - 1:8
    - Elektro
 - Fahrräder


In Delphi habe ich folgenden Code (sehr einfach gestrikt):

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
var
  MyTreeNode2: TTreeNode;
begin
 tv_eg1.Items.Clear;
 with ZQuery_GRUPPE do begin
  sql.Text:='SELECT * FROM GRUPPE WHERE GR_ID = 0';
  open;
 end;
 ZQuery_GRUPPE.First;
 repeat
  tv_eg1.Items.AddFirst( nil,ZQuery_GRUPPE.FieldByName('name').Text);
 until ZQuery_GRUPPE.FindNext=false;

 with ZQuery_GRUPPE do begin
  sql.Text:='SELECT * FROM GRUPPE WHERE GR_ID > 0';
  open;
 end;
 ZQuery_GRUPPE.First;
 repeat
  MyTreeNode2 := tv_eg1.Items[strtoint(ZQuery_GRUPPE.FieldByName('gr_id').Text)];
  tv_eg1.Items.AddChild(mytreenode2,ZQuery_GRUPPE.FieldByName('name').Text);
 until ZQuery_GRUPPE.FindNext=false;


Daraus ergibt sich leider folgendes Bild im Tree:


Quelltext
1:
2:
3:
4:
5:
- Fahrräder
- Autos
  - Verbrenner
  - Elektro
    - 1:8

Wieso das? Ich denk mal ich muss im Tree nach dem jeweiligen Node suchen und dann das entsprechende Child ranhängen. Aber wie kann ich nach dem Node suchen ohne dabei den Pfad anzugeben (z.B. "Autos/Verbrenner/1:8")? Gibt es für die Nodes im Tree auch eine "id" nach der ich im Tree suchen kann?


HelgeLange - Sa 17.02.07 15:58

Ich glaube, rein technisch gesehen musst Du nur die Abgfrage nach ID sortieren lassen, denn kein SubNode kann eher erstellt worden sein, als deren Parent.


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
tv_eg1.Items.Clear;
 with ZQuery_GRUPPE do begin
  sql.Text:='SELECT * FROM GRUPPE ORDER BY ID ascending';
  open;
 end;
 While not ZQuery_GRUPPE.EoF do begin
  If ZQuery_GRUPPE.FieldByName('id_gr').AsInteger = 0 Then
    tv_eg1.Items.AddFirst( nil,ZQuery_GRUPPE.FieldByName('name').Text)
  Else begin
    MyGr_ID := ZQuery_GRUPPE.FieldByName('id_gr').AsInteger;
    MyName := ZQuery_GRUPPE.FieldByName('name').Text;
    AddSubNodeMain(MyGr_ID, MyName);
  end;
  ZQuery_GRUPPE.Next;
 end;


Deine Aufgabe wird es nun sein, eine

Delphi-Quelltext
1:
 procedure AddSubNodeMain(MyGr_ID : Integer; MyName: String);                    

zu schreiben, die wieder eine rekursive funktion aufruft, welche immer tiefer in deinen Baum eindringt. Du gibst also dieser funktion einen deiner rootnodes mit (die, die gr_id=0 haben), die funktion testet, ob kinder da sind, wenn ja, schnappt sie sich das erste kind, ruft sich selbst auf mit dem ChildNode, wenn nicht, testet sie auf Siblings. Und bei jedem Node, der gefunden wird, wird geprüft, ob die Gr_ID des nodes die ist, die du suchst. Hast Du den node gefunden, gibst du ihn zurück als funktionsresult. Und wenn du in der funktion siehst, dass da ein Result ist (Result <> nil), dann immer schön die Suche abbrechen, dann kommst du aus der Rekursion raus.
Bist du nach dem ersten Durchlauf noch ohne Ergebnis, nimmst du ihn der procedure "AddSubNodeMain" den nächsten Sibling und schickst ihn wieder an die rekursive funktion.

Oh, und btw... wie du im Code ganz oben gesheen hast habe ich deine "Repeat..Until" gegen eine "While not EoF do.." getauscht. Das liegt daran, dass Repeat-Until fusslastige schleifen sind, While-Do sind aber kopflastige.
Eine Fusslastige schleife wird mind. 1x durchlaufen und wenn du aus irgendeinem Grund keine Daten drin hast, gibt es einen crash.
Bei einer kopflastigen Schleife kann das nicht passieren, da sie nicht unbedingt durchlaufen wird, sondern erst geprüft wird, ob eine gewissen Grundvoraussetzung gegeben ist (in dem Fall, ob überhaupt Daten für die abfrage zurück kamen).

Hoffe, das half dir jetzt ein bisschen, Klarheit reinzubringen in deinen Task. den rekursiven Algo hab ich dir bewusst nicht geschrieben, Du musst das ja lernen, nicht ich ;)

LG aus Caracas.


cyberax - Sa 17.02.07 17:46

Okay vielen Dank!

Muss also selbst überlegen ...?... :wink:
Okay ist richtig. Ich werde mich mal versuchen und das Resultat (auch wenn es unter Umständen zur allg. Belustigung beiträgt) :roll: später schreiben.
Grüße zurück aus Deutschland


cyberax - So 18.02.07 23:35

Bin mit dieser Sache "leicht" überfordert...

Habe nun versucht das ganze mal zu ordnen und denke das die procedure AddSubNodeMain(MyGr_ID : Integer; MyName: String); wie folgt aufgebaut sein muss:

Quelltext
1:
2:
3:
 Node anlegen
 Kindertest durchfü0hren (where gr_id = id des akt. Nodes)
 bei Kindertest = JA ruft sich die Funktion selbst wieder auf, bei Kindertest = NEIN -> RAUS


Sie Sieht so bei mit aus:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
procedure AddSubNodeMain(MyGr_ID : Integer; MyName: String);
var
  MyTreeNode: TTreeNode;
begin
 MyTreeNode := artikel_baum.tv_eg1.Items[mygr_id];
 artikel_baum.tv_eg1.Items.AddChild(MyTreeNode,myname);
 if kindertest(artikel_baum.ZQuery_GRUPPE.FieldByName('id').AsInteger) = 1 then
  AddSubNodeMain(artikel_baum.zquery_search.FieldByName('gr_id').asinteger,artikel_baum.zquery_search.FieldByName('name').text);
end;


die Funktion "Kindertest" so:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
function kindertest(id:integer):real;
var
  MyTreeNode: TTreeNode;
begin
 with artikel_baum.zquery_search do begin
  sql.Text:='SELECT * FROM GRUPPE WHERE ID = :searchid';
  With Params.ParamByName('searchid'do begin
   DataType := ftinteger;
   Value := id;
  End;
  open;
 end;
 if artikel_baum.zquery_search.RecordCount>0 then
  result:=1
 else result:=0;
end;


Funktioniert nicht. (Hätte mich auch gewundert. :D )
Stimmt denn mein Denkansatz wie oben beschrieben mit deinem überein bzw. könnte er so funktionieren ?
Benutze ja in der Kindersuch- Funktion auch ein zweites Query. Ist das nötig?

Zitat:
Und bei jedem Node, der gefunden wird, wird geprüft, ob die Gr_ID des nodes die ist, die du suchst


Ich habe keine Ahnung, wie ich das machen soll??!!!??
Habe mir echt den Kopf zerbrochen und alles auch mehrmals aufgemalt, aber komm hier nicht weiter :cry:


HelgeLange - Mo 19.02.07 01:01

Naja, du hattest nicht ganz richtig gelesen ;)

In der AddSubNodeMain gehst du "nur" zum root-knoten, der schnappt sich das erste child un dsendet es an eine ANDERE funktion, die rekursiv ist und sich selbst aufruft, je tiefer sie eindringt in den Baum.

AddSubNodeMain geht nur durch die Siblings (Geschwister) in der ersten ebene, die als Parent den Rootnode haben, ungefähr so :

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
{*******************************************************************************
Name         : uTreeView_Helper.pas
Coding by    : Helge ange
Date         : 27.02.2004
Last Update  : 27.02.2004
Version      : 1.0.001
Last Changes :

1.0.001-------------------------------------------------------------------------
- initial version
*******************************************************************************}

unit uTreeView_Helper;

interface

uses Windows, DB, ComCtrls, Classes, SysUtils, Controls, GlobalVars, ResClient;

Type RNodeData = packed record
       StringID : Integer;
       HookNr   : Integer;
     end// RNodeData
     PNodeDate = ^RNodeData;

function TreeAddItem(Sender: TTreeView; IL: TImageList; pData : PConfigHookData; RC: TResClient; Resort: Boolean): TTreeNode;
function TreeFindItem(Sender: TTreeView; NodeItem: TTreeNode; Name: String): TTreeNode;

implementation

// In der ItemList sind in jedem String die Verweise auf die Bilder in der
// ImageList gespeichert. Nach der Nummer des Bildes folgt ein Leerzeichen
// und darauf der Text, welcher in der Liste erscheinen soll.
function TreeAddItem(Sender: TTreeView; IL: TImageList; pData: PConfigHookData; RC: TResClient; Resort: Boolean): TTreeNode;
var ThisNode, Node : TTreeNode;
    S : String;
    pND : PNodeDate;
begin
  Node := nil;
  // nil = level 0 Kein übergeordnetes Element = root
  // wird von TreeFindItem getestet
  S := RC.GetString(pData^.ParentName);
  ThisNode := TreeFindItem(Sender, Node, S);
  If ThisNode <> nil Then
    Node := ThisNode // Parent schon vorhanden
  Else begin
    New(pND);
    pND^.StringID := pData^.ParentName;
    pND^.HookNr := 0;
    Node := Sender.Items.AddChildObject(nil, S, pND);
    Node.ImageIndex := pData^.ParentImage;
    Node.SelectedIndex := pData^.ParentImage;
  end;  //  --  If ThisNode <> nil Then

  // jetzt den Eintrag für den Dialog
  New(pND);
  pND^.StringID := pData^.Name;
  pND^.HookNr := pData^.HookNr;
  S := RC.GetString(pData^.Name);
  ThisNode := Sender.Items.AddChildObject(Node, S, pND);
  ThisNode.ImageIndex := pData^.Image;
  ThisNode.SelectedIndex := pData^.Image;
  ThisNode.StateIndex := ThisNode.Level + 1;
  If Resort and (Node.Parent <> nilThen
    Node.Parent.AlphaSort;
  Result := ThisNode;
end// TreeAddItem

function TreeFindItem(Sender: TTreeView; NodeItem: TTreeNode; Name: String): TTreeNode;
begin
  if NodeItem = nil Then NodeItem := Sender.Items.GetFirstNode // this will be rootnode
                    Else NodeItem := NodeItem.GetFirstChild;
  // NodeItem ist das 1. Item des gewählten Levels
  // wenn dieses Level keine Items besitzt, ist NodeItem = nil
  if (NodeItem <> niland (NodeItem.Text <> Name) then
    repeat
      NodeItem := NodeItem.GetNextSibling;
    until (NodeItem = nilor (NodeItem.Text = Name);
  Result := NodeItem;
end// TreeFindItem

end.


Ist ne Unit, die ich mal vor jahren geschrieben habe. In dem Fall ging ich davon aus, dass es immer nur 2 Lebel gibt in dieser Anwendung. Aber es sollte für dich ja kein Problem sein, es dementsprechend zu erweiterm. dass TreeFindItem bei Unterknoten sich entsprechend selbst wieder aufruft. Ist natürlich mehr Code, als Du brauchst, da es ich es für mich für eine ganz spezielle Situation geschrieben habe... Aber es sollte etwas helfen :)


Delete - Mo 19.02.07 02:36

hallo cyberax, wenn du nur das ttreeview verwendest, kannste auch den baum über den index travisieren. das geht ähnlich wie bei 'er stringlist .


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
...
 t: ttreeview;
 i: integer;
...
begin
 for i := 0 to t.items.count - 1 do
  listbox.items.add(t.Items[i].Text); //listbox nur zur erläuterung angegeben
end.



PS: aus der OH: "Der Zugriff auf Einträge im Baumdiagramm über den Index kann sehr zeitaufwendig sein, wenn das Diagramm viele Einträge enthält. Verwenden Sie daher aus Geschwindigkeitsgründen in Ihren Anwendungen möglichst wenige Indexzugriffe auf Elemente."
von daher ist die zugriffsmethode von helge immer noch dieser vorzuziehen. daher dies nur als anregung.

ausserdem solltest du beim hinzufügen die schlüsselinformation mit in die daten des nodes mit aufnehmen, dann kannst jederzeit auf deine datenbank eindeutig referenziern :-) . und nicht vergessen, beim löschen des nodes auch wieder die data strukturen freizugeben, sonst neigt sich dein hauptspeicher recht schnell dem ende zu ...


HelgeLange - Mo 19.02.07 07:09

mir ist grad 'ne Idee gekommen (die für meine Zwecke zu unpraktisch wäre/war, aber eben für Dich ganz gut). Ich habe das letzten Mal für meine Resourcen-Sachen in meinen Resourcen-Manager eingebaut.

Kurze beschreibung des Resourcen-Problems bei mir. Man kann in pro Bibliothek (also EXE oder DLL) Resourcen ID's nur bis MAX_WORD (also 0-65.535) vergeben. Nur will ich allerdings die Möglichkeit haben, alle Resourcen von n-DLL's (die Zahl weiss ich ja jetzt noch nicht und kann im laufe der Jahre anwachsen) in einem Manager zusammenfassen und dass das Modul, welches eine Resource anfordert, nicht wissen muss, woher die Resource kommt. Bei einer Beschränkung von 65k "kann" es mal voll werden. Als Lösung habe ich mir den Basis-Multiplikator ausgedacht. Das funktioniert wie ein Überlauf-Register. Sagen wir, die Haupt-Resourcen-DLL hat die Basis 0, also alle angeforderten Resourcen mit einer ID zw. 0 und 65.535 kommen aus dieser DLL. Ist die Nummer grösser als 65.535, wird eine Modulo-Operation angewandt, wie oft passt eine 65.535 in die ID, bevor das Endergebnis kleiner ist. Damit habe ich dann a) die wirkliche ID der Resource und b) die Basis, wonach ich die entsprechende Resourcen-Bibliothek anwählen kann, um dann die Resource zu holen.

Ok, jetzt wirst du dich fragen, was das ganze mit dir zu tun hat. Sehr viel :)

Ich finde Deine Idee nicht gut (und widerspricht auch allen gängigen designs, die ich kenne), die Artikel mit den Gruppen zusammen in einem Treeview unterzubringen (abhängig natürlich von der Anzahl der Artikel). Ich würde ab mind. 50 Artikel das Ganze eher 3-gegliedert aufbauen : TreeView, ListView, Detail-Ansicht (wahrscheinlich ein Panel mit aufgelegten Formular oder Controls). Aber das ist im Endeffekt Deine Sache :P

Was Du machen kannst, um schnell den richtigen Baum zu finden, ist ungefähr das Gleiche, was ich mit meinem Resourcen mache. Ich habe das Beispiel mit meinen Resourcen deswegen gewählt, weil es verständlicher im Grundprinzip ist. Und mit dem Grundprinzip kannst Du auch Dein Problem einfacher lösen.

Für jede Hauptgruppe vergibst Du einen Basis-Multiplaikator. Sagen wir 1.000.000. Ist eine beliebige gesuchte ID grösser als 1 mio, brauchst Du in das erste Element garnicht reinzuschauen, da es unter 1 mio ist. Damit scheidet ein ganzer Tree beim durchsuchen aus. In der Untergruppe wählst Du dir einen anderen Multiplikator aus, sagen wir 10.000. Also hast Du platz für 100 Untergruppen (sollten eh nicht soviele werden, oder es wird zu unübersichtlich und Du solltest generell mal über das problem der datendarstellung nachdenken). Alle Unter-Untergruppen (also Kinder der kinds-gruppen der Hauptgruppen) kriegen auch einen Basis-Multiplikator, sagen wir 100, das gibt dir 100 IDs Platz für diese Untergruppen und 100 Platz für Artikel (die eh nur bis 50 gehen oder eben getrennt angezeigt werden sollten).

Wie findest Du da nun schnell den passenden Knoten ? Ganz einfach.

Nehmen wir an, die gelesene ID ist 1.059.756
Im Rootknoten findest Du 5 Kinder, deine Hauptgruppen. Sie haben die IDs 1, 2, 3, 4 und 5.
Deine Formel ist (ID - 1) * BasisMultiplikator für die StartID, EndID ist StartID + BasisMultiplikator - 1
Das Erste brauchst Du nicht zu durchsuchen, die ID ist 1, also StartID ist 0, EndID ist 999.999. Und anstelle da jetzt reinzugehen und alle Kinderknoten zu durchsuchen, beschränken wir uns auf den Test, ob die von uns gesuchte ID in diesem Bereich liegt, das ist eh allemal schneller ;)
Im 2. Item ist die ID 2, also hast du den Bereich von 1.000.000-1.999.999. Nun liegt Deine ID in dem Bereich, also gehst Du rein und ziehst von der gesuchten ID den Basis-Multiplikator für Hauptgruppen ab. Übriug bleibt die ID 59.756 und Dein neuer Basis-Multiplikator ist 10.000. Also los un den Knoten suchen, der als basis die 6 hat ((ID - 1) * BasisMultiplikator), also zw. 50.000 und 60.000... dann wieder in die nächste Kinds-Ebene...



Das ist natürlich nur die Idee, und ist auch etwas abgefahrener als meine Idee für meine Resourcen, aber hey, smot kannst Du echt schnell Dein Zeug finden, selbst wenn du tausende Knoten hast (ok, nur, wenn sie nicht alle auf einer Ebene liegen, sondern dem von dir geschildertem Auftreten folgen.. aber es ist ja eine Lösung für dich ;) )

Wenn Du das gern umsetzen willst, dann können wir gemeinsam drüber nachdenken, falls Du problem hast... Oder vllt hat jemand eine solche Lösung parat (bin ja nicht der Einzigste, der abgefahrene Ideen hat !)

So, ich geh jetzt ins Bett, ist spät...


cyberax - Mo 19.02.07 22:44

@Grenzgaenger:
habe deinen Vorschalg probiert, aber hat bei mir das selbe Ergebnis geliefert wie meine Funktion. Sprich die Einträge waren nicht an der richtigen Stelle. Das liegt sicher daran, dass der Index in jedem Knoten immer wieder von 0 beginnt. Daher geht meine Funktion auch so nicht auf und es kommt zu falschen Einordnungen im Tree.


@helgelange:
Also finde die Idee von dir wirklich sehr gut. Doch (wie dir schon bekannt ist :) ) bin ich noch nicht ganz hinter das System gestiegen.

Also lieber erst einmal zu deinen Funktionen...

Frage(n):
Die variablen Resort und IL benötige ich ja (vorerst) nicht um das Tree zu füllen.

Quelltext
1:
 S := RC.GetString(pData^.ParentName);                    

Ich müsste also an dieser Stelle mit einem zweiten Query den Rootnode suchen und den Name (???) des Nodes übergeben, auf den mittels "gr_id"(DB) von dem aktuellen Node (das erstellt werden soll) verwiesen wird ?? Ist dieser Name eindeutig??? "Autos" zum Beispiel kann doch mehrmals in dem Tree vorkommen....?!?
Oder wie erkenne ich sonst den Name des parents?

Und wofür steht das ...

Quelltext
1:
   pND^.HookNr := 0;                    


Ich hoffe das auch ich das hier irgendwann mal verstehe und hoffe bis dahin auf eure weitere Mithilfe!

Danke schonmal für eure Mühen!!
Und gute Nacht...


HelgeLange - Mo 19.02.07 23:51

nah, RC.GetString ist für meinen Resourcen-manager, einfach raus
er liefert den Namen für den Knoten wieder (ist wichtig für Mehrsprachigkeit etc. bei mir).

pND^ zeigt auf meine pNodedata-struktur und HookNr ist eine Variable in der Strucktur, die ich an jedem node brauche. Kann für Dich auch raus.

IL ist die ImageList, brauch ich scheinbar nicht mehr in der Routine, weiss auch nicht, warum es noch da ist ;)


In "TreeFindItem" musst du vom Prinzip nur noch entscheiden, ob ein Eintrag noch weitere Untereinträge hat, oder ob du zum NextSibling gehst.

In TreeAddItem kannst Du alles rauswerfen, was mit Images zu tun hat oder HookNr. Du musst Dir eine Struktur für Deine Nodedata basteln (wenn Dir die ID nicht reichen sollte), ansonsten gibst Du an die Routine eben nur auch Deine Gr_ID mit und vergleichst sie jeweils mit der ID des augenblicklichen Nodes.


Delete - Mi 21.02.07 01:39

hier mal 'n kleines beispiel, schnell runtergeschrieben aber es sollte in etwa zeigen, wie das funktioniert...


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ComCtrls, StdCtrls, contnrs;

type
  TForm1 = class(TForm)
    b: TButton;
    lb: TListBox;
    tv: TTreeView;
    od: TOpenDialog;
    procedure bClick(Sender: TObject);
    procedure tvDeletion(Sender: TObject; Node: TTreeNode);
  private
    { Private-Deklarationen }
  public
    { Public-Deklarationen }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

type
 tft= class (tobject)
  grp, nd: integer;
  bez: string;
 end;

procedure TForm1.bClick(Sender: TObject);
 procedure LoadAndSplit(const sl: tstringlist; const ol: tobjectList);
 var
  i: integer;
  ft: tft;
  s : string;
 begin
  for i := 0 to sl.Count - 1 do
  begin
   ft:= tft.create;
   s := sl.Strings[i];
   if s <> '' then
   begin
    ft.grp:= strtoint(copy(s,1,pos(',',s)-1));
    delete(s,1,pos(',',s));
    ft.nd:=strtoint(copy(s,1,pos(',',s)-1));
    delete(s,1,pos(',',s)); 
    ft.bez := s;
    ol.add(ft);
   end;
  end;
 end;
 procedure AddTV(const ol: tObjectList; nd: ttreenode);
  function AddNode(const ol: tObjectList;nd: tTreeNode): ttreenode;
  var
   dta: tft;
  begin
   dta := tft.Create;
   dta.grp := tft(ol.Items[0]).grp; //gruppenid
   dta.nd  := tft(ol.items[0]).nd;  //node id
   dta.bez := tft(ol.items[0]).bez; //knotenbezeichnung
   result  := tv.Items.AddChildObject(nd, tft(ol.Items[0]).bez, dta)
  end;
 begin
  if ol.Count > 0 then
  begin
   if tft(ol.Items[0]).grp = 0 then nd := NIL;
   if nd = NIL then
    nd:=addnode(ol, nd) //neuen Hauptknoten beginnen
   else 
    if tft(nd.Data).nd  = tft(ol.Items[0]).grp then //hier evtl. anpassungen vornehmen
     nd := addnode(ol, nd) //neuen Subknoten erstellen
    else
     nd := addnode(ol, nd.GetNext); //sonst neu positionieren
   ol.Delete(0);
   AddTV(ol,nd);
  end
 end;
 var
 sl: tstringlist;
 ol: tobjectlist;
 i : integer;
begin
 od.Filter := 'textdatei | *.txt';
 if od.execute and fileexists(od.FileName) then
 begin
  sl := tstringlist.Create;
  try
   sl.loadfromfile(od.FileName);
   ol := tobjectlist.Create;
   try
    LoadAndSplit(sl, ol);
    for i := 0 to ol.Count - 1 do
     lb.Items.Add(inttostr(tft(ol.Items[i]).grp)+', '+inttostr(tft(ol.items[i]).nd)+', '+tft(ol.items[i]).bez);
    AddTV(ol, NIL); 
   finally
    ol.Free;
   end;
  finally
   sl.free;
  end;
 end;
end;

procedure TForm1.tvDeletion(Sender: TObject; Node: TTreeNode);
begin
 if (node <> NILand (node.data <> NILthen
 begin
  tobject(node.data).free;
  node.Data := NIL;
 end;
end;

end.


es wird vorausgesetzt, dass die eingabedatei sortiert ist.

alles andere sollte bereits im thread erwähnt sein. wenn du grossere sprünge hast, musst evtl. bei der IF anweisung in der form AddTV anpassungen vornehmen.

<HTH>


uwewo - Mi 21.02.07 08:58

Hallo,

warum musst Du denn überhaupt alle Daten sofort in das TreeView schreiben?

Ich hatte das mal wie folgt gelöst:

Erstelle zuerst die Haupgruppen mit einem Dummy Eintrag damit das + vor den Hauptgruppen erscheint, jetzt reagierst Du erst beim öffnen einer Hauptgruppe

Delphi-Quelltext
1:
2:
procedure TForm.TreeListExpanding(Sender: TObject;
  Node: TTreeNode; var AllowExpansion: Boolean);


und führst hier die neue SQL Anweisung aus, um die Hauptgruppe mit Untergruppen zu füllen usw. Das spart Zeit und Speicher beim öffnen des Formulars, ausserdem ist es auch übersichtlicher zu programmieren.

Nur eine Idee


cyberax - Mi 21.02.07 19:45

Kurze Zwischenfrage: wie kann ich denn meine Integer Variable (gr_id) einem Treenode zuweisen?

Quelltext
1:
MyTreeNode := artikel_baum.tv_eg1.Items[mygr_id];                    

So funktioniert es jedenfalls nicht, da so das treenode immer nil bzw. "()"...?!? was bewirkt, dass immer nur ROOT Einträge erstellt werden


Delete - Mi 21.02.07 22:36

ganz einfach, wenn du in den source lurkst, den ich dir gab.


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
  function AddNode(const ol: tObjectList;nd: tTreeNode): ttreenode;
  var
   dta: tft;
  begin
   dta := tft.Create;
   dta.grp := tft(ol.Items[0]).grp; //gruppenid
   dta.nd  := tft(ol.items[0]).nd;  //node id
   dta.bez := tft(ol.items[0]).bez; //knotenbezeichnung
   result  := tv.Items.AddChildObject(nd, tft(ol.Items[0]).bez, dta)//hier wird ein neuer node erzeugt und zugewiesen.
  end;


du kannst nicht direkt mit 'ner id 'n neuen node erzeugen, hierfür musst du ein neues objekt erzeugen (läuft normal automatisch, in AddChild). lurk mal dort unter der hilfe, es gibt da 'ne menge an methoden die dafür gedacht sind, 'n node an den tree zu heften.

<HTH>


cyberax - So 25.02.07 22:33

Hallo,

ich glaube ich hab's nun endlich (mit eurer Hilfe) hinbekommen. :D

Hier mal der Code:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
function TForm1.GetFieldList: TStringList;
var gruppen_id:integer;
    akt_id:integer;
    akt_gruppe:integer;
    id_wert:integer;
begin
 FieldList.clear;
 with form1.ZQuery2 do begin
  sql.Text:='SELECT * FROM GRUPPE WHERE ID = :GRUPPE';
  With Params.ParamByName('GRUPPE'do begin
   DataType := ftinteger;
   Value := form1.ZQuery1.fieldbyname('GR_ID').asinteger;
  End;
  open;
 end;
 if ZQuery2.RecordCount>0 then
  if ZQuery2.FieldByName('gr_id').asinteger > 0 then begin //ZUM ROOT VORDRINGEN
   akt_id:=ZQuery2.fieldbyname('id').AsInteger;
   akt_gruppe:=zQuery2.fieldbyname('gr_id').AsInteger;
   repeat
    with form1.ZQuery2 do begin
     sql.Text:='SELECT * FROM GRUPPE WHERE ID = :GRUPPE';
     With Params.ParamByName('GRUPPE'do begin
      DataType := ftinteger;
      Value := akt_gruppe;
     End;
     open;
    end;
    akt_gruppe:=ZQuery2.fieldbyname('GR_ID').AsInteger;
    if akt_gruppe=0 then repeat //ROOT GEFUNDEN NUN WIEDER TIEFER IN BAUM
     id_wert:= ZQuery2.fieldbyname('ID').asinteger;
     FieldList.add(ZQuery2.fieldbyname('NAME').asstring);
     with form1.ZQuery2 do begin
      sql.Text:='SELECT * FROM GRUPPE WHERE gr_id = :GRUPPE';
      With Params.ParamByName('GRUPPE'do begin
       DataType := ftinteger;
       Value := id_wert;
      End;
      open;
     end;
//     exit;
    until ID_wert = AKT_ID;
   until akt_gruppe=0;
  end //ZUM ROOT VORDRINGEN
  else
   repeat
    FieldList.add(ZQuery2.fieldbyname('NAME').asstring);
   until ZQuery2.FindNext=false;
 FieldList.add(ZQuery1.fieldbyname('NAME').asstring);

 Result := FieldList;
end;


Das Problem ist halt, dass ich erst in die eine Richtung nach weiteren Einträgen / Knoten suchen und dann in die andere. Sicher kann man das Verbessern. Nur leider hat's bei mir nie funktioniert, so wie ich's probiert hatte.

Bin für Verbesserungsvorschläge und Fragen offen.

Danke schonmal auch für die bisherige Hilfe!!!
Gruß cyberax


cyberax - Mo 26.02.07 20:49

Hallo nochmal,

hier nochmal eine geänderte Variante der Function. Konnte bis jetzt keine "Fuktionsstörungen" festestellen....

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
function TForm1.GetFieldList: TStringList;
var akt_gruppe:integer;
    i,j:integer;
begin
 FieldList.clear;
 with form1.ZQuery2 do begin
  sql.Text:='SELECT * FROM GRUPPE WHERE ID = :GRUPPE';
  With Params.ParamByName('GRUPPE'do begin
   DataType := ftinteger;
   Value := form1.ZQuery1.fieldbyname('GR_ID').asinteger;
  End;
  open;
 end;
 if ZQuery2.RecordCount>0 then
  if ZQuery2.FieldByName('gr_id').asinteger > 0 then begin //ZUM ROOT VORDRINGEN
   repeat
    FieldList.add(ZQuery2.fieldbyname('NAME').asstring);
    akt_gruppe:=zQuery2.fieldbyname('gr_id').AsInteger;
    with form1.ZQuery2 do begin
     sql.Text:='SELECT * FROM GRUPPE WHERE ID = :GRUPPE';
     With Params.ParamByName('GRUPPE'do begin
      DataType := ftinteger;
      Value := akt_gruppe;
     End;
     open;
    end;
   until akt_gruppe=0;
   i:= 0;
   j :=  FieldList.count-1;
   while i<j do
    begin
     FieldList.Exchange(I,J);
     inc(i);
     dec(j)
    end;
  end
  else
   repeat
    FieldList.add(ZQuery2.fieldbyname('NAME').asstring);
   until ZQuery2.FindNext=false;
 FieldList.add(ZQuery1.fieldbyname('NAME').asstring);
 Result := FieldList;
end;


Gruß cyberax