Welche uses braucht GetSubDirectories?

Rund um die LCL und andere Komponenten
Lion
Beiträge: 199
Registriert: Mo 24. Mär 2014, 09:58

Re: Welche uses braucht GetSubDirectories?

Beitrag von Lion »

Ok... hatte ich auch so gedacht... Die TreeView bleibt aber leer... :x

Habe das Projekt angefügt, wenn jemand Lust und Zeit hast, könnte sich das anschauen. :mrgreen:
Dateianhänge
treeviewnachbuch.zip
(276.03 KiB) 103-mal heruntergeladen

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

Re: Welche uses braucht GetSubDirectories?

Beitrag von theo »

Lion hat geschrieben:Ok... hatte ich auch so gedacht... Die TreeView bleibt aber leer... :x
Ja, da stimmt ja auch nicht viel. :wink:
Was willst du eigentlich erreichen?
Möchtest du dich nicht lieber an TShellTreeView orientieren?
Das funktioniert immerhin schon mal.

ruewa
Beiträge: 153
Registriert: Sa 12. Apr 2014, 14:43

Re: Welche uses braucht GetSubDirectories?

Beitrag von ruewa »

Lion hat geschrieben:Ja, aber es knallt, siehe Screenshot. Weiß jetzt nicht was der Fehler bedeutet.
Der Fehler bedeutet, daß der Node, den Du übergibst (z.B. TreeView1.Items[0]) noch gar nicht existiert. Wenn Du mit dem TreeView arbeitest, mußt Du den zuerst erstellen, wie's sehr viel einfacher geht, dazu anschließend.

Nodes sind einfach Einträge im TreeView. Die heißen Nodes ("Knoten"), weil sie sowohl einen Elternteil und Kinder, als auch Geschwister haben können, bzw. graphisch gesehen: Verbindungen nach oben, unten, links und rechts haben. Deshalb haben Nodes auch eine etwas komplizierte Datenstruktur, sie müssen sich z.B. merken, wie ihre Eltern, Geschwister und Kinder heißen - wie im richtigen Leben halt...

Leicht modifiziert, funktioniert Dein Listing dann doch:

Code: Alles auswählen

Function TForm1.ShowDirectory(Const ADir: String; TV: TTreeView;
                                 ParentNode: TTreeNode): Integer;
  Var I,II: Integer;
         N: TTreeNode;
         L: TStringList;
         D: String;
begin
                                           { Statt dem hier:
  L := TStringList.Create;
  try
    Result := GetSubDirectories(ADir, L);
                                            nimm das: }
  L := FindAllDirectories(ADir, false);    
  try
    Result := L.Count;
                                           { Und dann weiter wie gehabt: }
    if result > 0 then ...
 
Was FindAllDirectories angeht: Wie gesagt, erstellt die Funktion diese Liste selbst, was schnell mal zu Speicherlecks führt, wenn man den Mechanismus nicht versteht. Außerdem: Wenn ADir z.B. 'C:' ist und man den 2. Parameter auf "true" setzt, rödelt es ewig, bis womöglich jedes Verzeichnis auf Deinem Computer eingelesen ist!

Der Aufruf geht wie gesagt deshalb schief, weil Du einen nicht existenten Node übergibst. Du brauchst dem TreeView gegenüber nur ehrlich zu sein und ihm stattdessen "nil" übergeben, schon funktioniert es:

Code: Alles auswählen

ShowDirectory('C:', TreeView1, nil);

Ich bin mir aber nicht sicher, ob das wirklich das ist, was Du haben möchtest. Einen Verzeichnisbaum von Hand zu programmieren, mag lehrreich sein, aber dafür gibt es schon eine fertige Lösung: TShellTreeView (ist im Komponentenreiter "Misc"). Da gibst Du Dein gewünschtes Verzeichnis in die die Property "Root" ein (im Objektinspektor oder zur Laufzeit), und fertig ist die Laube!

Gruß Rüdiger

Lion
Beiträge: 199
Registriert: Mo 24. Mär 2014, 09:58

Re: Welche uses braucht GetSubDirectories?

Beitrag von Lion »

Danke Dir ruewa! Das funktioniert, aber das Problem ist, es wird nur die erste Ebene der Verzeichnisstruktur angezeigt, also es werden keine Unterverzeichnisse eingelesen. Kann es sein, weil ParentNode nil ist?

