[Lazarus] Rozważania o TreeView

Projekty użytkowników forum zarówno sprzętowe, jak i związane z programowaniem w dowolnym języku.
Awatar użytkownika
gaweł
Expert
Expert
Posty: 761
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

[Lazarus] Rozważania o TreeView

Postautor: gaweł » sobota 11 maja 2019, 02:53

Potyczki z TreeView

Ostatnio gwałtownie tworzyłem nowe oprogramowanie, gdzie
komponent TTreeView był bardzo istotnym elementem tego programu.
Reminiscencje z terenu walki z relaksacyjną muzą w tlehttps://www.youtube.com/watch?v=QCPbMnwGhbU


Lazarus - zintegrowane środowisko programistyczne (IDE) oparte na kompilatorze Free Pascal. Jest to wzorowane na Delphi wizualne środowisko programistyczne oraz biblioteka Lazarus Component Library (LCL), która jest odpowiednikiem VCL. Program napisany w środowisku Lazarus można bez żadnych zmian skompilować dla dowolnego obsługiwanego procesora, systemu operacyjnego i interfejsu okienek. Lazarus (w większości przypadków) jest zgodny z Delphi. Jest brakującą częścią układanki, która pozwala na rozwijanie programów, podobnie jak w Delphi, na wszystkich platformach obsługiwanych przez FPC. W odróżnieniu od Javy, która stara się, aby raz napisana aplikacja działała wszędzie (write once run anywhere), Lazarus i Free Pascal starają się, aby raz napisana aplikacja kompilowała się wszędzie (write once compile anywhere). Ponieważ dostępny jest dokładnie taki sam kompilator, w większości przypadków nie trzeba wprowadzać żadnych zmian, aby otrzymać taki sam produkt dla różnych platform.
Lazarus zawiera wiele komponentów przydatnych do szybkiego tworzenia aplikacji. Jednym z takich komponentów jest TTreeView → komponent do „obróbki na ekranie” wszelkich zagadnień o strukturze drzewiastej. Sam komponent wygląda jak okienko do przeglądania tekstów.

Budowanie drzewa

Typowym problemem w posługiwaniu się komponentem TreeView jest zasilenie go w dane. Poniższy przykład pokazuje sposób zainicjowania „płaskiej” (jednowarstwowej) struktury danych. W programie, jest utworzona operacja związana z utworzeniem formy programu, w której jest wypełniona wstępnie jakaś struktura drzewa.

Kod: Zaznacz cały

unit treev1unit ;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ComCtrls;

type

  { TTreeViewExample1Form }

  TTreeViewExample1Form = class(TForm)
    TreeView: TTreeView;
    procedure FormCreate(Sender: TObject);
  private
    { private declarations }
  public
    { public declarations }
  end;

var
  TreeViewExample1Form: TTreeViewExample1Form ;

implementation

{$R *.lfm}

{ TTreeViewExample1Form }

procedure TTreeViewExample1Form.FormCreate(Sender: TObject);
  var
    RootNode                      : TTreeNode ;
  begin
    TreeView . Items . Clear ;
    RootNode := TreeView . Items . AddFirst ( nil , '<root>' ) ;
    TreeView . Items . AddChild ( RootNode , 'Element 1' );
    TreeView . Items . AddChild ( RootNode , 'Element 2' );
    TreeView . Items . AddChild ( RootNode , 'Element 3' );
    TreeView . Items . AddChild ( RootNode , 'Element 4' );
  end;

end.
W powyższym programie, instrukcja TreeView.Items.Clear; ma za zadanie uwalić dotychczasową istniejącą strukturę (w tym programie to wywołanie nic nie wnosi, gdyż na dzień dobry struktura nie zawiera żadnych danych, jednak w znakomitej większości przypadków istnieje potrzeba „uwalenia” dotychczasowej struktury danych). Kolejne wywołanie RootNode:=TreeView.Items.AddFirst(nil, '<root>'); powoduje utworzenie pierwszego elementu będącego „korzeniem” struktury drzewiastej. Funkcja ta zwraca wskaźnik do utworzonego elementu. W każdym kolejnym wywołaniu TreeView.Items.AddChild(RootNode,'Element 1'); dodawane jest „dziecko” do utworzonego „rodzica” (identyfikowanego zachowanym wskaźnikiem RootNode). Powyższy program daje następujący efekt:
treev1-ul01.png
jak się kliknie na „+”, to się rozwinie struktura.
treev1-ul02.png
Efekt rozwiniętej struktury daje się uzyskać programowo na „dzień dobry”. W tym celu należy powyższy ciąg instrukcji uzupełnić instrukcją RootNode.Expanded:=True ;, co oznacza, że wszystkie elementy „dzieci” przynależne do określonego poziomu drzewa będą na dzień dobry rozwinięte.
Kod istotnego fragmentu programu wygląda następująco:

Kod: Zaznacz cały

procedure TTreeViewExample1Form.FormCreate(Sender: TObject);
  var
    RootNode                      : TTreeNode ;
  begin
    TreeView . Items . Clear ;
    RootNode := TreeView . Items . AddFirst ( nil , '<root>' ) ;
    TreeView . Items . AddChild ( RootNode , 'Element 1' );
    TreeView . Items . AddChild ( RootNode , 'Element 2' );
    TreeView . Items . AddChild ( RootNode , 'Element 3' );
    TreeView . Items . AddChild ( RootNode , 'Element 4' );
    RootNode . Expanded := True ;
  end;
Często zachodzi potrzeba utworzenia wstępnej struktury drzewiastej jako bardziej złożonej. Rozpatrzmy taki przykład:

Kod: Zaznacz cały

