Dateiauflistung verschnellern

Für alles, was in den übrigen Lazarusthemen keinen Platz, aber mit Lazarus zutun hat.
Antworten
Nils
Beiträge: 130
Registriert: Mo 28. Mai 2007, 12:36
Kontaktdaten:

Dateiauflistung verschnellern

Beitrag von Nils »

Hi,

der Windows Explorer listet Dateien immer schneller als mein Programm. Die Frage ist eigentlich kurz und einfach: Wie könnte man die Auflistung noch verschnellern ? Der folgende Code sollte sogar - abgesehen von so Kleinigkeiten wie das Auslesen des Icons eines Dateityps - unter Linux laufen. Habe es allerdings noch nicht getestet. Bietet Lazarus noch irgendwelche Funktionen die Delphi nicht bietet ? Der Code wird später noch unter Linux zum Einsatz kommen, daher hielt ich es für sinnvoll, mit dem SearchRec zu arbeiten, da dieses wie ich irgendwo gelesen hatte auch mit Linux benutzbar sei.

Der Code ist weder komplex noch lang. Die Kommentare lassen es viel aussehen, erklären allerdings alles ausführlich.

Code: Alles auswählen

type
  TItem = record
    Pfad, Name, Typ, Alter : String;
    ImageIndex             : Integer;
  end;
  TDynItemArray = Array of TItem;
 
  TRazFileManager = class
  private
    LV                     : TListView;
    Sep                    : String; // / oder \ Beispiel: /mnt/datene/blubb/asd oder E:\blubb\asd.
    Ext                    : TStrings; // Stringlist mit allen Dateiendungen, von denen bisher ein Icon geladen worden ist.
    Dirs, Files, DirsFiles : TDynItemArray; // Dirs=Array mit Verzeichnissen, Files=Array mit Dateien, DirsFiles=Sortierter Zusammenschluss von Dirs und Files.
    Img16, Img32           : TImageList; // In Img16 sind die kleinen, in Img32 die etwas größeren Icons.
    Pfad                   : String; // Um später besser navigieren zu können schadet es nicht, den Pfad parat zu haben, statt ihn ständig ermitteln zu müssen.
  public
    Filter              : TStrings; // Suchfilter.
    procedure Liste(Directory : String; Recursive, ClearList : Boolean); // Das ist der kleine Algorithmus.
  end;
 
implementation
 