Wieso ich kein ShellTreeView nehme habe ich hier kurz geschildert. http://www.lazarusforum.de/viewtopic.php?f=18&t=8499
Dateianhänge
Screenshot....JPG

ruewa
Beiträge: 153
Registriert: Sa 12. Apr 2014, 14:43

Re: Welche uses braucht GetSubDirectories?

Beitrag von ruewa »

Lion hat geschrieben:aber das Problem ist, es wird nur die erste Ebene der Verzeichnisstruktur angezeigt, also es werden keine Unterverzeichnisse eingelesen.
Um das zu ändern, könntest Du den 2. Parameter von FindAllDirectories auf "true" setzen. Aber das kann dann frustrierend lange dauern, weil die Funktion Deinen Rechner dann u.U. bis in die hinterste Ecke durchstöbert.

Aber auch das ist Krampf, dann bekommst Du zwar eine endlose Auflistung, aber auch wieder keinen echten Verzeichnisbaum. Im Grunde müßtest Du von Hand auf jeder Ebene jedes Verzeichnis wiederum durch Dein "ShowDirectory" jagen, dann eben mit dem jeweiligen Verzeichnis als Parent. Am Ende hättest Du bestenfalls einen Humunculus von ShellTreeView erschaffen...
Lion hat geschrieben:Wieso ich kein ShellTreeView nehme...
Das war mir entgangen, entschuldige. Da hatte Theo aber völlig recht: Die ShellTreeView lädt nicht den kompletten Verzeichnisbaum auf einmal ein, sondern schaut bei Bedarf nach der angeforderten Ebene. Auf diese Weise vermeidet sie das oben genannte Zeitproblem.

Soweit ich sehe, stört Dich ja, daß im eingeklappten Zustand von Anfang an unklar ist, ob ein angezeigtes Verzeichnis Unterverzeichnisse enthält oder nicht. Gefühlsmäßig würde ich da aber eher bei einer ShellTreeView ansetzen, da wird es sicher Möglichkeiten geben (vielleicht programmatisch auf- und gleich wieder einklappen oder irgend sowas).

Gruß Rüdiger
Zuletzt geändert von ruewa am Di 3. Feb 2015, 18:40, insgesamt 1-mal geändert.

Lion
Beiträge: 199
Registriert: Mo 24. Mär 2014, 09:58

Re: Welche uses braucht GetSubDirectories?

Beitrag von Lion »

Ja mich stören die [+]-Zeichen bei leeren Verzeichnissen. Könnte ich das wegbekommen, würde mir das schon reichen. :roll:

ruewa
Beiträge: 153
Registriert: Sa 12. Apr 2014, 14:43

Re: Welche uses braucht GetSubDirectories?

Beitrag von ruewa »

Hallo Lion,

stimmt, das würde mich auch stören. Ich hab mir das nun etwas genauer angesehen und eine Möglichkeit gefunden, das abzustellen. Muß aber dazu sagen, daß ich mit der ShellTreeView bisher nicht viel gemacht habe und ihr Innenleben daher nicht gut kenne. Es dürfte also sicherlich noch bessere Wege geben.

Es ist so, wie Theo sagte: Die ShellTreeView lädt nur das, was sie augenblicklich braucht, ihr Items.Count wächst beständig an, je mehr Ordner man öffnet. Es sieht so aus, als würde die Property TTreenode.HasChildren anfänglich auf true initialisiert, ohne echte Prüfung. Das ist wahrscheinlich der Ursprung des Problems.

Überhaupt finde ich die Komponente - freundlich ausgedrückt - doch arg sperrig, das Ändern mancher Eigenschaften (z.B. von AutoExpand oder HotTrack) hat bei mir überhaupt nichts bewirkt. Und daß ein Doppelklick mal Expandieren bewirkt und mal das Editieren des Dateinamens (so ist's jedenfalls bei mir), ist schon ziemlich nervig (das kann man bestimmt auch irgendwo abstellen, hab's nur nicht gefunden auf die Schnelle). Die Dokumentation ist auch beklagenswert. Aber okay, mit etwas Gebastel erfüllt das Ding dann schon seinen Zweck. Was Du bemängelst, die Anfangsbehauptung von Unterverzeichnissen, wo keine sind, läßt sich relativ einfach beheben - es ist nur mühsam, den Ansatzpunkt zu finden. Folgende Prozedur gehört eigentlich in die ShellTreeView-Komponente selbst, aber der Einfachheit halber habe ich sie als Methode von TForm1 implementiert:

Code: Alles auswählen

procedure TForm1.UpdateShellTreeView(STV: TShellTreeView);
var
  TN : TTreeNode;
begin
  STV.BeginUpdate;
  TN := STV.Items.GetFirstVisibleNode;
  Repeat
    if not TN.Expanded then
    begin
      TN.Expand(false);
      TN.Collapse(false);
    end;
    TN := TN.GetNextVisible;
  until TN = nil;
  STV.EndUpdate;
end;
Das ist es dann schon weitgehend. Die letzte Schwierigkeit besteht nur noch darin, das passende Event zu finden, wo man den Aufruf plazieren kann, ohne daß er ständig durchlaufen wird und Zeit raubt, aber doch da ist, wenn man ihn braucht. Das einzig Passende, das ich gefunden habe, ist TShellTreeView.OnCustomDraw:

Code: Alles auswählen

procedure TForm1.ShellTreeView1CustomDraw(Sender : TCustomTreeView;
  const ARect : TRect; var DefaultDraw : Boolean);
begin
   UpdateShellTreeView(ShellTreeView1);
end;
Wie gesagt: Ist nur ein Workaround und sicher nicht das Gelbe vom Ei. Aber so funktioniert es jetzt glaube ich ganz gut, auch mit einem akzeptablen Zeitverhalten. Sonst müßte man halt in die Komponente selbst gehen und da wäre vermutlich der aussichtsreichste Weg der, dafür zu sorgen, daß TTreeNode.HasChildren beim Initialisieren/Hinzufügen gleich überprüft und korrekt gesetzt wird.

Gruß Rüdiger

Lion
Beiträge: 199
Registriert: Mo 24. Mär 2014, 09:58

Re: Welche uses braucht GetSubDirectories?

Beitrag von Lion »

Danke Dir!

Habe es eben ausprobiert und die TreeView verhält sich irgendwie komisch. Bei sehr großen Verzeichnissen wird sie einfach leer und beim Schließen reagiert die Anwendung nicht, ist wohl im Hintergrund am arbeiten.

Ich selber habe mir folgendes gebastelt, da habe ich zwar noch immer alle [+]-Zeichen, aber dafür bekommen alle leeren Verzeichnisse ein "leeres"-Icon. Man kann dann zumindest an den Icons erkennen, dass das Verzeichnis leer ist. Ist aber nur beim Start der Anwendung so, muss schauen, dass das auch beim arbeiten immer so bleibt.

Code: Alles auswählen

 
procedure TMainForm.LoadRootFolderToShellTreeView();
var
  i: Integer;
  sl: TStringList;
begin
  ShellTreeView1.BeginUpdate;
  ShellTreeView1.Root := gvDirectory;
 
  for i := 0 to ShellTreeView1.Items.Count - 1 do
  begin
    sl := FindAllDirectories(ShellTreeView1.GetPathFromNode(ShellTreeView1.Items[i]), false);
 
    if sl.Count <= 0 then
    begin
      // hier wollte ich die [+]-Zeichen ausschalten, aber geht nicht, es wird entweder für das ganze TreeView ein- oder ausgeschaltet
      //ShellTreeView1.ShowButtons := False; 
      ShellTreeView1.Items[i].ImageIndex := 1;
    end else
    begin
      //ShellTreeView1.ShowButtons := True;
      ShellTreeView1.Items[i].ImageIndex := 0;
    end;
  end;
 
  ShellTreeView1.EndUpdate;
end; 
 
Dateianhänge
Screenshot.JPG
Screenshot.JPG (24.52 KiB) 2988 mal betrachtet

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

Re: Welche uses braucht GetSubDirectories?

Beitrag von theo »

Oder einfach so?

Bei OnCustomDrawItem einhängen.

Code: Alles auswählen