procedure TTreeViewExample1Form.FormCreate(Sender: TObject);
  var
    RootNode                      : TTreeNode ;
    ElementNode                   : TTreeNode ;
  begin
    TreeView . Items . Clear ;
    RootNode := TreeView . Items . AddFirst ( nil , '<root>' ) ;
    ElementNode := TreeView . Items . AddChild ( RootNode , 'Element 1' );
    TreeView . Items . AddChild ( ElementNode , 'Element 1 ---> 1' );
    TreeView . Items . AddChild ( ElementNode , 'Element 1 ---> 2' );
    TreeView . Items . AddChild ( ElementNode , 'Element 1 ---> 3' );
    TreeView . Items . AddChild ( ElementNode , 'Element 1 ---> 4' );
    ElementNode . Expanded := True ;
    ElementNode := TreeView . Items . AddChild ( RootNode , 'Element 2' );
    TreeView . Items . AddChild ( ElementNode , 'Element 2 ---> 1' );
    TreeView . Items . AddChild ( ElementNode , 'Element 2 ---> 2' );
    TreeView . Items . AddChild ( ElementNode , 'Element 2 ---> 3' );
    TreeView . Items . AddChild ( ElementNode , 'Element 2 ---> 4' );
    ElementNode := TreeView . Items . AddChild ( RootNode , 'Element 3' );
    TreeView . Items . AddChild ( ElementNode , 'Element 3 ---> 1' );
    TreeView . Items . AddChild ( ElementNode , 'Element 3 ---> 2' );
    TreeView . Items . AddChild ( ElementNode , 'Element 3 ---> 3' );
    TreeView . Items . AddChild ( ElementNode , 'Element 3 ---> 4' );
    ElementNode := TreeView . Items . AddChild ( RootNode , 'Element 4' );
    RootNode . Expanded := True ;
  end;
Uruchomienie powyższego programu daje następujący efekt na ekranie:
treev1-ul03.png
Rozwijając wszystkie powyższe elementy powstaje:
treev1-ul04.png
Łatwo skojarzyć powyższą wyświetloną strukturę z zapisem instrukcji w procedurze inicjującej. Tworzenie struktur drzewiastych może być różnie zagłębione. Kolejny przykład inicjacji:

Kod: Zaznacz cały

procedure TTreeViewExample1Form.FormCreate(Sender: TObject);
  var
    RootNode                      : TTreeNode ;
    ElementNode                   : TTreeNode ;
    ElementNode1                  : TTreeNode ;
    ElementNode2                  : TTreeNode ;
  begin
    TreeView . Items . Clear ;
    RootNode := TreeView . Items . AddFirst ( nil , '<root>' ) ;
    ElementNode := TreeView . Items . AddChild ( RootNode , 'Element 1' );
    ElementNode1 := TreeView . Items . AddChild ( ElementNode , 'El 1 -> 1' );
    ElementNode2 := TreeView . Items . AddChild ( ElementNode1 , 'El 1 -> 1 -> 1' );
    TreeView . Items . AddChild ( ElementNode2 , 'El 1 -> 1 -> -> 1' );
    TreeView . Items . AddChild ( ElementNode2 , 'El 1 -> 1 -> -> 2' );
    TreeView . Items . AddChild ( ElementNode2 , 'El 1 -> 1 -> -> 3' );
    TreeView . Items . AddChild ( ElementNode2 , 'El 1 -> 1 -> -> 4' );
    TreeView . Items . AddChild ( ElementNode1 , 'El 1 -> 1 -> 2' );
    TreeView . Items . AddChild ( ElementNode1 , 'El 1 -> 1 -> 3' );
    TreeView . Items . AddChild ( ElementNode1 , 'El 1 -> 1 -> 4' );
    TreeView . Items . AddChild ( ElementNode , 'El 1 -> 2' );
    TreeView . Items . AddChild ( ElementNode , 'El 1 -> 3' );
    TreeView . Items . AddChild ( ElementNode , 'El 1 -> 4' );
    ElementNode := TreeView . Items . AddChild ( RootNode , 'Element 2' );
    TreeView . Items . AddChild ( ElementNode , 'El 2 ---> 1' );
    TreeView . Items . AddChild ( ElementNode , 'El 2 ---> 2' );
    TreeView . Items . AddChild ( ElementNode , 'El 2 ---> 3' );
    TreeView . Items . AddChild ( ElementNode , 'El 2 ---> 4' );
    ElementNode := TreeView . Items . AddChild ( RootNode , 'Element 3' );
    TreeView . Items . AddChild ( ElementNode , 'El 3 ---> 1' );
    TreeView . Items . AddChild ( ElementNode , 'El 3 ---> 2' );
    TreeView . Items . AddChild ( ElementNode , 'El 3 ---> 3' );
    TreeView . Items . AddChild ( ElementNode , 'El 3 ---> 4' );
    ElementNode := TreeView . Items . AddChild ( RootNode , 'Element 4' );
    RootNode . Expanded := True ;
  end; 
Powyższy program w wyniku uruchomienia daje następujące wyniki:
treev1-ul05.png
Rozwijając kliknięciem myszki określony element powstaje w kolejnych fazach:
treev1-ul06.png
treev1-ul07.png
treev1-ul08.png
Jak widać, zasilanie odpowiednimi danymi struktury TTreeView nie jest sprawą skomplikowaną, jak już się wie jak to zrobić. Do tej pory nie miałem takiej potrzeby, więc nie wgryzałem się w problematykę, jednak ostatnio gwałtownie zaistniała taka potrzeba, więc musiałem pokonać mnóstwo problemów, by uzyskać oczekiwany efekt. Zajęło mi to mnóstwo czasu, więc gdyby jakaś dusza potrzebowała, to może już skorzystać... z przetartych ścieżek.
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.

Prawdziwe słowa nie są przyjemne. Przyjemne słowa nie są prawdziwe.
Lao Tse

Awatar użytkownika
gaweł
Expert
Expert
Posty: 761
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [Lazarus] Rozważania o TreeView

Postautor: gaweł » sobota 11 maja 2019, 15:14

Potyczki z TreeView
przykład budowania drzewa z „wyższej półki”


z nostalgiczną muzą w tlehttps://www.youtube.com/watch?v=kJ_BJHwkqeI

Dobrym przykładem ilustrującym problematykę zasilania struktury drzewiastej danymi jest próba wyświetlenia struktury kartotek wraz z plikami w nich zawartymi. Załóżmy, że istnieje potrzeba wyświetlenia struktury kartotek wraz z plikami w określonej lokalizacji (to jest po wskazaniu ścieżki dostępu do interesującego miejsca). Z założenia, program ma pokazać zawartość wskazanej kartoteki nie dalej niż jeden poziom w głąb.
Forma programu pokazana jest na ilustracji:
treev2-il01.png
Zawiera ona pole do wprowadzenia wskazania na kartotekę, która ma zostać pokazana w okienku TreeView, przycisk do zlecenia wykonania operacji oraz samo okienko TreeView.
Sam program jest następujący:

Kod: Zaznacz cały

unit treev2unit;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls,
  ComCtrls, DirStruct ;

type

  { TTreeViewExample2Form }

  TTreeViewExample2Form = class(TForm)
    RunButton: TButton;
    RootNameEdit: TEdit;
    TreeView: TTreeView;
    procedure FormCreate(Sender: TObject);
    procedure RunButtonClick(Sender: TObject);
  private
    { private declarations }
  public
    { public declarations }
  end;

var
  TreeViewExample2Form: TTreeViewExample2Form;

implementation

{$R *.lfm}

{ TTreeViewExample2Form }

procedure TTreeViewExample2Form.FormCreate(Sender: TObject);
begin
  Position := poDesktopCenter ;
end;


procedure TTreeViewExample2Form.RunButtonClick(Sender: TObject);
var
  TopEntry                        : DirFileElementPtr ;
  TmpEntry                        : DirFileElementPtr ;
  FileEntry                       : AnyFileElementPtr ;
  RootNode                        : TTreeNode ;
  ElementNode                     : TTreeNode ;
begin
  TopEntry := GetFileListe ( RootNameEdit . Text , '*.*' ) ;
  if TopEntry = nil then
  begin
    ShowMessage ( 'Wskazane miejsce nie zawiera żadnych plików' ) ;
    exit ;
  end ;
  TreeView . Items . Clear ;
  TmpEntry := TopEntry ;
  RootNode := TreeView . Items . AddFirst ( nil , RootNameEdit . Text ) ;
  while TmpEntry <> nil do
  begin
    FileEntry := TmpEntry ^ . FileListeLink ;
    ElementNode := TreeView.Items.AddChild ( RootNode , TmpEntry^.FileName ) ;
    while FileEntry <> nil do
    begin
      TreeView . Items . AddChild ( ElementNode , FileEntry ^ . FileName ) ;
      FileEntry := FileEntry ^ . FwdLink ;
    end ;
    TmpEntry :=  TmpEntry ^ . FwdLink ;
  end ;
  RootNode . Expanded := True ;
  DisposeDirListe ( TopEntry ) ;
end ;

end.
Po wpisaniu w polu edycyjnym wskazania na położenie wyświetlanej kartoteki należy kliknąć na przycisk „Wykonaj”. Spowoduje to wywołanie obsługi kliknięcia przycisku, która w pierwszej kolejności „wczyta z dysku” strukturę wskazanej kartoteki, jako wywołanie funkcji:
TopEntry:=GetFileListe(RootNameEdit.Text,'*.*');
Zawarta jest ona w oddzielnym module (wprost wyjętym w pewnego wiedźmińskiego oprogramowania) i zostanie pokazana w dalszej części. Zadaniem tej funkcji jest „wygarnięcie” z dysku listy kartotek zawartych we wskazanym miejscu. W dalszej kolejności wygarnięte są pliki z każdej napotkanej kartoteki. Efektem działania funkcji jest lista (zwracana jako wskaźnik będący wynikiem funkcji). Lista zbudowana jest z dwóch typów elementów:
  • listy nazw kartotek (typ DirFileElementPtr),
  • listy nazw plików dowiązanych do danej kartoteki (typ AnyFileElementPtr).
Definicje typów są następujące:

Kod: Zaznacz cały

  DirFileElementPtr               = ^ DirFileElement ;
  DirFileElement                  = record
                                      FileName           : String ;
                                      FileListeLink      : AnyFileElementPtr ;
                                      FwdLink            : DirFileElementPtr ;
                                    end ; 
oraz

Kod: Zaznacz cały

 AnyFileElementPtr               = ^ AnyFileElement ;
  AnyFileElement                  = record
                                      FileName           : String ;
                                      FwdLink            : AnyFileElementPtr ;
                                    end ;   
Listy te są powiązane następująco:
treev2-il02.png
Jeżeli zwrócony wskaźnik jest różny od nil, to zawiera strukturę pokazaną na powyższej ilustracji. Dane zawarte w tej liście służą do zainicjowania struktury TreeView (pokazania informacji w okienku).
W pierwszej kolejności utworzony jest węzeł root w wyniku wywołania:
RootNode := TreeView . Items . AddFirst ( nil , RootNameEdit . Text ) ;
Dalej są założone dwie pętle: jedna po liście elementów określających nazwy kartotek i druga po liście nazw plików zawartych w danej kartotece. Każde utworzenie węzła:
ElementNode := TreeView . Items . AddChild ( RootNode , TmpEntry ^ . FileName ) ;
dotyczące nazwy kartoteki podaje wskazanie „ojca” jako zmienną RootNode, czyli będą dołączane bezpośrednio do węzła root. Wynik działania funkcji zostaje zawarty w zmiennej ElementNode. Jest to element wskazujący na „ojca” dla elementów dodawanych na następnym poziomie zagłębienia.
Pliki z kolei, są dołączane w wyniku wywołania funkcji:
TreeView . Items . AddChild ( ElementNode , FileEntry ^ . FileName ) ;
gdzie tym razem jako wskazanie na „ojca” występuje zmienna ElementNode. I tak aż do końca listy: jednej jak i drugiej. Po załadowaniu danych do struktur TreeView, wczytana wcześniej lista wskaźnikowa jest zwalniana (w wyniku wywołania DisposeDirListe ( TopEntry ) ;).
Po uruchomieniu programu należy wpisać w odpowiednim polu: e:\microgeek\opublikowane (bo dokładnie tak jest na moim dysku) i walnąć „Wykonaj”.
treev2-il03.png
Daje to następujący wynik:
treev2-il04.png
i zgadza się to z tym, co pokazuje windozowy eksplorator.
treev2-il05.png
Z widoku TreeView wynika, że niektóre elementy nie zawierają elementów „w głąb”, choć w rzeczywistości żadna z kartotek nie jest pusta. Ma to swoje sensowne wyjaśnienie: program nie pokazuje struktury kartotek głębiej niż jeden poziom w głąb. Przykładowo w przypadku kartoteki „Diy-03-fgen” rzeczywista prawda jest następująca:
treev2-il06.png
nie ma tu plików (tylko kolejna struktura kartotek) → program z założenia pokazuje tylko jeden poziom zagłębienia.
Rozwinięcie innego elementu pokazuje:
treev2-il07.png
co jest zgodne z tym, co zeznaje oprogramowanie windozy:
treev2-il08.png
Obiecany wcześniej kawałek oprogramowania prezentuje się następująco:

Kod: Zaznacz cały

unit DirStruct ;

{$mode objfpc}{$H+}

interface

uses
  Classes , SysUtils ;

type
  AnyFileElementPtr               = ^ AnyFileElement ;
  AnyFileElement                  = record
                                      FileName           : String ;
                                      FwdLink            : AnyFileElementPtr ;
                                    end ;
  DirFileElementPtr               = ^ DirFileElement ;
  DirFileElement                  = record
                                      FileName           : String ;
                                      FileListeLink      : AnyFileElementPtr ;
                                      FwdLink            : DirFileElementPtr ;
                                    end ;

function GetFileListe ( DirRoot   : String ;
                        FileMask  : String ) : DirFileElementPtr ;

procedure DisposeDirListe ( TopEntry : DirFileElementPtr ) ;

implementation


procedure DisposeDirListe ( TopEntry : DirFileElementPtr ) ;
var
  TmpElement                      : DirFileElementPtr ;
  DelElement                      : DirFileElementPtr ;
  TmpFileElement                  : AnyFileElementPtr ;
  DelFileElement                  : AnyFileElementPtr ;
begin
  TmpElement := TopEntry ;
  while TmpElement <> nil do
  begin
    DelElement := TmpElement ;
    if DelElement ^ . FileListeLink <> nil then
    begin
      TmpFileElement := DelElement ^ . FileListeLink ;
      while TmpFileElement <> nil do
      begin
        DelFileElement := TmpFileElement ;
        TmpFileElement := TmpFileElement ^ . FwdLink ;
        dispose ( DelFileElement ) ;
      end ;
    end ;
    TmpElement := TmpElement ^ . FwdLink ;
    dispose ( DelElement ) ;
  end ;
end ;


function SpecialDirName ( FileName : String ) : boolean ;
var
  StrL                            : Integer ;
  Ch1                             : Char ;
  Ch2                             : Char ;
begin
  StrL := Length ( FileName ) ;
  if StrL = 1 then
  begin
    Ch1 := FileName . Chars [ 0 ] ;
    if Ch1 = '.' then
      SpecialDirName := True
    else
      SpecialDirName := False ;
  end
  else
  begin
    if StrL = 2 then
    begin
      Ch1 := FileName . Chars [ 0 ] ;
      Ch2 := FileName . Chars [ 1 ] ;
      if ( Ch1 = '.' ) and ( Ch2 = '.' ) then
        SpecialDirName := True
      else
        SpecialDirName := False ;
    end
    else
    begin
      SpecialDirName := False ;
    end ;
  end ;
end ;


function MakeFileMask ( Directory : String ;
                        Mask      : String ) : String ;
var
  FileMask                        : String ;