// Als Parameter benötigt man das Verzeichnis (Standard "root"), ob rekursiv gesucht werden soll (Standard "False") und ob die ListView wieder geleert werden soll (Standard "True").
procedure TRazFileManager.Liste(Directory : String; Recursive, ClearList : Boolean);
  // Durchsuchen eines einzelnen Verzeichnisses
  procedure ScanDir(Directory : String);
  var SR        : TSearchRec;
      lIcon     : TIcon; // Ein temporäres Icon welches zur Verarbeitung leider nötig ist.
      fTyp      : String; // Das ist die wahre Dateiendung (zum Beispiel: .mp3). Der TItem.Typ ist hingegen die Beschreibung eines Typen (zum Beispiel: MPEG3).
      IconIndex : Integer; // IconIndex darf nicht mit ImageIndex verwechselt werden, es dient zur Zuordnung des Icons: Ist es schon geladen ?.
      ViewSize  : Integer; // Die Funktion GetExtIcon liest das Icon eines Dateityps/Programms aus. Da die Funktion intern prüft, welcher ViewStyle gerade benutzt wird, ist es zwecklos, dies hier ein paar Zeilen später noch einmal zu prüfen. Man muss schließlich wissen, in welche ImageList das Icon soll. Daher gibt es die Variable ViewSize, welche die GetExtIcon zurückgibt. Dies erspart ein paar ors. 
  begin
    if FindFirst(Directory+'*.*', faAnyFile and not faDirectory, SR) = 0 then
      try
        repeat
          // Dateisuche: Passt der Filter ?
          if (Filter.IndexOf(ExtractFileExt(SR.Name)) <> -1) or (Filter.Count < 1) then
          begin
            SetLength(Files, Succ(Length(Files)));
            // Dateiendung der aktuellen Datei bestimmen.
            fTyp := LowerCase(ExtractFileExt(Directory+SR.Name));
            // Ab jetzt ist Pfad nicht der in private deklarierte Pfad mehr, sondern der in TItem deklarierte. Denn Files ist ein TDynItemArray.
            with Files[High(Files)] do
            begin
              // Pfad setzen.
              Pfad  := Directory;
              // Name bestimmen und setzen.
              Name  := ExtractFileName(Directory+SR.Name);
              // Dateibeschreibung bestimmen und setzen.
              Typ   := GetExtDes(fTyp, False);
              // Letzte Dateiänderung auslesen.
              Alter := IntToStr(FileAge(Directory+SR.Name));
              // Weitere Dateiinformationen folgen in Zukunft noch, aber ich hätte sie zur Übersicht auch so weggelassen.
 
              // Wurde das Icon dieser Dateiendung bereits geladen ?
              IconIndex  := Ext.IndexOf(fTyp);
              if IconIndex = -1 then
              begin                                                                   
                lIcon := TIcon.Create;
                if fTyp = '.exe' then // Derzeit noch etwas windowsspezifisch, ich werde das auch noch ändern.
                begin
                  // Da die Programmicons nur sehr grundlegend etwas mit der Dateiendung zu tun haben (Ausnahme: DOS-Programme haben kein Icon, bekommen daher ein festes), muss man Programme anders behandeln. Es wird daher der komplette Pfad+Dateiname+Dateiendung hinzugefügt. Wenn man zwei mal in unterschiedlichen Ordner eine a.exe hat. Heißt das nicht, dass es das selbe Programm ist. Daher gehe ich hier auf Nummer sicher. Ich muss alledings gestehen, dass ich die Stringlist Ext etwas vergewaltige, da keine Dateiendung hinzugefügt wird, aber das ist eigentlich egal.
                  Ext.Add(Directory+SR.Name);
                  // Icon auslesen mit dem kompletten Pfad+Dateiname+Dateiendung, sowie eine temporäre Zuweisung auf das schon oben erzeugte lokale Icon (lIcon). Das False sagt nur, dass die Datei nicht existieren muss, was die beiden Angaben LV.ViewStyle und ViewSize für einen Sinn haben, ist oben bei der Deklaration von ViewSize erklärt.
                  GetExtIcon(Directory+SR.Name, lIcon, False, LV.ViewStyle, ViewSize)
                end else
                begin
                  // Da abgesehen von Programmen die Icons von der Dateiendung abhängen, kann man hier lockerer arbeiten.
                  Ext.Add(fTyp);
                  GetExtIcon(fTyp, lIcon, False, LV.ViewStyle, ViewSize);
                end;
 
                // Wie bei der Deklaration von ViewSize beschrieben, gibt GetExtIcon die größe des Icons zurück. Daher wird hier übersichtlich geprüft, welche Imagelist befüllt werden muss.
                if ViewSize = 16 then
                begin
                  // Oben wurde das Icon auf lIcon zugewiesen oder eher gesagt gezeichnet.
                  Img16.AddIcon(lIcon);
                  // Der ImageIndex des ListView-Eintrags ist logischerweise der höchste der Imagelist.
                  ImageIndex := Pred(Img16.Count);
                end else
                if ViewSize = 32 then
                begin                 
                  Img32.AddIcon(lIcon);
                  ImageIndex := Pred(Img32.Count);
                end;
                lIcon.Free;
              end else
                // Wenn in Ext die Dateiendung doch gefunden wurde, muss sie nicht ausgelesen werden, daher nimmt man einfach den IconIndex+ImgAL. AL steht für Anfangslänge. Ich weise schon ganz am Anfang Icons wie Datenträger, CDROM, Diskette usw. zu. Damit das dann alles relativ automatisch abläuft, habe ich diese Variable eingeführt, welche einfach draufaddiert werden muss.
                ImageIndex := IconIndex+ImgAL;
            end;
          end;
        until
          FindNext(SR) <> 0;
      finally
        FindClose(SR);
      end;
 
    if FindFirst(Directory+'*.*', faAnyFile, SR) = 0 then
      try
        repeat
          // Ist es ein Verzeichnis ?
          if ((SR.Attr and faDirectory) = faDirectory) and (SR.Name <> '.') and (SR.Name <> '..') then
          begin
            SetLength(Dirs, Succ(Length(Dirs)));
            with Dirs[High(Dirs)] do
            begin
              // Pfad setzen.
              Pfad        := Directory;
              // Name bestimmen und setzen.
              Name        := ExtractFileName(Directory+SR.Name);
              // Beschreibung setzen
              Typ         := 'Verzeichnis';
              // Vielleicht fällt es jemanden auf: Bei der Dateisuche wurde auch das Änderungsdatum der Datei (FileAge) hinzugefügt, hier nicht. Gibt es eine Möglichkeit, das Änderungsdatum eines Ordners zu ermitteln ?
              // Die Verzeichnisse werden bei dem zur Übersichltichkeit weggelassenen Konstruktur als letzte Icons geladen. Daher kann ich logischerweise einfach ImgAL setzen.
              ImageIndex  := ImgAL;
            end;
            // Bei einer Rekursion ist die Prozedur noch nicht fertig.
            if Recursive then
              ScanDir(Directory+SR.Name+Sep);
          end;
        until
          FindNext(SR) <> 0;
      finally
        FindClose(SR);
      end;
  end;