procedure TForm1.ShellTreeView1CustomDrawItem(Sender: TCustomTreeView;
  Node: TTreeNode; State: TCustomDrawState; var DefaultDraw: Boolean);
var sl: TStringList;
begin
  sl := FindAllDirectories(TShellTreeView(Sender).GetPathFromNode(Node), false);
  Node.HasChildren:= sl.Count > 0;
  sl.free;   
end;     

Lion
Beiträge: 199
Registriert: Mo 24. Mär 2014, 09:58

Re: Welche uses braucht GetSubDirectories?

Beitrag von Lion »

Hm... verstehe ich nicht ganz... und was ist mir der LoadRootFolderToShellTreeView();?

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

Re: Welche uses braucht GetSubDirectories?

Beitrag von theo »

Lion hat geschrieben:Hm... verstehe ich nicht ganz... und was ist mir der LoadRootFolderToShellTreeView();?
Wieso? Funktioniert das nicht bei dir?

LoadRootFolderToShellTreeView() ist von dir, das musst du selber wissen, was damit ist. :wink:

P.S. man könnte es natürlich noch effizienter machen, denn man braucht ja nicht wissen, wieviele Unterverzeichnisse da sind, sondern nur, ob es eines gibt.
Zuletzt geändert von theo am Mi 4. Feb 2015, 12:30, insgesamt 1-mal geändert.

Lion
Beiträge: 199
Registriert: Mo 24. Mär 2014, 09:58

Re: Welche uses braucht GetSubDirectories?

Beitrag von Lion »

Also wenn ich dein Vorschlag dazu nehme, dann sieht es schon mal gut aus. :D

Muss jetzt nur noch überlegen, wie ich das ganze in eine Prozedure packe.

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

Re: Welche uses braucht GetSubDirectories?

Beitrag von theo »

Lion hat geschrieben:Also wenn ich dein Vorschlag dazu nehme, dann sieht es schon mal gut aus. :D

Muss jetzt nur noch überlegen, wie ich das ganze in eine Prozedure packe.
Mit den Images?
Das sollte in einem Aufwasch gehen, dann kannst du LoadRootFolderToShellTreeView vergessen.

Code: Alles auswählen

procedure TForm1.ShellTreeView1CustomDrawItem(Sender: TCustomTreeView;
  Node: TTreeNode; State: TCustomDrawState; var DefaultDraw: Boolean);
var sl: TStringList;
begin
  sl := FindAllDirectories(TShellTreeView(Sender).GetPathFromNode(Node), false);
  Node.HasChildren:= sl.Count > 0;
  if Node.HasChildren then Node.ImageIndex:=0 else Node.ImageIndex:=1;
  sl.free;   
end;    
 

ruewa
Beiträge: 153
Registriert: Sa 12. Apr 2014, 14:43

Re: Welche uses braucht GetSubDirectories?

Beitrag von ruewa »

Lion hat geschrieben:Habe es eben ausprobiert und die TreeView verhält sich irgendwie komisch. Bei sehr großen Verzeichnissen wird sie einfach leer und beim Schließen reagiert die Anwendung nicht, ist wohl im Hintergrund am arbeiten.
Aha... Das kann ich nicht bestätigen, bei mir arbeitet dieser Workaround völlig reibungslos, auch wenn ich viele Verzeichnisse aufmache. Aber während meiner Versuche daran hatte ich das auch öfters. Bist Du sicher, daß Du es in das Event OnCustomDraw eingehängt hast und nicht in OnCustomDrawItem oder eines der OnAdvancedCustomDraws? Genau da hatte ich das nämlich auch. Ansonsten könntest Du noch versuchen, alle Properties auf Standard zurückzustellen, da scheint es auch manchmal komische Quereffekte zu geben. Eine weitere Fehlermöglichkeit wäre, Expand / Collapse mit dem Parameter True für rekursive Aktionen aufzurufen, dann kannst Du natürlich erstmal Kaffee kochen gehen...

Kann aber auch sein, daß da Unterschiede zwischen Windows und Linux / GTK2 zuschlagen. Ich weigere mich seit 12, 13 Jahren beharrlich, Windows-Rechner auch nur anzukucken, deshalb kann ich das weder testen noch etwas dazu sagen.
Lion hat geschrieben:Ich selber habe mir folgendes gebastelt...