begin
  FileMask := String ( '' ) ;
  if Length ( Directory ) <> 0 then
  begin
    FileMask := Directory + String ( '\' ) ;
  end ;
  FileMask := FileMask + Mask ;
  MakeFileMask := FileMask ;
end ;


function GetDirListe ( Directory : String ) : DirFileElementPtr ;
var
  TopElement                      : DirFileElementPtr ;
  TmpElement                      : DirFileElementPtr ;
  LastElement                     : DirFileElementPtr ;
  FileMask                        : String ;
  FileInfo                        : TSearchRec ;
  Handle                          : Longint ;
begin
  TopElement := nil ;
  LastElement := nil ;
  FileMask := MakeFileMask ( Directory , '*' ) ;
  Handle := FindFirst ( FileMask , faDirectory , FileInfo ) ;
  if Handle = 0 then
  begin
    repeat
      with FileInfo do
        begin
          if ( Attr and faDirectory ) = faDirectory then
          begin
            if not SpecialDirName ( Name ) then
            begin
              new ( TmpElement ) ;
              TmpElement ^ . FileName := Name ;
              TmpElement ^ . FileListeLink := nil ;
              TmpElement ^ . FwdLink := nil ;
              if TopElement = nil then
              begin
                TopElement := TmpElement ;
              end
              else
              begin
                LastElement ^ . FwdLink := TmpElement ;
              end ;
              LastElement := TmpElement ;
            end ;
          end ;
        end ;
    until FindNext ( FileInfo ) <> 0 ;
  end ;
  GetDirListe := TopElement ;
end ;


function GetListOfDirectoryFiles ( DirName  : String ;
                                   FileMask : String ) : AnyFileElementPtr ;
var
  TopElement                      : AnyFileElementPtr ;
  TmpElement                      : AnyFileElementPtr ;
  LastElement                     : AnyFileElementPtr ;
  DirFileMask                     : String ;
  FileInfo                        : TSearchRec ;
  Handle                          : Longint ;
begin
  TopElement := nil ;
  LastElement := nil ;
  DirFileMask := MakeFileMask ( DirName , FileMask ) ;
  Handle := FindFirst ( DirFileMask , faDirectory , FileInfo ) ;
  if Handle = 0 then
  begin
    repeat
      with FileInfo do
        begin
          if ( Attr and faDirectory ) <> faDirectory then
          begin
            new ( TmpElement ) ;
            TmpElement ^ . FileName := Name ;
            TmpElement ^ . FwdLink := nil ;
            if TopElement = nil then
            begin
              TopElement := TmpElement ;
            end
            else
            begin
              LastElement ^ . FwdLink := TmpElement ;
            end ;
            LastElement := TmpElement ;
          end ;
        end ;
    until FindNext ( FileInfo ) <> 0 ;
  end ;
  GetListOfDirectoryFiles := TopElement ;
end ;


function GetFileListe ( DirRoot   : String ;
                        FileMask  : String ) : DirFileElementPtr ;
var
  TopElement                      : DirFileElementPtr ;
  TmpElement                      : DirFileElementPtr ;
  FileNamePrefix                  : String ;
begin
  TopElement := GetDirListe ( DirRoot ) ;
  if TopElement <> nil then
  begin
    FileNamePrefix := DirRoot ;
    if not FileNamePrefix . IsEmpty then
      FileNamePrefix := FileNamePrefix + '\' ;
    TmpElement := TopElement ;
    while TmpElement <> nil do
    begin
      TmpElement ^ . FileListeLink := GetListOfDirectoryFiles ( FileNamePrefix + TmpElement ^ . FileName , FileMask ) ;
      TmpElement := TmpElement ^ . FwdLink ;
    end ;
  end ;
  GetFileListe := TopElement ;
end ;

end.
Nie ma tam żadnych nadzwyczajnych rozwiązań, ot zwykła rzemieślnicza robota, którą należy wykonać, bo inaczej nic z tego nie będzie.
Dla tropicieli rozwiązań:
treeview2.zip
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.

Prawdziwe słowa nie są przyjemne. Przyjemne słowa nie są prawdziwe.
Lao Tse

Awatar użytkownika
gaweł
Expert
Expert
Posty: 761
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [Lazarus] Rozważania o TreeView

Postautor: gaweł » niedziela 12 maja 2019, 21:19

Rekurencyjne podejście do
ładowania danych do TreeView.


z relaksacyjną muzą w tlehttps://www.youtube.com/watch?v=j4_NHGbYtJA

Algorytm wygarniania listy plików zawartych na dysku zaprezentowany w poprzedniej części można zaliczyć do grupy o rozwiązaniu iteracyjnym. Nie jest to szczytowo genialne rozwiązanie, ale spełnia swoją rolę. Specyfika rozwiązywanego zagadnienia jest taka, że zaproponowane doskonale spełnia swoje zadanie. Jednak jak się coś „przymusza” do zachowań, które nie są naturalne w danym rozwiązaniu, to jak się okazuje materia stawia opór. Po prostu wszystko jest dostosowane do swojej roli, którą realizuje, w której doskonale się odnajduje. Przymuszenie algorytmu z poprzedniej części do wygarnięcia całego drzewa z dowolnie długimi gałęziami będzie skomplikowane i nie należy nic robić na siłę.
Jednak czasami zachodzi potrzeba na więcej i w tym miejscu „z nieba spadło” nieoczekiwane rozwiązanie. Nie że bym tego potrzebował, ale skoro już spadło, to można się nad tym rozwiązaniem pochylić. Skoro się tak zdarzyło, to zapewne tak miało być. Niżej zaprezentowany algorytm wygarnia całą strukturę kartotek z dysku do widoku w TreeView i jest rozwiązaniem rekurencyjnym. Wiadomo, pewne rozwiązania są bardziej optymalne w wariancie iteracyjnym (ot choćby pierwszy z brzegu przykład: wyzerowanie tablicy → wszyscy robią to iteracyjnie, co nie znaczy, że nie da się tego zrobić rekurencyjnie), inne w rekurencyjnym. Cała sztuka polega na właściwej ocenie sytuacji i wybrania odpowiedniego rozwiązania.
Zanim zaprezentuję przykład do rekurencyjnego wygarniania zawartości dysku, chcę podziękować obu inspiratorkom. Chylę czoła przed Nataszą, która podesłała mi swój wariant i Wiktorią, która we właściwy dla siebie sposób zainspirowała do działań, które jakoś tak się potoczyły, że powstały „Rozważania na drzewami”.
Pozwoliłem sobie na pewne modyfikacje plików źródłowych, które dostałem od Nataszy. Nie znaczy, że zawierały jakiekolwiek wady, ot po prostu zaadaptowałem to co przychodzi do potrzeb własnego mikrokosmosu.
W Lazarus utworzony jest program, którego forma wygląda następująco:
treev3-il01.png
Zawiera ona pole do wprowadzenia położenia korzenia drzewa, maski wyświetlanych plików oraz przycisku do odpalenia rakiety.
Sam program wygląda następująco:

Kod: Zaznacz cały

unit treev3unit;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls,
  ComCtrls;

type

  { TTreeViewExample3Form }

  TTreeViewExample3Form = class(TForm)
    FileMaskEdit: TEdit;
    RunButton               : TButton ;
    RootNameEdit            : TEdit ;
    StatusBar               : TStatusBar;
    TreeView                : TTreeView ;
    procedure FormCreate ( Sender : TObject ) ;
    procedure RunButtonClick ( Sender : TObject ) ;
    procedure PopulateTree( LocalRootPath : String ;
                            LocalFileMask : String ;
                            LocalTreeNode : TTreeNode ) ;
  private
    { private declarations }
  public
    DirCounter             : Cardinal ;
    FileCounter            : Cardinal ;
    { public declarations }
  end;

var
  TreeViewExample3Form : TTreeViewExample3Form ;

implementation

{$R *.lfm}

{ TTreeViewExample3Form }

procedure TTreeViewExample3Form.FormCreate ( Sender : TObject ) ;
begin
  Position := poDesktopCenter ;
end ;


procedure TTreeViewExample3Form.PopulateTree( LocalRootPath : String ;
                                              LocalFileMask : String ;
                                              LocalTreeNode : TTreeNode ) ;
var
  FileInfo                        : TSearchRec ;
  Node                            : TTreeNode ;
begin
  (* w pierwszej kolejnosci kartoteki *)
  if FindFirst ( localRootPath + '\*' , faAnyFile or faDirectory , FileInfo ) = 0 then
  begin
    repeat
      if ( FileInfo . Name = '.' ) or ( FileInfo . Name = '..' ) then
        continue ;
      if ( FileInfo . Attr and faDirectory ) = faDirectory then
      begin
        Node := TreeView . Items . AddChild ( localTreeNode , FileInfo . Name ) ;
        DirCounter := succ ( DirCounter ) ;
        PopulateTree ( localRootPath + '\' + FileInfo . Name , LocalFileMask , Node ) ;
      end ;
    until FindNext ( FileInfo ) <> 0 ;
  end ;
  FindClose ( FileInfo ) ;
  (* w drugim obiegu pliki *)
  if FindFirst (localRootPath + '/' + LocalFileMask , faAnyFile or faDirectory , FileInfo ) = 0 then
  begin
    repeat
      if ( FileInfo . Attr and faDirectory ) <> faDirectory then
      begin (* 1 *)
        FileCounter := FileCounter + 1 ;
        TreeView . Items . AddChild ( localTreeNode , FileInfo . Name ) ;
      end (* 1 *) ;
    until FindNext ( FileInfo ) <> 0 ;
  end ;
  FindClose ( FileInfo ) ;
end ;


procedure TTreeViewExample3Form.RunButtonClick ( Sender : TObject ) ;
var
  RootNode                        : TTreeNode ;
begin
  TreeView . Items . Clear ;
  DirCounter := 0 ;
  FileCounter := 0 ;
  RootNode := TreeView . Items . AddFirst ( nil , RootNameEdit . Text ) ;
  PopulateTree ( RootNameEdit . Text , FileMaskEdit . Text , RootNode ) ;
  RootNode . Expanded := True ;
  StatusBar . Panels . Items [ 0 ] . Text := 'Lacznie: ' + IntToStr ( DirCounter ) +
              ' kartotek      oraz: ' + IntToStr ( FileCounter ) + ' plikow' ;
end ;

end.
Idea rozwiązań rekurencyjnych koncepcyjnie jest bardzo prosta, czasem gorzej jest z realizacją, ale to oddzielny problem. W obsłudze kliknięcia przycisku znajduje się (oprócz uwalenia całego dotychczasowego śmietnika zawartego w strukturach TreeView) utworzenie pierwszego węzła (tego bez rodzica). Zmienna RootNode identyfikuje to miejsce. W dalszej kolejności następuje „zaludnianie” drzewa. Do wywołania rekurencyjnej procedury wniesione zostaje: łańcuch znaków wskazujący na kartotekę, która ma zostać wygarnięta, maska dla plików oraz wskazanie na węzeł, do którego wszystko należy podłączyć (wewnątrz już wywołanej procedury).
Ze „środka” procedury PopulateTree wygląda to następująco:

Kod: Zaznacz cały

procedure TTreeViewExample3Form.PopulateTree( LocalRootPath : String ;
                                              LocalFileMask : String ;
                                              LocalTreeNode : TTreeNode ) ;
należy „zrobić dir'a” miejsca na dysku określonego przez LocalRootPath i wygarnąć z tego miejsca kartoteki i wszystkie pliki pasujące do maski (LocalFileMask) oraz każdą dołączaną pozycję podczepić pod ojca identyfikowanego przez LocalTreeNode. Przy pierwszym wywołaniu parametry mogą następujące:
LocalRoothPath = 'e:\microgeek'
LocalFileMask = '*.*'
LocalTreeNode → wskazanie na „sztucznie utworzony” węzeł w obsłudze przycisku.
Kawałek softu:

Kod: Zaznacz cały

  if FindFirst ( localRootPath + '\*' , faAnyFile or faDirectory , FileInfo ) = 0 then
  begin
    repeat
      if ( FileInfo . Name = '.' ) or ( FileInfo . Name = '..' ) then
        continue ;
      if ( FileInfo . Attr and faDirectory ) = faDirectory then
      begin
        Node := TreeView . Items . AddChild ( localTreeNode , FileInfo . Name ) ;
        DirCounter := succ ( DirCounter ) ;
        PopulateTree ( localRootPath + '\' + FileInfo . Name , LocalFileMask , Node ) ;
      end ;
    until FindNext ( FileInfo ) <> 0 ;
  end ;
  FindClose ( FileInfo ) ;
przegląda kartotekę wskazaną w wywołaniu (FindFirst(localRootPath+'\*',faAnyFile or faDirectory,FileInfo)) w poszukiwaniu innych kartotek (zawartych lokalnie). W algorytmie olewane są śmieci w postaci „.” i „..”. Jeżeli aktualna pozycja jest kartoteką (pozycje określające pliki są chwilowo olewane), to tworzony jest nowy węzeł:

Kod: Zaznacz cały

Node := TreeView . Items . AddChild ( localTreeNode , FileInfo . Name ) ;
który jest lokalnie zachowany w zmiennej Node. Dalej następuje rekurencyjne wywołanie, które należy zinterpretować jako: dodaj wszystkie dzieci do powstałego ojca, jakkolwiek głęboko by to sięgało.

Kod: Zaznacz cały

PopulateTree ( localRootPath + '\' + FileInfo . Name , LocalFileMask , Node ) ;
Tu warto zauważyć, że do kolejnego rekurencyjnego wywołania jako wskazanie na kartotekę wniesione są nowe dane powstałe z połączenia tekstu identyfikującego bieżącą kartotekę i nazwy kolejnej kartoteki prowadzącej w głąb drzewa.
Jeżeli program zakończył przeglądanie bieżącej kartoteki w poszukiwaniu elementów „kartotekowych”, przechodzi do drugiej fazy: ponownego przejrzenia bieżącej kartoteki w poszukiwaniu innych „członków rodziny”. Odnalezione klasyczne pliki są „dziećmi”, które należy połączyć z „ojcem” wniesionym z zewnątrz wywołania, z tym, że w tym miejscu do głosu dochodzi maska nazw plików (w fazie poszukiwania kartotek ekwiwalent maski jest '*').

Kod: Zaznacz cały

if FindFirst (localRootPath+'/'+LocalFileMask , faAnyFile or faDirectory , FileInfo ) = 0 then
begin
  repeat
    if ( FileInfo . Attr and faDirectory ) <> faDirectory then
    begin (* 1 *)
      FileCounter := FileCounter + 1 ;
      TreeView . Items . AddChild ( localTreeNode , FileInfo . Name ) ;
    end (* 1 *) ;
  until FindNext ( FileInfo ) <> 0 ;
end ;
FindClose ( FileInfo ) ;
Tu znalezione „potomstwo” jest dołączane bezpośrednio do „ojca”.
Generalnie algorytmy rekurencyjne są proste w swej koncepcji działania, trochę gorzej jest z zakumaniem ich działania, to już nie zawsze jest takie proste. Jak się już wie (przynajmniej troszkę, nie koniecznie wszystko, to wszystko zaczyna wyglądać inaczej). Występuje tu pewna bariera pojęciowa, którą należy pokonać. Wiem, bo sam przez to przeszedłem. Strukturami drzewiastymi zajmuję się od tak dawna, że można powiedzieć, że od zawsze. Może kiedyś najdzie mnie wena pisarska i napiszę coś ciekawego o zwykłych drzewach i b-drzewach (to jest dopiero ciekawy wynalazek).
Teraz wracając do programu... przy okazji „łażenia” po różnych zakamarkach na dysku postanowiłem zliczyć kartoteki i zwykłe pliki. Po przejściu całości wynik jest wyświetlany na dole okienka.
Po uruchomieniu programu tradycyjnie podaję „forumową” kartotekę do „molestowania”.
treev3-il02.png
Wynik tak troszkę mnie przeraził: tysiąc kartotek i ponad dwadzieścia pięć tysięcy plików. O dzizes... nie spodziewałem się.
Rozklikana struktura bieżącego tematu wygląda następująco:
treev3-il03.png
Wprowadziłem maskę, by program wyświetlił tylko pliki zdjęć fotograficznych. Tym razem jest tego znacząco mniej (znaczy liczna kartotek nie uległa zmianie, jest tylko mniej samych plików).
treev3-il04.png
Rozwinięcie pozycji „e:\microgeek\opublikowane” z tematem „kryształowe dziecko”:
treev3-il05.png
Chwila nostalgii, pamiętam dziewczę, które niezwykle mocno pasjonuje się elektroniką, pozostało kilka fotek. Jak odjeżdżała, to zapowiedziała, że chce jeszcze mnie odwiedzić. Zobaczymy... wszystko zależy od niej.
Temat metalowych obudówek, setki zdjęć.
treev3-il06.png

Dla tropicieli:
treeview3.zip
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.

Prawdziwe słowa nie są przyjemne. Przyjemne słowa nie są prawdziwe.
Lao Tse

Awatar użytkownika
tasza
Expert
Expert
Posty: 949
Rejestracja: czwartek 12 sty 2017, 10:24
Kontaktowanie:

Re: [Lazarus] Rozważania o TreeView

Postautor: tasza » niedziela 12 maja 2019, 22:40

No proszę, jak to się ciekawie temat rozwinął. Ładowanie struktury systemu plików do TreeView, czy rekurencyjnie czy jakkolwiek inaczej zmusza kontrolkę ekranową do masy obliczeń na wewnętrzny użytek, dorysowanie kreseczek pomiędzy elementami, ikon (jeżeli są z nimi skojarzone), przy większych ilościach elementów - wykonywane są przeliczenia na suwakach kontrolki (scrollbars).
Aby to wszystko wyłączyć i pozwolić się kontrolce najeść danymi jakby po cichu, bez odświeżania ekranu - wprowadzono dwie komplementarne metody Begin/EndUpdate. Proponuję w metodzie RunButtonClick() na początku samym dostawić TreeView.BeginUpdate; na koniec metody TreeView.EndUpdate; Efekt jest wyraźnie zauważalny przy dużych porcjach danych, sztuczka działa w Lazarus i w Delphi. Polecam, sama korzystam.
___________________________________________ ____ ___ __ _ _ _ _
J​eżeli dadzą ci papier w linie, pisz w poprzek. Juan Ramón Jiménez

Awatar użytkownika
gaweł
Expert
Expert
Posty: 761
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [Lazarus] Rozważania o TreeView

Postautor: gaweł » poniedziałek 13 maja 2019, 15:41

Kolejne funkcje TTreeView

Po ostatnich eksperymentach okazuje się, że rzeczywiście zastosowanie odpowiednich zaklęć daje wymierne efekty. Jak zażądać przetworzenia całej struktury dysku, to soft pierwszy raz się sporo natrudził (czas trwania wyraża się w minutach, tak, że nawet naszła mnie sugestia, czy program nie poszedł gdzieś w maliny, jednak konieczne jest wykazanie się jakąś dozą cierpliwości → wyjdzie). Lampka od dysku się wręcz na stałe włączyła i ani myśli zgasnąć. Jak puścić akcję po raz drugi, to czas już spada do kilkunastu sekund. Ot, dobry algorytm cache do potęga.
Tak na fali drzewiastych ciekawostek, to w komponencie TTreeNode jest fajna funkcja: GetPath, która zwraca w postaci string'u listę ojców zaczynając od korzenia a kończąc na jakimś wybranym elemencie (dziecku): coś takiego jak lista rodzinnych pokoleń. Przykład użycia występuje w obsłudze przycisku „Ścieżka”. Jak wcześniej zostanie kliknięta jakaś pozycja (zostanie zaznaczona), to GetPath wyświetla wynik w linii statusowej na dole okienka. Tu przy okazji pokazane jest jak określić jaki element jest zaznaczony. W TTreeView jest funkcja, która zwraca liczbę zaznaczonych elementów. Jeżeli wartość jest różna od zera, to oznacza, że jest zaznaczony minimum jeden element. Ogólnie TTreeView może pracować w trybie MultiSelect (aczkolwiek w tym konkretnym przypadku tak nie jest). Ze względu na MultiSelect zaznaczone elementy są umieszczone w tablicy Selections, gdyż może być ich wiele. Jest ona indeksowana od zera. Jeżeli jest zaznaczony jeden element, to znajduje się pod indeksem 0. Reasumując: TreeView . Selections [ 0 ] . GetTextPath to podanie własnej nazwy wybranego elementu wraz z listą „ojców”. Ponieważ pozycje w drzewie odzwierciedlają realną strukturę plików na dysku, to wynik funkcji odpowiada prawdziwej nazwie dyskowej wskazanej pozycji. Można tą wiedzę jakoś wykorzystać, przykładowo spróbować przejrzeć traktując to jako plik tekstowy (no nie zawsze jest to prawdziwe i czasami można obejrzeć binarne wnętrzności). Można ewentualnie plik usunąć z dysku.
Więc zmodyfikowany program wygląda następująco:
treev4-il01.png
Program (przy okazji...) po każdym dołączeniu do węzła całej struktury (czyli po wywołaniu rekurencyjnym oraz po pierwszym wywołaniu z obsługi przycisku) dodałem żądanie rozwinięcia danej gałęzi (Node . Expanded := True ;):

Kod: Zaznacz cały

unit treev4unit;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls,
  ComCtrls, treev4fileviewunit;

type

  { TTreeViewExample3Form }

  TTreeViewExample3Form = class(TForm)
    ViewButton              : TButton;
    DelButton               : TButton;
    PathButton              : TButton;
    FileMaskEdit            : TEdit;
    RunButton               : TButton ;
    RootNameEdit            : TEdit ;
    StatusBar               : TStatusBar;
    TreeView                : TTreeView ;
    procedure DelButtonClick(Sender: TObject);
    procedure FormCreate ( Sender : TObject ) ;
    procedure PathButtonClick(Sender: TObject);
    procedure ViewButtonClick ( Sender : TObject ) ;
    procedure RunButtonClick ( Sender : TObject ) ;
    procedure PopulateTree( LocalRootPath : String ;
                            LocalFileMask : String ;
                            LocalTreeNode : TTreeNode ) ;
    function NotSelected : boolean ;
  private
    { private declarations }
  public
    DirCounter             : Cardinal ;
    FileCounter            : Cardinal ;
    { public declarations }
  end;

var
  TreeViewExample3Form : TTreeViewExample3Form ;

implementation

{$R *.lfm}

{ TTreeViewExample3Form }


function TTreeViewExample3Form.NotSelected : boolean ;
begin
  if TreeView . SelectionCount = 0 then
  begin
    ShowMessage ( 'Nie masz zaznaczonej pozycji' ) ;
    NotSelected := true ;
    exit ;
  end ;
  NotSelected := false ;
end ;


procedure TTreeViewExample3Form.FormCreate ( Sender : TObject ) ;
begin
  Position := poDesktopCenter ;
end ;


procedure TTreeViewExample3Form.PopulateTree( LocalRootPath : String ;
                                              LocalFileMask : String ;
                                              LocalTreeNode : TTreeNode ) ;
var
  FileInfo                        : TSearchRec ;
  Node                            : TTreeNode ;
begin
  (* w pierwszej kolejnosci kartoteki *)
  if FindFirst ( localRootPath + '\*' , faAnyFile or faDirectory , FileInfo ) = 0 then
  begin
    repeat
      if ( FileInfo . Name = '.' ) or ( FileInfo . Name = '..' ) then
        continue ;
      if ( FileInfo . Attr and faDirectory ) = faDirectory then
      begin
        Node := TreeView . Items . AddChild ( localTreeNode , FileInfo . Name ) ;
        DirCounter := succ ( DirCounter ) ;
        PopulateTree ( localRootPath + '\' + FileInfo . Name , LocalFileMask , Node ) ;
        Node . Expanded := True ;
      end ;
    until FindNext ( FileInfo ) <> 0 ;
  end ;
  FindClose ( FileInfo ) ;
  (* w drugim obiegu pliki *)
  if FindFirst (localRootPath + '/' + LocalFileMask , faAnyFile or faDirectory , FileInfo ) = 0 then
  begin
    repeat
      if ( FileInfo . Attr and faDirectory ) <> faDirectory then
      begin (* 1 *)
        FileCounter := FileCounter + 1 ;
        TreeView . Items . AddChild ( localTreeNode , FileInfo . Name ) ;
      end (* 1 *) ;
    until FindNext ( FileInfo ) <> 0 ;
  end ;
  FindClose ( FileInfo ) ;
end ;


procedure TTreeViewExample3Form.RunButtonClick ( Sender : TObject ) ;
var
  RootNode                        : TTreeNode ;
begin
  TreeView . BeginUpdate ;
  TreeView . Items . Clear ;
  DirCounter := 0 ;
  FileCounter := 0 ;
  RootNode := TreeView . Items . AddFirst ( nil , RootNameEdit . Text ) ;
  PopulateTree ( RootNameEdit . Text , FileMaskEdit . Text , RootNode ) ;
  RootNode . Expanded := True ;
  StatusBar . Panels . Items [ 1 ] . Text := 'Ka: ' + IntToStr ( DirCounter ) ;
  StatusBar . Panels . Items [ 2 ] . Text := 'Pl: ' + IntToStr ( FileCounter ) ;
  TreeView . EndUpdate ;
end ;


procedure TTreeViewExample3Form.PathButtonClick(Sender: TObject);
begin
  if NotSelected then
    exit ;
  StatusBar . Panels . Items [ 0 ] . Text := TreeView . Selections [ 0 ] . GetTextPath ;
end ;


procedure TTreeViewExample3Form.ViewButtonClick ( Sender : TObject ) ;
begin
  if NotSelected then
    exit ;
  try
    ViewFileForm . TextFileMemo . Lines . LoadFromFile ( TreeView . Selections [ 0 ] . GetTextPath ) ;
  except
    ShowMessage ( 'Ups' ) ;
    exit ;
  end ;
  ViewFileForm . ShowModal ;
end ;


procedure TTreeViewExample3Form.DelButtonClick ( Sender : TObject ) ;
begin
  if NotSelected then
    exit ;
  if not DeleteFile ( TreeView . Selections [ 0 ] . GetTextPath ) then
    ShowMessage ( 'Ups' ) ;
end;


end.
Program odpalony ze wskazaniem na roota dysku daje (po pewnym czasie) następujący wynik. :shock: :shock: :shock: co za śmietnik, ponad 100 tysięcy plików. Tak przewinąłem kawałek i widzę jakieś pliki, których nie kojarzę (no można się zgubić we własnym matriksie).
treev4-il02.png
Klik na przycisk „Ścieżka” i już wiadomo, to pliki od instalki programu QUARTUS ściągniętego ze strony ATERA, wersja 7.2 (przedpotopowa), przewijam, jest tego całe mnóstwo. Po jaką chusteczkę mi to? Uwalić.
treev4-il03.png
Przewijam dalej, jakieś pliki od C (nie moje), klik i … wiadomo, zainstalowany SDCC (tylko to już któraś kopia na tym samym dysku).
Tak zainspirowany działaniami porządkowymi, posprzątałem własne środowisko, wygarnąłem niepotrzebne śmieci. To nie jest jeszcze koniec porządków, trzeba się będzie nad tym pochylić, ale zrobiłem milowy krok, odchudziłem dysk o kilkadziesiąt tysięcy plików.
treev4-il04.png
Normalnie aż lżej się oddycha...

Dla tropicieli:
treeview4.zip
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.

Prawdziwe słowa nie są przyjemne. Przyjemne słowa nie są prawdziwe.
Lao Tse


Wróć do „DIY”

Kto jest online

Użytkownicy przeglądający to forum: Obecnie na forum nie ma żadnego zarejestrowanego użytkownika i 3 gości