var i : Integer;
begin
  // Bei jeder Suche muss alles abgesehen von der Stringlist Ext zurückgesetzt werden.
  SetLength(Dirs , 0); 
  SetLength(Files, 0);
  SetLength(DirsFiles, 0);
 
  // Root heißt in dem Fall Übersicht der Datenträger.
  if LowerCase(Directory) = 'root' then
  begin
    // Wenn man einen leeren Pfad hat, kann es Ärger geben, daher setze ich ihn hier.
    Pfad := 'root';
    GetDrives(LV);
  end else
  begin
    // Zur besseren Performance.
    LV.Items.BeginUpdate;
    try
      if ClearList then
        LV.Items.Clear;
      if not DirectoryExists(Directory) then
        Exit;
      if Directory[Length(Directory)] <> Sep then
        Directory := Directory+Sep;
      // Und los geht die Suche mit der oben gezeigten und erklärten ScanDir.
      ScanDir(Directory);
    finally
      // Falls es in einem Verzeichnis keine Unterverzeichnisse gibt, diese Absicherung.
      if High(Dirs) > 0 then
        // Ergebnis ist ein alphabetisch sortiertes Array.
        QuickSort(Dirs, 0, High(Dirs));
      // Erst die Ordner hinzufügen. In ScanDir werden alle Informationen in das TDynItemArray geschrieben, daher können diese hier flott wieder ausgelesen werden.
      for i := 0 to High(Dirs) do
      begin
        with LV.Items.Add do
        begin
          Caption := Dirs[i].Name;
          SubItems.Add(Dirs[i].Typ);
          ImageIndex := Dirs[i].ImageIndex;
        end;
        // Damit man später auch bei Doppelklick (zur Übersichtlichkeit weggelassen) keinen Ärger bekommt, wird das Array DirsFiles nun in sortierter Form befüllt. DirsFiles ist daher KEIN direkter Zusammenschluss aus dem in ScanDir entstandenen Dirs und Files. Es ist ein direkter Zusammenschluss aus den beiden eben SORTIERTEN Arrays Dirs und Files.
        SetLength(DirsFiles, Succ(Length(DirsFiles)));
        DirsFiles[High(DirsFiles)] := Dirs[i];
      end;
      // Der bei finally beginnende Code wiederholt sich hier noch einmal, allerdings auf Dateien statt Ordner bezogen. Ich kann ihn leider nicht mehr hinschreiben, da sonst dieser Code zu lange wäre und am Ende was von der Codebox weggeschnitten würde. 
      [...]
      // Was man öffnet muss man schließen.
      LV.Items.EndUpdate;
    end;
  end;
end;

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

Beitrag von theo »

Schau doch mal was am meisten bremst.
Ein paar writeln mit GetTickCount sollte schon etwas Klarheit bringen.
Die Listview ist, mindestens unter GTK2, eigentlich gar nicht so lahm.
1000 Einträge sind fast verzögerungsfrei. Liegt's an den Icons?
Ein VirtualTreeView wäre theoretisch schon besser.

pluto
Lazarusforum e. V.
Beiträge: 7192
Registriert: So 19. Nov 2006, 12:06
OS, Lazarus, FPC: Linux Mint 19.3
CPU-Target: AMD
Wohnort: Oldenburg(Oldenburg)

Beitrag von pluto »

Die ListView hatte unter Delphi ein Praktisches Event dafür, ob es das auch bei Lazarus gibt weiß ich nicht.
Mit Hilfe dieses Event kannst du eine z.b. Liste nehmen dort alle Einträge reinpacken und nur die zeichnen die gerade sichtbar sind.
http://www.delphipraxis.net/topic90011_ ... +schneller" onclick="window.open(this.href);return false;
http://www.delphipraxis.net/topic95035_ ... ew+virtual" onclick="window.open(this.href);return false;
http://www.delphipraxis.net/topic132536 ... ew+virtual" onclick="window.open(this.href);return false;
und es sind noch mehre solche Beiträge vorhanden.

Ich glaube ein Stichwort währe aufjedenfall noch Virtual. Die ListView kann man in einen Virtual
Modus schalten. Wenn ich den Beitrag finde, werde ich ihn posten.

ach ja, schau dir noch mal die unit:FileUtil
dort gibt es schon einiges Fertig.
MFG
Michael Springwald