Code: Alles auswählen

...
  for i := 0 to ShellTreeView1.Items.Count - 1 do ...
  ...
  ShellTreeView1.Items[i].ImageIndex := 0; ...
Die i:=0ToCount-1-Variante hatte ich zuerst versucht, dann aber schnell wieder verworfen. Das Problem bei diesem Konstrukt ist, daß sich der Inhalt der Liste ständig ändert, sobald man irgendwas auf- und zumacht, Count wächst dabei ständig an. Und offenbar werden die Nodes dabei nicht einfach hinten angefügt, sondern eingeordnet, so daß diese Schleife im Endeffekt irgendwelche zufälligen Treffer abarbeitet und dann abbricht. Das ist jetzt bei Dir nicht der Fall, weil Du keine Expand-/Collapse-Aktionen durchführst, ich erwähne es nur mal vorsorglich.

ShowButtons ist klar, das ist eine node-übergreifende TreeView-Property und eignet sich daher nicht. Und ein Fehler ist, daß Du die Stringlisten zwar en masse erzeugst, aber nicht mehr freigibst: In die i-Schleife gehört hinten also noch ein SL.Free.

Ansonsten aber ist Deine Variante gar nicht so verkehrt, nur trifft ImageIndex das Ziel nicht wirklich. Das Kästchen scheint an TTreeNode.HasChildren zu hängen, und das ist erstmal ungeprüft auf true gesetzt (das ist m.E. der interne Kern des Problems). Das hab ich jetzt mal ausprobiert, und - eh voila! - es funktioniert:

Code: Alles auswählen

procedure TForm1.UpdateShellTreeView(STV: TShellTreeView);
var
  TN : TTreeNode;
  SL : TStringList;
begin
  STV.BeginUpdate;
  TN := STV.Items.GetFirstVisibleNode;
  Repeat
    if (not TN.Expanded) and TN.HasChildren then
    begin
      SL := FindAllDirectories(STV.GetPathFromNode(TN), false);
      TN.HasChildren := SL.Count > 0;
      SL.Free;
    end;
    TN := TN.GetNextVisible;
  until TN = nil;
  STV.EndUpdate;
end;
Ist eigentlich auch die elegantere Methode als der Schotterweg über Expand/Collapse - Danke für die Anregung! Bin gespannt, ob das bei Dir funktioniert.

Gruß Rüdiger

Edit: Ah, sehe gerade, Theo hatte diesselbe Idee.
Edit2: Richtig, Theo, Deine Variante, es in OnCustomDrawItem einzuhängen, ist noch eleganter. Ich würde nur aus Laufzeitgründen noch die Bedingung if (not expanded) and HasChildren hinzufügen.

Lion
Beiträge: 199
Registriert: Mo 24. Mär 2014, 09:58

Re: Welche uses braucht GetSubDirectories?

Beitrag von Lion »

theo hat geschrieben:
Lion hat geschrieben:Also wenn ich dein Vorschlag dazu nehme, dann sieht es schon mal gut aus. :D

Muss jetzt nur noch überlegen, wie ich das ganze in eine Prozedure packe.
Mit den Images?
Das sollte in einem Aufwasch gehen, dann kannst du LoadRootFolderToShellTreeView vergessen.

Code: Alles auswählen

procedure TForm1.ShellTreeView1CustomDrawItem(Sender: TCustomTreeView;
  Node: TTreeNode; State: TCustomDrawState; var DefaultDraw: Boolean);
var sl: TStringList;
begin
  sl := FindAllDirectories(TShellTreeView(Sender).GetPathFromNode(Node), false);
  Node.HasChildren:= sl.Count > 0;
  if Node.HasChildren then Node.ImageIndex:=0 else Node.ImageIndex:=1;
  sl.free;   
end;    
 
Sag mal und wie lade ich die TreeView während der Laufzeit immer wieder neu?

Edit: Habe schon gefunden:

Code: Alles auswählen

 
  ShellTreeView1.BeginUpdate;
  ShellTreeView1.Root := '';
  ShellTreeView1.Root := gvDirectory;
  ShellTreeView1.EndUpdate; 
 
Zuletzt geändert von Lion am Mi 4. Feb 2015, 13:36, insgesamt 2-mal geändert.

Antworten