Nils
Beiträge: 130
Registriert: Mo 28. Mai 2007, 12:36
Kontaktdaten:

Beitrag von Nils »

Da du die Lösung gefunden hast, frage ich auch direkt dich hier:
Wieso muss man OwnerDraw anstellen. Und wenn man an den obigen Code denkt, wie soll man das mit OnData überhaupt lösen ?
Meine Musik: spiker-music.net

pluto
Lazarusforum e. V.
Beiträge: 7192
Registriert: So 19. Nov 2006, 12:06
OS, Lazarus, FPC: Linux Mint 19.3
CPU-Target: AMD
Wohnort: Oldenburg(Oldenburg)

Beitrag von pluto »

Das ist eigentlich recht einfach.

Du nimmst z.b. ein TObjectList dort packst du deine Einträge rein.
Also muss du jetzt nur noch eine kleine klasse erstellen die alle nötigen Infos enthält. Diese Klasse muss du natürlich auch in der ObjectList verwenden.

Ich habe gerade gesehen das es kein Ondata gibt bei der Listview in Lazarus.
So wie ich das Oben beschrieben habe sollte das aber unter Delphi gehen, wie das jetzt genau unter Lazarus geht weiß ich im Moment leider noch nicht genau. Aber ich werde mal nach schauen.

Sonst könnte ich dir nur vorschlagen die Virtual StringTree zu verwenden.
Ich meinde zwar die verwendung von der VST aus verschiedenen gründen, aber für dich könnte es was sein.

ach ja, was mir an deinen code noch auffällt. List du gleich alle Verzeichnise ein ? also rekusiv ?
Der Explorer macht dies nicht. Der Lädt immer nur eine Ebene.
MFG
Michael Springwald

Nils
Beiträge: 130
Registriert: Mo 28. Mai 2007, 12:36
Kontaktdaten:

Beitrag von Nils »

Ich habe ein TDynItemArray namens DirsFiles. Sähe das dann in etwa so aus ?

Code: Alles auswählen

procedure ListViewData(Sender: TObject; Item: TListItem);
begin
  Item.Caption := DirsFiles[Item.Index];
  // usw.
end;
Ich liste nur optional rekursiv. Da es nur zwei Codezeilen ausmacht, habe ich es einfach eingebaut. Eventuell benötigt man es irgendwann mal für andere Zwecke.
Meine Musik: spiker-music.net

pluto
Lazarusforum e. V.
Beiträge: 7192
Registriert: So 19. Nov 2006, 12:06
OS, Lazarus, FPC: Linux Mint 19.3
CPU-Target: AMD
Wohnort: Oldenburg(Oldenburg)

Beitrag von pluto »

ich glaube so würde es aussehen, ja. da es so ein event aber anscheind nicht gibt, kannst du höchstens dafür sorgen das die ListView nicht bei jedem hinzufügen neu zeichnet. Dazu gibt es BeginUpdate und EndUpdatet....
Sie sollte unter ListView.Items zu finden sein.
Ich liste nur optional rekursiv.
Gut... das wahr bei mir nämlich der Fall, das ich es nicht tat *G*.... vor einigen Jahren.

Ich frage mich warum es dieses OnData Event nicht gibt in Lazarus. Sollte das etwa nicht in GTK drin sein bzw. in den anderen Widegates ?
Ich habe ein TDynItemArray namens DirsFiles
Naja, ich glaube eine TObjectListe währe einfacher... aber das ist auch teilweise Geschmack sache glaube ich.
MFG
Michael Springwald

pluto
Lazarusforum e. V.
Beiträge: 7192
Registriert: So 19. Nov 2006, 12:06
OS, Lazarus, FPC: Linux Mint 19.3
CPU-Target: AMD
Wohnort: Oldenburg(Oldenburg)

Beitrag von pluto »

Ich habe mir die Soruce von der ListView angesehen und bin zum Schluss gekommen, das es mit etwas Änderungen am Soruce der LW. Das ganze schnell gehen sollte
ich dachte da an Add bzw. AddItem dort wird bei jedem Hinzufügen einiges erneuert.
Das kostet natürlich Zeit. Wobei mir ist noch nicht ganz klar, ob die LW alles zeichnet oder nur den Sichtbaren bereich. Ich nehme mal an den Sichtbaren bereich.

Ich kann dir im Moment nur drei "Ratschläge" geben:
a) du wartest bist das Lazarus-Team dort ein OnData eingebaut hat, was vermutlich nicht geht.
b) eine eigene ListView bauen.
c) eine andere ListView nutzen die unter Lazarus läuft.

Da ich mich im Moment mit dem Thema Scrollen außenander gesetzt habe könnte es auch gut sein, das ich versuchen werde eine "eigene" ListView die dann von TCustomControll abgeleitet wird, erstelle. Aber das weiß ich noch nicht. Vorgenommen habe ich mir das schon vor einiger Zeit. Eigentlich sollte es eine Listbox werden, aber eine ListView würde auch gehen. Allerdings wird meine Variante wahrscheinlich nicht Delphi Kompatibel sein.
Aber wie gesagt, ich weiß noch nicht ob und wann ich damit anfange, weil ich im Moment noch andere Interessante Projekte offen habe bzw. weiter machen möchte.

Evlt. währe das auch ein gutes Projekt für ein Gemeinschaft Projekt.
MFG
Michael Springwald

Nils
Beiträge: 130
Registriert: Mo 28. Mai 2007, 12:36
Kontaktdaten:

Beitrag von Nils »

Ich bleibe vorerst sowieso bei Delphi. Wollte ich für Linux noch was machen, müsste ich eh einiges neu programmieren, da mein Programm von vornherein her gar nicht plattformunabhängig sein kann.
Meine Musik: spiker-music.net

pluto
Lazarusforum e. V.
Beiträge: 7192
Registriert: So 19. Nov 2006, 12:06
OS, Lazarus, FPC: Linux Mint 19.3
CPU-Target: AMD
Wohnort: Oldenburg(Oldenburg)

Beitrag von pluto »

achso. Dann ist es ja einfach. Bei Delphi gibt es diese Möglichkeit. Die ListView in einen Virtual Modus zu schalten. Wie genau weiß ich aber nicht mehr. Aber es geht. Aber da hast du ja auch noch andere Möglichkeiten. z.b. gibt es bereitst viele Komponenten für Delphi die Dateien und Verzeichnise anzeigen auch schnell. Teilweise sind die auch schon bei den Standart Versionen von Delphi gleich dabei.

Für Lazarus schreibe ich gerade welche. Aber das dauert noch eine weile, weil ich komplett von vorne anfangen. Dadurch bekommen sie auch (hoffe ich) mehr Funktionen und sind vorallem kürtzer, weil ich auf vorhandene Funktionen zurückgreife.

Ob Meine Datei verwaltungs Komponenten allerdigns schneller sind oder gleich schnell wie der Explorer weiß ich nicht.
MFG
Michael Springwald

Nils
Beiträge: 130
Registriert: Mo 28. Mai 2007, 12:36
Kontaktdaten:

Beitrag von Nils »

Man aktiviert den virtuellen Modus indem man OwnerData aktiviert. Anschließend muss man den Count setzen und OnData verwenden. Ich habe es so gemacht:

Code: Alles auswählen

constructor TRazFileManager.Create(aForm : TForm; aSep : String; aViewStyle : TViewStyle);
begin
  [...]
  LV := TListView.Create(Form);
  with LV do
  begin
    [...]
    OwnerData := True;
    [...]
  end;
  [...]
end;
 
procedure TRazFileManager.Liste(Directory : String; Recursive, ClearList : Boolean);
  [...]
begin
  [...]
  LV.Items.Count := Succ(Length(DirsFiles));
  [...]
end;
 
procedure TRazFileManager.LVData(Sender: TObject; Item: TListItem);
begin
  if Pred(Item.Index) > -1 then
    Item.Caption := DirsFiles[Pred(Item.Index)].Name+' < '+IntToStr(Item.Index);
end;
Ich weiß nicht warum, aber wenn ich anstelle von Pred(Item.Index) Item.Index verwende, gibt es sofort Zugriffsverletzungen. Mache ich es so wie eben gezeigt, werden alle Einträge gelistet, allerdings um eins nach hinten verschoben. Damit der letzte Eintrag überhaupt gelistet wird, habe ich bei LV.Items.Count ein Succ davor geschrieben. Dieses Succ müsste normalerweise ein Pred sein. Ich möchte die Aufschiebung nach hinten nicht, seht ihr meinen Fehler ? Die Erstellung von DirsFiles ist im ersten Beitrag kommentiert zu sehen.

pluto
Lazarusforum e. V.
Beiträge: 7192
Registriert: So 19. Nov 2006, 12:06
OS, Lazarus, FPC: Linux Mint 19.3
CPU-Target: AMD
Wohnort: Oldenburg(Oldenburg)

Beitrag von pluto »

Eigentlich brauchst du die Abfrage überhaupt nicht.
Die LV.Items.Count ist immer um eins höher weil sie bei 1 anfängt und nicht bei 0.
der Item.Index allerdings fängt bei 0 an.
MFG
Michael Springwald

Antworten