Rozdział 7. Obsługa plików

Adam Boduch

Czym jest plik? Tego chyba nie trzeba wyjaśniać żadnemu użytkownikowi komputera. Istnieje kilka rodzajów plików: tekstowe, binarne, typowane itp. Pliki są wykorzystywane przez programy do pobierania lub przechowywania informacji; mogą też zawierać binarne fragmenty programu. Ten rozdział będzie poświęcony plikom i ich obsłudze w Delphi. Zaczniemy od rzeczy najprostszych, przechodząc do coraz bardziej zaawansowanych aspektów. Nie będzie to jednak coś, co przysporzy Ci bólu głowy ? obsługa plików nie jest trudna. Wystarczy tylko znać kilka podstawowych poleceń.

1 Pliki tekstowe
     1.1 Inicjalizacja
     1.2 Tworzenie nowego pliku
     1.3 Otwieranie istniejącego pliku
     1.4 Odczyt plików tekstowych
     1.5 Zapis nowych danych w pliku
     1.6 Zapis danych na końcu pliku
2 Pliki amorficzne
     2.7 Otwieranie i zamykanie plików
     2.8 Tryb otwarcia pliku
     2.9 Zapis i odczyt danych
     2.10 Przykład działania ? kopiowanie plików
3 Inne funkcje operujące na plikach
          3.10.1 FilePos
          3.10.2 FileSize
          3.10.3 Seek
          3.10.4 Truncate
          3.10.5 Rename
          3.10.6 RemoveFile
          3.10.7 Operacje na ścieżkach plików
4 Funkcje operujące na katalogach
          4.10.8 mkDir
          4.10.9 rmDir
          4.10.10 RemoveDirectory
5 Pliki typowane
     5.11 Deklaracja
     5.12 Tworzenie pliku i dodawanie danych
     5.13 Odczyt rekordu z pliku
     5.14 Przykład działania ? książka adresowa
          5.14.11 Projektowanie interfejsu
          5.14.12 Założenia programu
          5.14.13 Procedura ReadFile
          5.14.14 Kasowanie elementu
6 Kopiowanie i przenoszenie plików
     6.15 Kopiowanie
     6.16 Przenoszenie pliku
     6.17 Struktura TSHFileOpStruct
7 Strumienie
     7.18 Podział strumieni
     7.19 Prosty przykład na początek
     7.20 Konstruktor klasy TFileStream
     7.21 Pozostałe metody i właściwości klasy TStream
          7.21.15 Właściwości
                    7.21.15.1 Position
                    7.21.15.2 Size
          7.21.16 Metody
                    7.21.16.3 CopyFrom
                    7.21.16.4 Read, ReadBuffer
                    7.21.16.5 Seek
                    7.21.16.6 Write, WriteBuffer
     7.22 Praktyczny przykład
          7.22.17 Rzut okiem na interfejs programu
          7.22.18 Kod źródłowy programu
8 Wyszukiwanie
     8.23 Rekord TSearchRec
     8.24 Jak zrealizować wyszukiwanie?
          8.24.19 FindFirst
          8.24.20 FindNext
          8.24.21 FindClose
     8.25 Rekurencja
9 Praktyczny przykład
     9.26 Wyszukiwanie plików
10 Informacja o dyskach
     10.27 Pobieranie listy napędów
     10.28 Pobieranie rozmiaru dysku
     10.29 Pobieranie dodatkowych informacji
11 Podsumowanie

W tym rozdziale:
*nauczysz się korzystać z podstawowych funkcji operujących na plikach tekstowych;
*dowiesz się, w jaki sposób można kopiować, kasować czy przenosić swoje pliki;
*nauczysz się korzystać ze strumieni;
*stworzysz swoją pierwszą wyszukiwarkę oraz nauczysz się tworzyć listę plików znajdujących się w danym katalogu;
*dowiesz się, na czym polega algorytm rekurencji.

Pliki tekstowe

Podstawowym rodzajem plików są pliki tekstowe, których budowa jest bardzo prosta. Pliki tekstowe zawierają wyłącznie tekst, którego kolejne wiersze są oddzielone znakami nowego wiersza.

Inicjalizacja

Zanim stworzymy nowy plik lub otworzymy już istniejący, wymagana jest jego inicjalizacja. W rzeczywistości jest to przypisanie konkretnego pliku do jakiejś zmiennej, a realizuje to polecenie AssignFile. Wygląda to tak:

var
  TF : TextFile;
begin
  AssignFile(TF, ?C:\plik.txt?);
  { dalsze operacje }
end;

Wywołanie procedury wbudowanej AssignFile powoduje skojarzenie zmiennej tekstowej ? TX z plikiem C:\plik.txt. Taka konstrukcja jest wymagana, aby rozpocząć dalszą pracę z plikiem. Pierwszy parametr musi być zmienną typu TextFile, a kolejny to ścieżka pliku.

Funkcja AssignFile, jak i inne tego typu należą do funkcji wbudowanych. Oznacza to, że ich deklaracja znajduje się w module System, który jest automatycznie włączany do każdego projektu.

Tworzenie nowego pliku

Za tworzenie nieistniejącego wcześniej pliku odpowiada funkcja Rewrite. Jest to również funkcja systemowa, której nagłówek przedstawia się następująco:

procedure Rewrite(var F: File [; Recsize: Word ] );

Pierwszym parametrem musi być nazwa zmiennej plikowej ? w naszym przypadku będzie to TextFile. Drugim parametrem na razie się nie przejmuj, gdyż jest on opcjonalny. Tak się składa, że jedna funkcja (w tym wypadku ? Rewrite) może być wykorzystywana do otwierania kilku rodzajów plików ? w takiej właśnie sytuacji jest wykorzystywany ten drugi parametr.
Oto przykład utworzenia nowego pliku tekstowego:

procedure TMainForm.btnRewriteClick(Sender: TObject);
var
  TF : TextFile;
begin
  AssignFile(TF, 'C:\plik.txt');
  try
    Rewrite(TF);
  finally
    CloseFile(TF);
  end;
end;

Na wszelki wypadek cały kod umieściłem w bloku try..finally. Dzięki temu zawsze ? bez względu na to, czy podczas tworzenia pliku wystąpi jakiś błąd ? wykonana zostanie instrukcja CloseFile, zwalniająca zmienną TF.

Listing powyższego programu możesz znaleźć na dołączonej do książki płycie CD-ROM w katalogu ..listingi/7/Create File.

Otwieranie istniejącego pliku

Jeżeli jakiś plik tekstowy już istnieje, to w celu dalszej edycji należy go otworzyć poleceniem Reset.

procedure Reset(var F [: File; RecSize: Word ] );

Pierwszy parametr to ? tak samo jak w przypadku polecenia Rewrite ? nazwa zmiennej typu TextFile. Drugi parametr jest parametrem opcjonalnym, identycznie jak w przypadku funkcji Rewrite.

procedure TMainForm.btnResetClick(Sender: TObject);
var
  TF : TextFile;
begin
  AssignFile(TF, 'C:\plik2.txt');
  try
    Reset(TF);
  finally
    CloseFile(TF);
  end;
end;

Należy uważać na to, czy plik, który próbujemy otworzyć, istnieje. W przeciwnym wypadku program wygeneruje wyjątek EFileNotFound, co zazwyczaj skończy się wyświetleniem komunikatu o błędzie. W takiej sytuacji przydaje się funkcja FileExists, która sprawdza istnienie podanego w parametrze pliku.

if FileExists('C:\plik.txt')
  Reset(TF)
else Rewrite(TF);

Jeżeli plik istnieje, zostanie otwarty, a w przeciwnym wypadku ? utworzony.

Odczyt plików tekstowych

Pamiętasz, jak we wcześniejszych fragmentach książki (a dokładnie w rozdziale drugim) wspominałem o funkcjach Read i Readln? Te dwie funkcje służą także do odczytywania informacji z plików tekstowych. Spójrz na poniższy fragment kodu:

procedure TMainForm.btnResetClick(Sender: TObject);
var
  TF : TextFile;
  S : String; // zmienna tymczasowa
begin
  AssignFile(TF, 'C:\plik.txt');
  try
    Reset(TF);
    { pętla odczytuje kolejne wiersze pliku tekstowego }
    while not Eof(TF) do
    begin
      Readln(TF, S); // odczytanie wierszy i przypisanie zawartości do zmiennej S
      memFile.Lines.Add(S);
    end;
  finally
    CloseFile(TF);
  end;
end;

end.

Oprócz użycia standardowych procedur Read i Readln skorzystałem także z funkcji Eof. Funkcja Eof informuje o napotkaniu końca pliku podczas odczytywania jego zawartości. A zatem pętla w powyższym kodzie będzie wykonywana, dopóki nie zostaną odczytane wszystkie wiersze pliku tekstowego.

Pierwszym parametrem procedury Readln musi być nazwa zmiennej typu TextFile. Drugi parametr to zmienna typu String, do której przypisana zostanie zawartość wiersza.

Po uruchomieniu programu zostanie wyświetlone okno przedstawione na rysunku 7.1.

7.1.jpg
Rysunek 7.1. Zawartość pliku wczytana do komponentu TMemo

Prawdopodobnie rzadko będziesz używał takiego zapisu, gdyż VCL posiada własne funkcje służące do przetwarzania plików tekstowych. Zamiast stosować ten ? dość skomplikowany ? zapis można wykorzystać taki sposób:

Memo.Lines.LoadFromFile('C:\plik.txt');

Okazuje się, że jednym wierszem kodu można zastąpić szereg wbudowanych instrukcji języka Object Pascal.

Zapis nowych danych w pliku

Gdy chcemy zapisać nowe dane w pliku, należy skorzystać z funkcji Writeln i Write. Wspominałem o nich już w rozdziale drugim, ale tym razem ich użycie ? choć podobne ? nieznacznie się różni.

Pierwszy parametr musi być wskazaniem na zmienną typu TextFile, natomiast parametr drugi to zmienna typu String.

Writeln(TF, S);

Oto przykład na udoskonalenie programu, który zaprezentowany został w poprzednim podpunkcie. Tym razem procedura umożliwia zapis danych z komponentu TMemo:

procedure TMainForm.btnSaveClick(Sender: TObject);
var
  TF : TextFile;
  i : Integer;
begin
  AssignFile(TF, 'C:\plik.txt');
  try
    Rewrite(TF);
    for I := 0 to memFile.Lines.Count ?1 do
      Writeln(TF, memFile.Lines[i]);

  finally
    CloseFile(TF);
  end;
end;

Powyższy kod powoduje zapisanie wszystkich wierszy z komponentu TMemo do pliku tekstowego. Właściwość Count klasy TStrings (właściwość Lines komponentu TMemo jest typu TStrings! Tak, całe VCL jest ze sobą połączone!) zwraca ilość wierszy znajdujących się w komponencie.

Zamiast instrukcji Writeln możesz zastosować także Write. Różnica pomiędzy tymi dwoma poleceniami polega na tym, że to drugie polecenie nie dodaje na końcu tekstu znaku nowego wiersza. W konsekwencji tekst nie będzie podzielony na wiersze, ale wszystko zostanie zapisane w jednym ciągu.

Zapis danych na końcu pliku

Niekiedy przytrafia się sytuacja, w której chcemy zapisać jakieś dane do pliku, ale umieszczając je tylko na jego końcu. W takim wypadku przydatna staje się funkcja Append. Za jej pomocą możliwe jest otwarcie pliku i ustawienie punktu zapisu na jego końcu.

W praktyce wygląda to tak:

procedure TMainForm.btnAppendClick(Sender: TObject);
var
  TF : TextFile;
begin
  AssignFile(TF, 'C:\plik.txt');
  try
    Append(TF);
    Writeln(TF, edtText.Text);
  finally
    CloseFile(TF);
  end;
end;

Polecenie Append jednocześnie powoduje otwarcie pliku ? nie jest więc konieczne wcześniejsze zastosowanie procedury Reset.

Nie istnieją funkcje umożliwiające przemieszczanie się po plikach tekstowych w przód lub w tył.

Pliki amorficzne

W tym rozdziale plikami amorficznymi będę nazywał pliki o nieregularnej budowie, czyli pliki binarne. Napisałem nieregularnej, gdyż pliki amorficzne nie mają określonej budowy, a ich poszczególne wiersze nie są zakończone znakiem nowego wiersza czy jakimś innym specyficznym znakiem.

Obsługa plików binarnych może się przydać do odczytu fragmentów danych, ich zapisu lub skopiowania do innego pliku. Przykładem może być odczyt części danych z pliku mp3 (tzw. tag ? informacja o wykonawcy, tytule piosenki itp.). Obsługa tych operacji z poziomu Delphi jest raczej prosta; nazwy funkcji są takie same, jak w przypadku plików testowych, choć istnieją polecenia specyficzne właśnie dla plików amorficznych.

Otwieranie i zamykanie plików

Otwieranie oraz zamykanie pliku amorficznego realizowane jest także przez funkcje AssignFile, Reset, CloseFile lub Rewrite.

Polecenie Append, prezentowane w poprzednim podpunkcie, może być używane jedynie w kontekście plików tekstowych.

Oto przykładowe otwarcie i zamknięcie wybranego przez użytkownika pliku:

procedure TMainForm.btnOpenClick(Sender: TObject);
var
  F : File;
begin
{ jeżeli otwarte zostanie okno i wybrany plik }
  if OpenDialog.Execute then
  begin
    AssignFile(F, OpenDialog.FileName);
    try
      try
        Reset(F, 1); // otwórz plik
        ShowMessage('Plik został otwarty');
      except
      { jeżeli wystąpi błąd ? wyświetl wyjątek }
        raise;
      end;
    finally
      CloseFile(F); // zamknij plik
    end;
  end;
end;

W odniesieniu do plików amorficznych należy skorzystać ze zmiennej typu File, a nie ? jak w poprzednich przykładach ? TextFile. Procedura Reset z poprzedniego przykładu posiada drugi parametr (opcjonalny), który określa wielkość rekordu używanego podczas operacji na plikach. Drugi parametr ? Reset ? ma znaczenie jedynie w kontekście plików amorficznych. W przypadku, gdy parametr ten jest pusty, Delphi za domyślną wartość uzna 128, co może spowodować nieprawidłowe działanie programu. Dlatego też jako bezpieczną wartość należy w tym miejscu podawać cyfrę 1.

Tryb otwarcia pliku

Język Object Pascal zawiera zmienną globalną FileMode, która definiuje sposób zapisu lub odczytu danych. Mówiąc innymi słowami, określa ona prawa dostępu do pliku. Domyślna wartość tej zmiennej to fmOpenReadWrite, czyli możliwość zarówno odczytu, jak i zapisu danych w pliku. Warto poznać tę zmienną już teraz, gdyż wiedza ta przyda się w dalszej części rozdziału, gdy będziemy mówili o strumieniach.

Zmienna FileMode określa sposób dostępu do plików amorficznych ? nie tekstowych!

Tabela 7.1 zawiera wartości, które mogą być przypisane do zmiennej FileMode lub które będą wykorzystywane podczas omawiania strumieni.

Tabela 7.1. Wartości określające dostęp do pliku

WartośćOpis
fmCreateJeżeli plik nie istnieje ? zostanie utworzony; w przeciwnym wypadku zostanie otwarty
fmOpenReadPlik zostanie otwarty jedynie do odczytu
fmOpenWritePlik zostanie otwarty do zapisu
fmOpenReadWritePlik zostanie otwarty zarówno do zapisu, jak i do odczytu
fmShareExclusiveDostęp do pliku jest niemożliwy z poziomu innych programów
fmShareDenyWriteZapis z poziomu innych programów jest zabroniony
fmShareDenyReadOdczyt z poziomu innych programów jest zabroniony
fmShareDenyNonePełny dostęp do pliku dla innych aplikacji

Zapis i odczyt danych

W celu odczytania jakiejś ilości danych lub zapisania określonej porcji informacji należy skorzystać z funkcji BlockWrite i BlockRead. Obie funkcje wymagają podania bufora, czyli danych, które planujemy zapisać w pliku.

Spójrz na poniższy fragment kodu:

procedure TForm1.Button1Click(Sender: TObject);
var
  F : File;
  Buffer : array[0..255] of char;
begin
  AssignFile(F, 'C:\dane.txt');
  try
    Rewrite(F, 1);
    Buffer := 'Jakieś dane...';
    BlockWrite(F, Buffer, SizeOf(Buffer));
  finally
    CloseFile(F);
  end;
end;

W procedurze zadeklarowane zostały dwie zmienne: F, która określa plik amorficzny, oraz tablica Buffer (255 elementowa). Utworzenie pliku przebiega standardowo (Rewrite). Następnie do tablicy Buffer (czyli naszego bufora) zostają przypisane dane. Kolejnym krokiem jest zapisanie danych w pliku za pomocą procedury BlockWrite.

Pierwszym parametrem owej procedury musi być nazwa zmiennej typu File. Kolejny parametr to nazwa bufora, a trzeci to rozmiar danych zapisywanych w pliku. Funkcja SizeOf zwraca rzeczywisty rozmiar, w tym wypadku ? tablicy.

Po uruchomieniu programu na dysku powstanie plik ? na pozór ? tekstowy. Sprawdź jednak jego rozmiar. Okaże się, że niepozorny plik tekstowy zajmuje aż 256 bajtów! Jest to spowodowane tym, że tablica Buffer zajmuje 256 bajtów (pamiętaj ? 0 też jest elementem tablicy).

Przykład działania ? kopiowanie plików

Kopiowanie w gruncie rzeczy opiera się na pobieraniu jakiegoś fragmentu pliku i dodawaniu tego fragmentu to pliku drugiego.

Procedury BlockWrite oraz BlockRead posiadają jeszcze jeden, opcjonalny parametr, który określa, ile rzeczywiście bajtów zostało odpowiednio odczytanych lub zapisanych. Ten parametr przyda nam się podczas projektowania procedury kopiującej plik.

Wygląd przykładowego programu służącego do kopiowania przedstawiony został na rysunku 7.2.

7.2.jpg
Rysunek 7.2. Wygląd programu

Po naciśnięciu przycisku otwarte zostanie okno, w którym użytkownik będzie musiał wskazać ścieżkę do pliku, z którego zostanie utworzona kopia (na dysku C:). Przebieg procesu kopiowania pliku zostanie przedstawiony na pasku postępu (komponent TProgressBar).

Kod źródłowy programu przedstawiony jest na listingu 7.1. Po zapoznaniu się z nim przeczytaj zamieszczone poniżej omówienie.

Listing 7.1. Kopiowanie plików

unit MainFrm;

interface

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

type
  TMainForm = class(TForm)
    OpenDialog: TOpenDialog;
    GroupBox1: TGroupBox;
    lblFile: TLabel;
    pbCopy: TProgressBar;
    btnCopy: TButton;
    procedure btnCopyClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.btnCopyClick(Sender: TObject);
var
  SrcFile, DstFile : File; { plik źródłowy i plik przeznaczenia }
  FSize : Integer;       { rozmiar kopiowanego pliku }
  Bytes : Integer;       { ilość odczytanych danych }
  Buffer : array[0..255] of byte;  { bufor przechowujący dane }
  TotalSize : Integer;     { ilość skopiowanych już bajtów }
begin
  if OpenDialog.Execute then
  begin
  { wyświetl na etykiecie ścieżkę kopiowanego pliku }
    lblFile.Caption := 'Plik ' + OpenDialog.FileName;
    AssignFile(SrcFile, OpenDialog.FileName);
    try
      Reset(SrcFile, 1); { otwórz plik }
      FSize := FileSize(SrcFile); { odczytaj rozmiar pliku }
      pbCopy.Max := (FSize div 1000);  { maksymalna pozycja na pasku postępu }

      AssignFile(DstFile, 'C:\' + ExtractFileName(OpenDialog.FileName) + '~');
      try
      { utwórz plik }
        Rewrite(DstFile, 1);

        repeat
          Application.ProcessMessages;

          { odczytaj dane }
          BlockRead(SrcFile, Buffer, SizeOf(Buffer), Bytes);
          if Bytes > 0 then  { jeżeli liczba odczytanych bajtów jest większa od 0 }
          begin
          { przypisz odczytane dane do pliku }
            BlockWrite(DstFile, Buffer, Bytes);
            TotalSize := TotalSize + Bytes;
          end;
          { pozycja na pasku postępu }
          pbCopy.Position := (TotalSize div 1000);
          
        until Bytes = 0;

      finally
        CloseFile(DstFile);
      end;

    finally
      CloseFile(SrcFile);
    end;
  end;
end;

end.

Kod może wydać się nieco skomplikowany. W rzeczywistości polega po prostu na odczytywaniu w pętli kolejnych porcji danych.

Na samym początku następuje otwarcie pliku do skopiowania i utworzenie jego kopii. Zauważ, że wykorzystałem funkcję ExtractFileName. Służy ona do pobrania samej nazwy pliku z pełnej ścieżki.

Wcześniej zastosowałem nie omawiana jeszcze funkcję FileSize, która służy do pobierania rozmiaru otwartego pliku.

W programie do kopiowania danych użyłem pętli repeat..until, ponieważ wymagane jest co najmniej jednokrotne wykonanie pętli (co gwarantuje właśnie ta pętla). Fragment danych przypisywany jest do zmiennej Buffer, a ilość rzeczywiście odczytanych danych ? do zmiennej Bytes. Musiałem skorzystać z takiej konstrukcji z jednego powodu. Ilość odczytanych danych nie zawsze musi wynosić 255 bajtów (rozmiar tablicy) ? może być to mniejsza wartość, np. w przypadku, gdy rozmiar pliku nie jest zaokrąglony do 255 bajtów (co zdarza się bardzo rzadko i jest kwestią przypadku). Zapisanie w ten sposób odczytanej ?paczuszki? jest możliwe dzięki następującemu wierszowi kodu:

BlockWrite(DstFile, Buffer, Bytes);

Drugi parametr (Buffer) jest wskazaniem bufora, a trzeci parametr to rozmiar, czyli ilość odczytanych danych ? zmienna Bytes.

Pętla wykonywana jest dopóty, dopóki rozmiar odczytanych danych nie wynosi 0, co oznaczałoby, że cała zawartość pliku została odczytana.

Pełny kod źródłowy powyższego programu możesz znaleźć na płycie CD-ROM w katalogu ..listingi/7/Copy File.

W rezultacie powyższy przykład był jedynie zaprezentowaniem możliwości procedur BlockWrite i BlockRead, gdyż do kopiowania równie dobrze można użyć funkcji CopyFile ? o tym powiemy jednak w dalszej części rozdziału.

Inne funkcje operujące na plikach

Należy wspomnieć o kilku funkcjach, z których nieraz możesz skorzystać podczas operowania na plikach.

FilePos

function FilePos(var F): Longint;

Funkcja FilePos zwraca rezultat w postaci pozycji, której właśnie tyczą się operacje. W parametrze należy podać oczywiście nazwę zmiennej typu File.

FileSize

function FileSize(var F): Integer;

Funkcja podaje rozmiar (w bajtach) otwartego pliku:

var
  F : File;
begin
  AssignFile(F);
  Reset(F, 1);
  Label.Caption := 'Rozmiar pliku: ' + IntToStr(FileSize(F));
  CloseFile(F);
end;

Seek

procedure Seek(var F; N: Longint);

Procedura Seek (jak zresztą wszystkie powyższe funkcje) działa jedynie w odniesieniu do plików amorficznych i służy do przechodzenia do określonego miejsca pliku (definiowanego poprzez parametr N).

Truncate

procedure Truncate(var F);

Procedura Truncate służy do przycinania pliku. Podany w parametrze F plik zostaje obcięty od konkretnego miejsca do końca.

procedure TForm1.Button1Click(Sender: TObject);
var
  F : File;
begin
  AssignFile(F, 'C:\dane.txt');
  try
    Reset(F, 1);
    Seek(F, FileSize(F) div 2); // przejście na środek pliku
    Truncate(F);
  finally
    CloseFile(F);
  end;
end;

Powyższy kod powoduje skrócenie pliku dane.txt o połowę. Najpierw po otwarciu przechodzimy do środka pliku (wiem, że to brzmi trochę abstrakcyjnie), by później usunąć wszystkie dane znajdujące się poniżej.

Rename

procedure Rename(var F; Newname: string);
procedure Rename(var F; Newname: PChar);

Łatwo można się domyśleć, że te dwie procedury (przeciążane) służą do zmiany nazwy pliku. Pierwszym parametrem musi być zmienna typu File, a drugi parametr to nazwa nowego pliku.

RemoveFile

procedure RemoveFile(const AFileName: string);

W celu skasowania pojedynczego pliku można wywołać procedurę RemoveFile. Parametr AFileName powinien zawierać ścieżkę do pliku, który chcemy skasować.

Operacje na ścieżkach plików

W module SysUtils znajduje się kilka bardzo użytecznych funkcji, pozwalających na manipulowanie ścieżką pliku. Pisząc ?manipulowanie? mam na myśli uzyskiwanie ze ścieżki nazwy pliku, rozszerzenia czy katalogu, w którym plik się znajduje. Funkcje te opisałem w tabeli 7.2.

Tabela 7.2. Funkcje do uzyskiwania danych ze zmiennej String

FunkcjaOpis
ExtractFileDirZ kompletnej ścieżki pliku pobiera jedynie nazwę katalogu, w którym znajduje się plik ? np.: C:\Windows\System
ExtractFileDriveFunkcja ze ścieżki zwraca jedynie literę dysku, na którym znajduje się dany plik
ExtractFileExtFunkcja zwraca rozszerzenie pliku
ExtractFileNameZ podanej w parametrze ścieżki zwracana jest jedynie nazwa pliku
ExtractFilePathFunkcja działa podobnie jak ExtractFileDir, z tą różnicą, że zwraca nazwę katalogu ze znakiem \ na końcu: C:\Windows\System\
ExtractShortPathNameZwraca skróconą ścieżkę ? np. C:\Progra~1\MyComp~1\MyApp\MyApp.exe

Funkcje operujące na katalogach

W Delphi w prosty sposób możemy operować na katalogach, korzystając z poniższych funkcji. Co prawda nie ma ich dużo, ale do podstawowych operacji całkowicie wystarczą.

mkDir

procedure MkDir(const S: string); overload;
procedure MkDir(P: PChar); overload;

Procedura mkDir powinna być znana osobom, które wcześniej programowały w Turbo Pascalu. Umożliwia utworzenie katalogu określonego w parametrze S lub P (funkcja przeciążona).

procedure TForm1.Button1Click(Sender: TObject);
begin
  mkDir('C:\folder');
end;

rmDir

procedure RmDir(const S: string); overload;
procedure RmDir(P: PChar); overload;

Nazwa funkcji jest podobna do powyższej z tą różnicą, że owa funkcja ? rmDir (skrót od Remove Directory) ? powoduje usunięcie katalogu.

procedure TForm1.Button1Click(Sender: TObject);
begin
  rmDir('C:\folder');
end;

RemoveDirectory

function RemoveDir(const Dir: string): Boolean;

Funkcja RemoveDir także służy do usunięcia podanego w parametrze katalogu. W odróżnieniu od polecenia rmDir funkcja ta jest zawarta w module SysUtils (natomiast rmDir jest funkcją systemową) i zwraca False w przypadku, gdy nie uda się usunąć katalogu.

procedure TForm1.Button1Click(Sender: TObject);
begin
  RemoveDir('C:\folder');
end;

Funkcja nie zwraca komunikatu o błędzie w razie nieudanej próby usunięcia, tak jak to ma miejsce w przypadku rmDir.

Oba polecenia ? RemoveDir oraz rmDir ? nie usuną katalogu, w którym znajdują się pliki lub inne foldery. W takim przypadku należy usunąć pojedynczo wszystkie pliki znajdujące się w owym folderze, a dopiero później sam katalog. Wiedzę potrzebną do wykonania tego zadania zdobędziesz w dalszej części rozdziału.

Pliki typowane

Pliki typowane są kolejnym rodzajem plików; mogą okazać się bardzo przydatne w pisaniu programów. Dotąd poznałeś dwa rodzaje plików: tekstowe (dające się podzielić na wiersze) oraz binarne ? o nieregularnej budowie. Pliki typowane mogą zawierać dane o regularnym układzie ? np. całe rekordy danych. Rekordy możesz dowolnie odczytywać lub zapisywać. W ten sposób możesz stworzyć nawet prostą bazę danych.

Deklaracja

Deklaracja plików typowanych przebiega niestandardowo ? np. tak:

var
  MyFile : File of TRecord

;

Od tej pory zmienna MyFile definiuje nowy typ plików, które składać się będą z rekordów TRecord. Aby dopełnić procesu deklaracji nowego typu, należy określić jeszcze strukturę rekordu TRecord.

type
  TRecord = packed record
    Imie : String[20];
    Nazwisko : String[20];
    Wiek : Byte;
  end;

procedure TForm1.Button1Click(Sender: TObject);
var
  MyFile : file of TRecord;
begin

end;

end.

Od tej pory w plikach będzie można zamieszczać, a następnie odczytywać całe rekordy TRecord. Umożliwia nam to łatwe gromadzenie danych potrzebnych np. w trakcie działania programu.

Tworzenie pliku i dodawanie danych

Oto pierwsza zasada: wraz z plikami typowanymi nie może zostać użyte polecenie Writeln, a jedynie Write. W przeciwnym wypadku kompilator wyświetli komunikat o błędzie: [Error] MainFrm.pas(44): Illegal type in Write/Writeln statement.

Utworzenie pliku typowanego i dodanie do niego danych (rekordu) może wyglądać tak:

type
{ deklaracja rekordu }
  TRecord = packed record
    FName : String[30];
    SName : String[30];
    Age : Byte;
  end;

procedure TMainForm.btnCreateClick(Sender: TObject);
var
  F: file of TRecord;
  Rec : TRecord;
begin
{ wypełnienie rekordu danymi }
  Rec.FName := 'Piotr';
  Rec.SName := 'Nowak';
  Rec.Age := 89;
  AssignFile(F, 'dane.dat');
  try
    Rewrite(F);  // utworzenie pliku
    Write(F, Rec); // dodanie rekordu
  finally
    CloseFile(F);
  end;
end;

Tym razem plik zostanie zapisany w katalogu, w którym umieszczony jest program. Po uruchomieniu programu i wykonaniu powyższej procedury do pliku zostanie dodany rekord.

Odczyt rekordu z pliku

Zapisane rekordy można odczytać z pliku za pomocą procedury Read ? w podobny sposób jak przy odczytywaniu danych z innych rodzajów plików.

Oto przykład:

procedure TMainForm.Button1Click(Sender: TObject);
var
  F: file of TRecord;
  Rec : TRecord;
begin
  AssignFile(F, 'dane.dat');
  try
    Reset(F);
    Read(F, Rec);
   { rekord Rec zawiera informacje wczytane z pliku }
  finally
    CloseFile(F);
  end;
end;

Przykład działania ? książka adresowa

Abyś mógł lepiej utrwalić sobie wiadomości dotyczące plików typowanych, w tym punkcie przedstawię ćwiczenie, którego celem będzie stworzenie prostej książki adresowej, opartej na plikach typowanych.
Nasz program będzie zawierał proste funkcje, takie jak dodanie kontaktu oraz usunięcie go z pliku.
Program przedstawiony jest na rysunku 7.3.

7.3.jpg
Rysunek 7.3. Program ? książka adresowa

Projektowanie interfejsu

Nasz program składać się będzie z dwóch formularzy: jeden przedstawiony jest na rysunku 7.3, drugi natomiast to formularz służący do dodawania nowego pola w komponencie typu TListView.

Komponent TListView jest podzielony na kolumny, a to za sprawą właściwości ViewStyle, której nadano wartość vsReport. Tworzenie kolumn odbywa się za pośrednictwem właściwości Columns.

Obiekt TToolBar spełnia rolę paska narzędziowego ? na nim znajdują się dwa przyciski, tworzone za pomocą polecenia New Button z menu podręcznego owego komponentu.

Warto ustawić wartość właściwości AutoSize komponentu TToolBar na True, co pozwoli dopasować rozmiar paska narzędziowego do przycisków na nim się znajdujących.

Wyświetlanie etykiet tekstowych na przyciskach paska narzędziowego jest możliwe za pośrednictwem właściwości ShowCaptions.

Rysunek 7.4 przedstawia drugi formularz programu, wyświetlany podczas próby dodania nowego rekordu do naszej bazy.

7.4.jpg
Rysunek 7.4. Formularz służący do dodawania rekordów do bazy

Formularz zawiera kilka kontrolek typu TEdit, w których muszą się znaleźć dane do zapisania w bazie ? nie stanowi to nic nadzwyczajnego.

Założenia programu

Program ma być prostą bazą danych, składającą się z paru rekordów. Po każdym uruchomieniu programu wywoływana jest procedura ReadFile, która ma na celu odczytanie rekordów znajdujących się w pliku.

Po każdorazowym dodaniu rekordu zawartość pliku jest ponownie wyświetlana w komponencie TListView ? znowu następuje wywołanie procedury ReadFile.

Procedura ReadFile

Zadaniem procedury ReadFile jest odczytanie z pliku wszystkich rekordów, a następnie dodanie ich kolejno do komponentu TListView:

procedure TMainForm.ReadFile;
var
  F : TAddressFile;
  i : Integer;
  ListItem : TListItem;
  Rec : TAddress;
begin
  lvAddress.Clear;
  { anuluj, jeżeli plik z danymi nie istnieje }
  if not FileExists('file.dat') then Exit;
  
  AssignFile(F, 'file.dat');
  try
    Reset(F); // utwórz plik

    for I := 0 to FileSize(F) ?1 do
    begin
      Read(F, Rec);  // w pętli otwórz kolejne rekordy
      ListItem := lvAddress.Items.Add; // dodaj rekord do komponentu
      ListItem.Caption := Rec.Name;
      ListItem.SubItems.Add(IntToStr(Rec.Tel1));
      ListItem.SubItems.Add(IntToStr(Rec.Tel2));
      ListItem.SubItems.Add(Rec.Mail);
    end;

  finally
    CloseFile(F);
  end;
end;

Jeżeli jest to pierwsze uruchomienie programu lub plik z danymi nie istnieje, kod procedury zostaje pominięty (metoda Exit ? anulowanie wykonywania dalszej części kodu).

W dalszej części programu po otwarciu pliku następuje otwieranie w pętli kolejnych rekordów. W tym przypadku funkcja FileSize zwraca ilość rekordów znajdujących się w pliku, tak więc w każdej iteracji wykonywane jest odczytanie zawartości do rekordu TAddress. Wówczas nie pozostaje już nic innego, jak dodać zawartość owego rekordu do komponentu TListView.

Dodawanie elementu do komponentu odbywa się za pośrednictwem zmiennej typu TListItem. Do tego rekordu należy przypisać dane, które mają zostać dodane do komponentu TListView.

Kasowanie elementu

Object Pascal nie posiada procedury umożliwiającej kasowanie konkretnego rekordu z pliku ? należy tę funkcję zaprogramować samemu. Jak to wygląda w praktyce? Należy w miejsce pliku z bazą danych utworzyć pusty plik i do niego dodawać kolejne rekordy, odczytane z komponentu TListView, pomijając rekord, który został zaznaczony do usunięcia.

procedure TMainForm.btnRemoveClick(Sender: TObject);
var
  F : TAddressFile;
  Rec : TAddress;
  i : Integer;
begin
  AssignFile(F, 'file.dat');
  try
    Rewrite(F);  // utworzenie pliku i skasowanie poprzedniej zawartości

    for I := 0 to lvAddress.Items.Count ?1 do
    begin
    { jeżeli wykonywana iteracja nie ma numeru takiego samego, jak zaznaczony element }
      if I <> lvAddress.Selected.Index then
      begin
      { dodaj zawartość kolejnego elementu do pliku }
        Rec.Name := lvAddress.Items[i].Caption;
        Rec.Tel1 := StrToInt(lvAddress.Items[i].SubItems[0]);
        Rec.Tel2 := StrToInt(lvAddress.Items[i].SubItems[1]);
        Rec.Mail := lvAddress.Items[i].SubItems[2];
        Write(F, Rec);
      end;
    end;
  finally
    CloseFile(F);
    ReadFile; // odśwież zawartość komponentu
  end;
end;

Budowa tej procedury jest w gruncie rzeczy prosta ? jest to dodawanie poszczególnych elementów z komponentu TListView do pliku, z pominięciem elementu zaznaczonego.

W listingu 7.2 przedstawiony został kod źródłowy całego modułu MainFrm.pas, a listing 7.3 zawiera kod źródłowy modułu drugiego, służącego do dodawania nowych pól.

Listing 7.2. Kod źródłowy formularza MainFrm.pas

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ComCtrls, ToolWin, ImgList;

type
  TMainForm = class(TForm)
    ToolBar1: TToolBar;
    btnAdd: TToolButton;
    btnRemove: TToolButton;
    ImageList1: TImageList;
    StatusBar: TStatusBar;
    lvAddress: TListView;
    procedure FormCreate(Sender: TObject);
    procedure btnAddClick(Sender: TObject);
    procedure btnRemoveClick(Sender: TObject);
  private
    { Private declarations }
  public
    procedure ReadFile;
  end;

  TAddress = packed record
    Name : String[30];
    Tel1 : Integer;
    Tel2 : Integer;
    Mail : String[30];
  end;

  TAddressFile = file of TAddress;

var
  MainForm: TMainForm;

implementation

uses AddFrm;

{$R *.dfm}

{ TMainForm }

procedure TMainForm.ReadFile;
var
  F : TAddressFile;
  i : Integer;
  ListItem : TListItem;
  Rec : TAddress;
begin
  lvAddress.Clear;
  { anuluj, jeżeli plik z danymi nie istnieje }
  if not FileExists('file.dat') then Exit;
  
  AssignFile(F, 'file.dat');
  try
    Reset(F); // utwórz plik

    for I := 0 to FileSize(F) ?1 do
    begin
      Read(F, Rec);  // w pętli otwórz kolejne rekordy
      ListItem := lvAddress.Items.Add; // dodaj rekord do komponentu
      ListItem.Caption := Rec.Name;
      ListItem.SubItems.Add(IntToStr(Rec.Tel1));
      ListItem.SubItems.Add(IntToStr(Rec.Tel2));
      ListItem.SubItems.Add(Rec.Mail);
    end;

  finally
    CloseFile(F);
  end;
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
{ podczas otwierania programu wywołaj procedurę }
  ReadFile;
end;

procedure TMainForm.btnAddClick(Sender: TObject);
begin
  AddForm := TAddForm.Create(Application);
  AddForm.ShowModal;
  AddForm.Free;
end;

procedure TMainForm.btnRemoveClick(Sender: TObject);
var
  F : TAddressFile;
  Rec : TAddress;
  i : Integer;
begin
  AssignFile(F, 'file.dat');
  try
    Rewrite(F);  // utworzenie pliku i skasowanie poprzedniej zawartości

    for I := 0 to lvAddress.Items.Count ?1 do
    begin
    { jeżeli wykonywana iteracja nie ma numeru takiego samego, jak zaznaczony element }
      if I <> lvAddress.Selected.Index then
      begin
      { dodaj zawartość z kolejnego elementu do pliku }
        Rec.Name := lvAddress.Items[i].Caption;
        Rec.Tel1 := StrToInt(lvAddress.Items[i].SubItems[0]);
        Rec.Tel2 := StrToInt(lvAddress.Items[i].SubItems[1]);
        Rec.Mail := lvAddress.Items[i].SubItems[2];
        Write(F, Rec);
      end;
    end;
  finally
    CloseFile(F);
    ReadFile; // odśwież zawartość komponentu
  end;
end;

end.

Listing 7.3. Kod źródłowy formularza AddFrm.pas

unit AddFrm;

interface

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

type
  TAddForm = class(TForm)
    GroupBox1: TGroupBox;
    btnAdd: TBitBtn;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    edtName: TEdit;
    edtTel1: TEdit;
    edtTel2: TEdit;
    edtAddress: TEdit;
    procedure btnAddClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  AddForm: TAddForm;

implementation

{$R *.dfm}

uses MainFrm;

procedure TAddForm.btnAddClick(Sender: TObject);
var
  Rec : TAddress;
  F : TAddressFile;
begin
  AssignFile(F, 'file.dat');
  try
  { jeżeli plik istnieje, otwórz go; w przeciwnym wypadku ? utwórz }
    if FileExists('file.dat') then Reset(F) else Rewrite(F);
    Seek(F, FileSize(F)); // przesuń na koniec pliku

    Rec.Name := edtName.Text;
    Rec.Tel1 := StrToInt(edtTel1.Text);
    Rec.Tel2 := StrToInt(edtTel2.Text);
    Rec.Mail := edtAddress.Text;
    Write(F, Rec); // dodaj zawartość

  finally
    CloseFile(F);
    MainForm.lvAddress.Clear;
    MainForm.ReadFile;
  end;
end;

end.

Kopiowanie i przenoszenie plików

Podczas omawiania plików amorficznych przedstawiłem przykład kopiowania dwóch plików. Chciałem wtedy jedynie zaprezentować zasadę kopiowania danych, lecz stanowiło to trochę wyważanie otwartych drzwi. Istnieją bowiem funkcje, dzięki którym skopiowanie pliku jest jedynie kwestią jednego wiersza kodu.

Kopiowanie

Kopiowanie pliku (o kopiowaniu całych katalogów wspomnę później) może być zrealizowane za pomocą jednej funkcji API ? CopyFile.

Deklaracja owej funkcji w module Windows.pas przedstawia się następująco:

function CopyFile(lpExistingFileName, lpNewFileName: PChar; bFailIfExists: BOOL): BOOL; stdcall;

Pierwszym parametrem musi być ścieżka do kopiowanego pliku. Drugi parametr definiuje ścieżkę do nowego pliku, a ostatni (typu Bool) określa, czy w przypadku istnienia pliku o takiej nazwie program ma go zastąpić czy też nie.

Przykład użycia:

CopyFile('C:\plik.exe', 'D:\plik.exe', True);

Wadą takiego kopiowania jest mała możliwość manipulowania całą operacją. Nie możemy np. na komponencie TProgressBar pokazać postępu procesu kopiowania pliku.

Przenoszenie pliku

Przenoszenie pliku (inaczej mówiąc, jego wycinanie) jest równie proste co kopiowanie. Tu również wchodzi w grę jedna instrukcja ? MoveFile.

function MoveFile(lpExistingFileName, lpNewFileName: PChar): BOOL; stdcall;

Pierwszym parametrem musi być poprzednia ścieżka dostępu do pliku, a drugi parametr (lpNewFileName) musi być nową ścieżką:

MoveFile('C:\plik.exe', 'D:\plike.exe');

W tym momencie plik plik.exe z dysku C: zostanie przeniesiony na dysk D:

Struktura TSHFileOpStruct

W module ShellAPI znajduje się całkiem przydatny rekord ? TSHFileOpStruct ? który można wykorzystać do kopiowania lub przenoszenia plików. Oczywiście rekord jest używany jedynie w połączeniu z odpowiednimi funkcjami; prezentuje się w ten sposób:

  TShFileOpStruct = packed record
    Wnd: HWND;
    wFunc: UINT;
    pFrom: PWideChar;
    pTo: PWideChar;
    fFlags: FILEOP_FLAGS;
    fAnyOperationsAborted: BOOL;
    hNameMappings: Pointer;
    lpszProgressTitle: PWideChar;
  end;

Znaczenie poszczególnych parametrów jest następujące:
*Wnd ? uchwyt okna dialogowego używanego do pokazania statusu operacji.
*wFunc ? funkcja, jaka ma zostać wykonana ? patrz tabela 7.3.
*pFrom ? ścieżka pliku przeznaczonego do skopiowania, przeniesienia lub do innej operacji.
*pTo ? ścieżka pliku docelowego.
*fFlags ? flagi używane w połączeniu z operacją ? patrz tabela 7.4.
*fAnyOperationsAborted ? jeżeli użytkownik przerwie operacje przed jej zakończeniem, parametr ten będzie zawierał wartość True.
*hNameMappings ? parametr jest brany pod uwagę jedynie wtedy, gdy parametr fFlags zawiera wartość FOF_WANTMAPPINGHANDLE. Dotyczy to trochę bardziej zaawansowanego tematu, a mianowicie plików odwzorowanych.
*lpszProgressTitle ? tekst, który pojawi się w oknie kopiowania (standardowe okno systemu Windows). Parametr używany jedynie w przypadku, gdy fFlags to FOF_SIMPLEPROGRESS.

Tabela 7.3. Możliwe wartości parametru wFunc

ParametrOpis
FO_COPYKopiowanie plików. Używane są wówczas parametry wFrom i wTo
FO_DELETEUsuwanie plików. Parametr wFrom określa ścieżkę do usunięcia. wTo jest ignorowany
FO_MOVEPrzeniesienie pliku. Używane są wówczas parametry wFrom i wTo
FO_RENAMEZmienia nazwę pliku określonego parametrem wFrom. wTo zawiera nową nazwę

Tabela 7.4. Możliwe wartości parametru fFlags

ParametrOpis
FOF_ALLOWUNDOUmożliwia cofnięcie operacji w razie konieczności
FOF_FILESONLYZezwala na wykonywanie operacji jedynie na plikach
FOF_NOCONFIRMATIONNie wyświetla przycisku Tak na wszystkie, jeżeli jest to konieczne
FOF_NOCONFIRMMKDIRNie wyświetla zapytania o utworzenie katalogu, jeżeli jest to konieczne (tworzy go automatycznie)
FOF_RENAMEONCOLLISIONJeżeli plik istnieje, zmienia jego nazwę
FOF_SILENTNie wyświetla okienka z paskiem postępu operacji
FOF_SIMPLEPROGRESSWyświetla okienko postępu, ale nie pokazuje nazwy pliku
FOF_WANTMAPPINGHANDLEBardziej zaawansowany parametr, używany w połączeniu z plikami odwzorowanymi

To tyle, jeżeli chodzi o teorię. Sprawdźmy działanie owego rekordu w praktyce. Aby całość mogła zadziałać, należy na końcu wywołać funkcję SHFileOperation, której parametrem będzie wskazanie struktury TShFileOpStruct.

Oto przykład skopiowania programu do innego katalogu:

uses ShellAPI;

procedure TMainForm.Button1Click(Sender: TObject);
var
  Sh : TShFileOpStruct;
begin
  Sh.Wnd := Handle;
  Sh.wFunc := FO_COPY;
  Sh.pFrom := PChar(Application.ExeName);
  Sh.pTo := 'C:\kopia\kopia.exe';
  Sh.fFlags := FOF_ALLOWUNDO + FOF_NOCONFIRMATION;
  Sh.lpszProgressTitle := 'Trwa kopiowanie...';

  SHFileOperation(Sh);
end;

Po wykonaniu programu system najpierw zapyta Cię, czy chcesz utworzyć katalog C:\kopia (jeżeli nie istnieje), a dopiero później skopiuje dane.

Pełny kod źródłowy owego programu znajduje się na płycie CD-ROM w katalogu ../listingi/7/ShFileOpStruct.

Strumienie

Strumienie są specjalną formą wymiany i transportu danych, obsługiwaną przez klasę TStream. To określenie może nie jest zbyt precyzyjne, ale zaraz postaram się przedstawić Ci szczegółowe objaśnienia.

Dzięki strumieniom możesz w prosty sposób operować na danych znajdujących się w pamięci komputera, w plikach itp.

Poprzednie przykłady (pliki typowane i amorficzne) opierały się na wykorzystaniu funkcji WinAPI. Klasa TStream jest natomiast klasą VCL umieszczoną w module Classes, stąd obsługa samych plików, jak i transport danych, mogą być łatwiejsze.

Podział strumieni

Klasa TStream jest jedynie klasą bazową dla innych klas pochodnych ? strumieni operujących na innym typie danych. Przykładowo do operowania na plikach użyjemy klasy TFileStream, a do operowania na blokach pamięci ? TMemoryStream. Każda z takich klas charakteryzuje się odmiennymi właściwościami i metodami, stąd przed ich użyciem należy się zastanowić, która z nich będzie nam potrzebna.

Klasa TStream jest więc klasą bazową dla kilku klas pochodnych:

*TFileStream ? umożliwia dostęp do plików.
*TStringStream ? służy do manipulowania danymi typu String (łańcuchy tekstowe).
*TMemoryStream ? klasa służy do operowania na blokach pamięci.
*TBlobStream ? klasa strumieniowa, związana z bazami danych. O bazach danych będę mówił w trzeciej części niniejszej książki.
*TWinSocketStream ? klasa służąca do obsługi tzw. gniazd. Będę mówił o tym w dalszej części książki.
*TResourceStream ? klasa używana w połączeniu z zasobami. O zasobach będzie mowa w rozdziale 10.

Prosty przykład na początek

Pierwszy przykład to zapis danych ? właściwości danego komponentu. Wystarczy w tym wypadku skorzystać z metody WriteComponent klasy TStream. Jeżeli klasa TStream posiada ową metodę, to znaczy, że posiadają ją także klasy pochodne ? w tym TFileStream. Oto przykład:

procedure TMainForm.btnSaveClick(Sender: TObject);
var
  S : TFileStream;
begin
{ zapisz plik }
  S := TFileStream.Create('dane', fmCreate);
  S.WriteComponent(edtValue); // zapisz dane
  S.Free;
end;

Przykład jest prosty i krótki, ale bardzo funkcjonalny. Na samym początku należy wywołać konstruktora klasy TFileStream ? pierwszy parametr to nazwa pliku, a drugi to tzw. flaga (opiszę to później).

Konstruktor klasy jest specyficzny dla każdej klasy strumieniowej. Oznacza to, że każda klasa może posiadać inne parametry albo nie posiadać ich w ogóle.

Kolejny wiersz to wywołanie metody WriteComponent ? należy w niej podać nazwę komponentu, którego właściwości zostaną zapisane w pliku.

Odczyt danych to także kwestia paru wierszy:

  S := TFileStream.Create('dane', fmOpenRead);
  S.ReadComponent(edtValue);
  S.Free;

Przypisanie zapisanych w pliku właściwości realizowane jest przez procedurę ReadComponent. W parametrze musi znaleźć się nazwa komponentu, do którego program przypisze dane z pliku (listing 7.4.)

Listing 7.4. Zapis i odczyt właściwości komponentu TEdit

unit MainFrm;

interface

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

type
  TMainForm = class(TForm)
    btnSave: TButton;
    edtValue: TEdit;
    procedure btnSaveClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.btnSaveClick(Sender: TObject);
var
  S : TFileStream;
begin
{ zapisz plik }
  S := TFileStream.Create('dane', fmCreate);
  S.WriteComponent(edtValue); // zapisz dane
  S.Free;
end;

procedure TMainForm.FormCreate(Sender: TObject);
var
  S : TFileStream;
begin
  if not FileExists('dane') then Exit;  // anuluj, jeżeli plik nie istnieje
  
  S := TFileStream.Create('dane', fmOpenRead);
  S.ReadComponent(edtValue);
  S.Free;
end;

end.

Konstruktor klasy TFileStream

Praktycznie rzecz biorąc, jedynie klasa TFileStream wymaga podania parametrów otwarcia pliku. Drugi parametr konstruktora określa bowiem, w jaki sposób plik zostanie otwarty. A może ma on zostać utworzony? Możliwe do wykorzystania flagi przedstawiono w tabeli 7.5.

Tabela 7.5. Możliwe tryby otwarcia pliku

TrybOpis
fmCreatePlik ma zostać utworzony. Jeżeli istnieje, zostanie otwarty do zapisu
fmOpenReadPlik zostanie otwarty do odczytu
fmOpenWritePlik zostanie otwarty do zapisu
fmOpenReadWritePlik zostanie otwarty zarówno do zapisu, jak i do odczytu
fmShareExclusiveInne aplikacje nie będą mogły otworzyć pliku, podczas gdy nasza aplikacja korzysta z owego pliku
fmShareDenyWriteInne aplikacje mogą otwierać plik, ale tylko do odczytu
fmShareDenyReadInne aplikacje mogą otwierać plik jedynie do zapisu
fmShareDenyNoneBrak zabezpieczeń ? inne aplikacje mogą odczytywać i zapisywać dane w pliku

Parametry (flagi) znajdujące się w powyższej tabeli mogą być ze sobą łączone za pomocą operatora OR (może to wyglądać np. tak: Plik:=TFileStream.Create('Plik',fmCreate OR fmOpenReadWrite);).
Pamiętaj o tym, aby po skończeniu pracy z zasobem zwolnić go, wywołując metodę Free.

Pozostałe metody i właściwości klasy TStream

W niniejszej sekcji omówię jedynie właściwości i metody, które są obecne w klasie TStream, tak więc są obecne także i w klasach pochodnych. Jednak niektóre klasy pochodne posiadają inne, dodatkowe metody i właściwości, charakterystyczne dla danego strumienia.

Na razie przedstawię jedynie deklarację, owych metod i właściwości; ich wykorzystanie praktyczne zaprezentuję nieco dalej.

Właściwości

Position
```delphi property Position: Int64; ``` Właściwość Position określa położenie w danym strumieniu. Zwracana przez nią wartość określa ilość bajtów, jakie zostały już odczytane. Przykładowo jeżeli odczytywany przez nas plik ma wielkość 1 000 bajtów, możemy przejść do określonego miejsca tego pliku. Służy do tego metoda `Seek`. Jeżeli przesuniemy się np. na 300. bajt, właściwość Position zwróci wartość 300 (położenie w pliku).
Size
```delphi property Size: Int64; ``` Właściwość Size zwraca wielkość odczytywanego pliku ? strumienia. Wartość jest podawana w bajtach.

Metody

CopyFrom
```delphi function CopyFrom(Source: TStream; Count: Int64): Int64; ``` Funkcja `CopyFrom` jest używana wtedy, gdy chcemy skopiować dane z jakiegoś strumienia do innego strumienia. Pierwszy parametr musi być nazwą klasy typu `TStream` lub pochodną. Natomiast drugi parametr to ilość bajtów, które mają być przypisane drugiemu strumieniowi.
Read, ReadBuffer
```delphi function Read(var Buffer; Count: Longint): Longint; virtual; abstract; procedure ReadBuffer(var Buffer; Count: Longint); ``` Oba polecenia są do siebie bardzo podobne, a ich działanie jest wręcz identyczne. Jedyna różnica polega na tym, że `Read` jest funkcją, a `ReadBuffer` ? procedurą. Tak więc funkcja `Read` zwraca ilość bajtów, które zostały dotychczas odczytane.
Seek
```delphi function Seek(Offset: Longint; Origin: Word): Longint; overload; virtual; function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; overload; virtual; ``` Funkcja `Seek` służy do przesuwania się w pliku i ustawiania znacznika odczytu na konkretnej pozycji. Funkcja ta działa tak samo jak funkcja systemowa `Seek`, która omawiana była w trakcie opisywania plików amorficznych i typowanych.

Jak widzisz, dostępne są dwie funkcje ? w zależności od rodzaju parametrów system wybierze jedną z nich. Pierwszym parametrem musi być ilość bajtów, o jaką zostanie wykonany ?skok?. Natomiast kolejny parametr określa, jak będzie interpretowana wartość Offset (wartości możliwe do użycia w tym parametrze znajdują się w tabeli 7.6.).

Tabela 7.6. Możliwe wartości parametru Origin

WartośćOpis
soFromBeginningPrzesunięcie odbędzie się od początku zasobu
soFromCurrentWartość Offset określa ilość bajtów, o jaką nastąpi przesunięcie ? począwszy od dotychczasowej pozycji
soFromEndPrzesunięcie odbędzie się od tyłu
Write, WriteBuffer
```delphi function Write(const Buffer; Count: Longint): Longint; virtual; abstract; procedure WriteBuffer(const Buffer; Count: Longint); ``` Zasada jest taka, jak w przypadku funkcji `Read` i `ReadBuffer`. Tutaj także dwie funkcje odgrywają prawie identyczną rolę (zapis danych do strumienia).

Pierwszy parametr musi określać dane, które zostaną zapisane, a drugi ? ilość bajtów, która ma zostać dołączona do strumienia.

Jedna różnica dzieląca te oba polecenia polega na tym, że Write zwraca ilość bajtów zapisanych w strumieniu.

Praktyczny przykład

Jak dotąd więcej o strumieniach pisałem teoretycznie, nie podając praktycznych przykładów wykorzystania. Zaprezentuję wobec tego teraz przykład ? dzielenie plików. Nasz program, wykorzystując strumienie, podzieli wybrany plik na mniejsze fragmenty. Użytkownik oczywiście będzie mógł z powrotem połączyć wszystko w jedną całość.

Rzut okiem na interfejs programu

Główny formularz programu przedstawiony został na rysunku 7.5.

7.5.jpg
Rysunek 7.5. Wygląd programu służącego do dzielenia plików

Główny człon programu stanowią komponenty z zakładki Win 3.1 palety komponentów. Dzięki owym komponentom mamy możliwość wyświetlenia struktury katalogów na danej partycji oraz zawartość zaznaczonego folderu.

Do ustalenia rozmiaru pojedynczego pliku skorzystałem z komponentu TTrackBar z zakładki Win32. Domyślnie jeden podzielony fragment pliku będzie miał rozmiar równy 500 bajtów.

Na samym dole znajduje się pasek postępu (TProgressBar), który określa stopień wykonania dzielenia plików.

Kod źródłowy programu

Podstawowymi funkcjami w naszym programie będą dwie procedury, które zadeklarowałem w sekcji private klasy:

  private
  { procedura dzielenia plików }
    procedure DivFile(const FileName:  String);
  { procedura łączenia plików }
    procedure ConnectFile(const Dir : String);
  end;

Pierwsza procedura będzie dzieliła plik określony parametrem FileName; natomiast procedura druga spowoduje połączenie wszystkich plików, znajdujących się w określonym katalogu, w tym przypadku określonym parametrem Dir.

Najpierw przyjrzyj się procedurze DivFile, a ja później postaram się ją omówić:

procedure TMainForm.DivFile(const FileName: String);
var
  Input : TFileStream;
  Output : TFileStream;
  i : Integer;
  DirPath : String;
  BuffSize : Integer;
begin
  BuffSize := BufferTrack.Position; // pobierz rozmiar bufora ( rozmiar jednego pliku )
  DirPath :=  FileName + '.temp'; // dodaj rozszerzenie
  mkDir(DirPath);  // utwórz folder
  Input := TFileStream.Create(FileName, fmOpenRead);
  try
    ProgressBar.Max := (Input.Size div BuffSize);

  { po podzieleniu rozmiaru pliku przez bufor otrzymamy ilość "kawałków", z których
    będzie się składał podzielony plik }
    for I := 0 to (Input.Size div BuffSize) do
    begin
      Application.ProcessMessages;
      ProgressBar.Position := i;

    { w każdej iteracji pętli przesuń się w zawartości pliku o rozmiar bufora }
      Input.Seek(i * BuffSize, soFromBeginning);

    { utwórz w nowoutworzonym folderze plik odpowiadający fragmentowi dzielonego pliku }
      Output := TFileStream.Create((DirPath + '\' + ExtractFileName(FileName) + IntToStr(i) + '.temp'),
      fmCreate);
      try
      { następnie za pomocą funkcji CopyFrom ze strumienia określona ilość bajtów
        zostaje przekopiowana (bufor) do strumienia Output. Jeżeli pozostała do
        skopiowania część jest mniejsza od bufora, to trzeba skopiować tylko tę
        część, która pozostała do skopiowania... :))
      }
        if (Input.Size ? (i * BuffSize)) < BuffSize then
          Output.CopyFrom(Input, (Input.Size ? (i * BuffSize)))
        else Output.CopyFrom(Input, BuffSize);
      finally
        Output.Free;
      end;
    end;
  finally
    Input.Free;
  end;
end;

Na samym początku kod ten może wydać się niezwykle skomplikowany ? aby ułatwić jego odczytanie, umieściłem w nim sporo komentarzy.

Na samym początku następuje wczytanie pliku do strumienia ? zmiennej Input. Kolejnym krokiem jest określenie ilości iteracji pętli for. Ilość tę uzyskujemy, dzieląc rozmiar strumienia przez bufor :

for I := 0 to (Input.Size div BuffSize) do

Kolejnym krokiem jest skorzystanie z funkcji Seek w celu przemieszczenia się do określonego miejsca w pliku:

Input.Seek(i * BuffSize, soFromBeginning);

Dzięki temu w każdej iteracji pętli następuje przesunięcie o np. 500, 1 000, 1 500 bajtów itd. Podczas tej iteracji tworzony jest nowy strumień (plik), a do niego dodawane jest kolejne 500 bajtów danych.
Łączenie plików jest sprawą o tyle skomplikowaną, że niektóre instrukcje nie są Ci znane. Poznasz je dopiero w dalszej części tego rozdziału. Mam na myśli instrukcje służące do wyszukiwania plików w danym katalogu.

Oto procedura łącząca:

procedure TMainForm.ConnectFile(const Dir: String);
var
  SR : TSearchRec;
  Found : Integer;
  I : Integer;
  Input : TFileStream;
  Output : TfileStream;
  NumberOfFiles : Integer;
begin
  NumberOfFiles := 0;
{
  te instrukcje mają na celu uzyskanie ilości plików .temp znajdujących się
  w określonej lokalizacji ? ilość plików oznacza zmienna NumberOfFile.
}
  Found := FindFirst(Dir + '\*.temp', faAnyFile, SR);
  while Found = 0 do
  begin
    Inc(NumberOfFiles);
    Found := FindNext(SR);
  end;
  FindClose(SR);

{
   te instrukcje odpowiadają za stworzenie pliku ? to do niego zostanie włączona
   zawartość plików-kawałków...
}
  if not FileExists(ExtractFileDir(Dir) + ChangeFileExt(ExtractFileName(Dir), '')) then
    Output := TFileStream.Create(ExtractFileDir(Dir) + ChangeFileExt(ExtractFileName(Dir), ''), fmCreate)
  else Output := TFileStream.Create(ExtractFileDir(Dir) + ChangeFileExt(ExtractFileName(Dir), ''), fmOpenWrite);

  ProgressBar.Max := NumberOfFiles;

  try
    for I := 0 to NumberOfFiles ?1 do
    begin
      Application.ProcessMessages;
      ProgressBar.Position := i;
    { tutaj następuje otwarcie pliku-kawałka do skopiowania }
      Input := TFileStream.Create(Dir + '\' + ExtractFileName(ChangeFileExt(DirListBox.Directory, '')) + IntToStr(i) + '.temp',
      fmOpenRead);
      try
      { tutaj do pliku łączonego kopiujemy zawartość małego pliku (części) }
        Output.CopyFrom(Input, Input.Size);
      finally
        Input.Free;
      end;
    end;
  finally
    Output.Free;
  end;
end;

Na samym początku odbywa się wyszukanie wszystkich plików znajdujących się w danym katalogu. Następnie, po pobraniu ilości plików (zajmiemy się tym nieco dalej), następuje wykonanie pętli, która odczytywać będzie zawartość tych wszystkich plików i kolejno dodawać dane do jednego strumienia (listing 7.5.).

Listing 7.5. Listing programu do dzielenia plików

unit MainFrm;

interface

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

type
  TMainForm = class(TForm)
    BufferTrack: TTrackBar;
    lblBuffor: TLabel;
    DirListBox: TDirectoryListBox;
    FileListBox: TFileListBox;
    DriveCombo: TDriveComboBox;
    btnDivFile: TButton;
    ProgressBar: TProgressBar;
    btnConnectFile: TButton;
    procedure BufferTrackChange(Sender: TObject);
    procedure btnDivFileClick(Sender: TObject);
    procedure btnConnectFileClick(Sender: TObject);
  private
  { procedura dzielenia plików }
    procedure DivFile(const FileName:  String);
  { procedura łączenia plików }
    procedure ConnectFile(const Dir : String);
  end;

var
  MainForm: TMainForm;

implementation

{$R *.DFM}

{ TMainForm }

procedure TMainForm.DivFile(const FileName: String);
var
  Input : TFileStream;
  Output : TFileStream;
  i : Integer;
  DirPath : String;
  BuffSize : Integer;
begin
  BuffSize := BufferTrack.Position; // pobierz rozmiar bufora ( rozmiar jednego pliku )
  DirPath :=  FileName + '.temp'; // dodaj rozszerzenie
  mkDir(DirPath);  // utwórz folder
  Input := TFileStream.Create(FileName, fmOpenRead);
  try
    ProgressBar.Max := (Input.Size div BuffSize);

  { po podzieleniu rozmiaru pliku przez bufor otrzymamy ilość kawałków, z których
    składał się będzie podzielony plik }
    for I := 0 to (Input.Size div BuffSize) do
    begin
      Application.ProcessMessages;
      ProgressBar.Position := i;

    { w każdej iteracji pętli przesuń się w zawartości pliku o rozmiar bufora }
      Input.Seek(i * BuffSize, soFromBeginning);

    { utwórz w nowoutworzonym folderze plik odpowiadający fragmentowi dzielonego pliku }
      Output := TFileStream.Create((DirPath + '\' + ExtractFileName(FileName) + IntToStr(i) + '.temp'),
      fmCreate);
      try
      { następnie, za pomocą funkcji CopyFrom, ze strumienia  określona ilość bajtów 
        (bufor) zostaje przekopiowana do strumienia Output. Jeżeli pozostała do
        skopiowania część jest mniejsza od bufora, to trzeba skopiować tylko tę
        cześć, która pozostała do skopiowania... :))
      }
        if (Input.Size ? (i * BuffSize)) < BuffSize then
          Output.CopyFrom(Input, (Input.Size ? (i * BuffSize)))
        else Output.CopyFrom(Input, BuffSize);
      finally
        Output.Free;
      end;
    end;   
  finally
    Input.Free;
  end;  
end;

procedure TMainForm.BufferTrackChange(Sender: TObject);
begin
  lblBuffor.Caption := 'Bufor: ' + IntToStr(BufferTrack.Position);
end;

procedure TMainForm.btnDivFileClick(Sender: TObject);
begin
  DivFile(FileListBox.FileName);
end;

procedure TMainForm.ConnectFile(const Dir: String);
var
  SR : TSearchRec;
  Found : Integer;
  I : Integer;
  Input : TFileStream;
  Output : TfileStream;
  NumberOfFiles : Integer;
begin
  NumberOfFiles := 0;
{
  Te instrukcje mają na celu uzyskanie ilości plików .temp znajdujących się
  w określonej lokalizacji ? ilość plików oznacza zmienna NumberOfFile.
}
  Found := FindFirst(Dir + '\*.temp', faAnyFile, SR);
  while Found = 0 do
  begin
    Inc(NumberOfFiles);
    Found := FindNext(SR);
  end;
  FindClose(SR);

{
   Te instrukcje odpowiadają za stworzenie pliku ? to do niego zostanie włączona
   zawartości plików-kawałków...
}
  if not FileExists(ExtractFileDir(Dir) + ChangeFileExt(ExtractFileName(Dir), '')) then
    Output := TFileStream.Create(ExtractFileDir(Dir) + ChangeFileExt(ExtractFileName(Dir), ''), fmCreate)
  else Output := TFileStream.Create(ExtractFileDir(Dir) + ChangeFileExt(ExtractFileName(Dir), ''), fmOpenWrite);

  ProgressBar.Max := NumberOfFiles;

  try
    for I := 0 to NumberOfFiles ?1 do
    begin
      Application.ProcessMessages;
      ProgressBar.Position := i;
    { tutaj następuje otwarcie pliku ? kawałka do skopiowania }
      Input := TFileStream.Create(Dir + '\' + ExtractFileName(ChangeFileExt(DirListBox.Directory, '')) + IntToStr(i) + '.temp',
      fmOpenRead);
      try
      { tutaj do pliku łączonego kopiujemy zawartość małego pliku (części) }
        Output.CopyFrom(Input, Input.Size);
      finally
        Input.Free;
      end;
    end;
  finally
    Output.Free;
  end;
end;

procedure TMainForm.btnConnectFileClick(Sender: TObject);
begin
  ConnectFile(DirListBox.Directory);
end;

end.

Na dołączonej płycie CD-ROM znajduje się inny przykład zastosowania strumieni ? odczytywanie tzw. tagów z plików mp3. Kompletny kod źródłowy możesz znaleźć w katalogu ..listingi/7/mp3Tag.

Wyszukiwanie

W poprzednim podpunkcie podczas omawiania strumieni zamieściłem przykład, w którym wykorzystane zostały nieznane Ci dotychczas funkcje. Miały one na celu wyszukanie plików znajdujących się w danym katalogu. Teraz zajmiemy się właśnie procedurami umożliwiającymi znalezienie konkretnego pliku w określonym miejscu lub stworzenie listy wszystkich plików.

Rekord TSearchRec

Podczas korzystania z funkcji wyszukiwania będziesz zmuszony poznać strukturę rekordu TSearchRec. Po wykonaniu odpowiednich instrukcji w owym rekordzie zapisane zostaną informacje na temat znalezionego pliku.

type 
   TSearchRec = record
    Time: Integer;
    Size: Integer;
    Attr: Integer;
    Name: TFileName;
    ExcludeAttr: Integer;
    FindHandle: THandle;
    FindData: TWin32FindData;
end;

Do wyszukiwania będziemy używać instrukcji FindFirst oraz FindNext. Wówczas należy w parametrze podać zmienną wskazującą rekord TSearchRec. Ów rekord dostarcza nam informacji o rozmiarze pliku, czasie jego utworzenia oraz atrybutach.

Jak zrealizować wyszukiwanie?

Zrealizowanie procedury przeszukania dysku wcale nie jest takie trudne. Posługiwać się będziemy trzema podstawowymi funkcjami z modułu SysUtils, które w połączeniu z rekordem TSearchRec dadzą zamierzony efekt. Te funkcje to FindFirst, FindNext i FindClose.

FindFirst

function FindFirst(const Path: string; Attr: Integer; var F: TSearchRec): Integer;

Na samym początku całego procesu należy skorzystać z funkcji FindFirst, która inicjuje proces wyszukiwania. Pierwszym parametrem musi być katalog (ścieżka), w którym program będzie przeszukiwał pliki. Drugi parametr to atrybuty plików, które mają być brane pod uwagę (patrz tabela 7.7). Parametr ostatni ? F ? to wskazanie na rekord TSearchRec.

Tabela 7.7. Atrybuty plików

Nazwa atrybutuOpis
faReadOnlyPliki tylko do odczytu
faHiddenPliki ukryte
faSysFilePliki systemowe
faDirectoryKatalogi
faArchiveArchiwa</td</tr>
faAnyFileDowolne pliki

Atrybuty plików mogą być ze sobą połączone za pomocą operatora +. Przykładowo jeżeli chcesz, aby program wyszukał pliki ukryte oraz systemowe, to w drugim parametrze należy podać kombinację:

faHidden + faSysFile.

Jeżeli operacja wyszukania powiedzie się, funkcja zwróci 0. W przeciwnym wypadku zwrócony zostanie numer błędu.

FindFirst('C:\Windows\*.*', faAnyFile, SR);

Powyższy przykład spowoduje znalezienie dowolnych plików z katalogu C:\Windows.

Podając ścieżkę do katalogu w pierwszym parametrze, wpisałem na końcu znaki .. Jest to tzw. maska, oznaczająca rozszerzenia plików. W takim wypadku pod uwagę będą brane pliki z dowolnym rozszerzeniem.

FindNext

function FindNext(var F: TSearchRec): Integer;

Funkcja FindNext jest stosowana w połączeniu z wyżej omówioną funkcją ? FindFirst. Można powiedzieć, że obie te funkcje uzupełniają się wzajemnie i razem realizują proces przeszukiwania.

FindFirst jedynie inicjuje proces wyszukiwania, a po znalezieniu pierwszego pliku zwraca wartość 0. Aby program szukał dalej, należy wywołać funkcję FindNext, w której podany musi zostać parametr typu TSearchRec.

FindClose

procedure FindClose(var F: TSearchRec);

Po zakończeniu całego procesu związanego z wyszukiwaniem należy zwolnić odpowiednie zasoby i pamięć zarezerwowaną przez funkcję FindFirst. Wykorzystanie tej procedury jest proste ? w parametrze wystarczy jedynie podać nazwę zmiennej wskazującej na rekord TSearchRec.

Rekurencja

Podczas swojej dalszej przygody z programowaniem często możesz spotkać się z terminem rekurencji. Najprościej mówiąc, jest to procedura, która wywołuje samą siebie. Może brzmi to nieco dziwnie, ale tak jest w rzeczywistości! W pewnym momencie, podczas wykonywania algorytmu (określonej operacji), następuje ponowne odwołanie się do wykonywanej funkcji.

Rekurencja zostanie wykonana przez nas podczas realizacji zadania, jakim jest wyszukiwanie. Wygląda to tak: użytkownik wykonuje określone czynności, które mają za zadanie wyszukanie wszystkich plików, przykładowo w katalogu C:\Moje dokumenty. I tutaj następuje wywołanie procedury ? np. Search('C:\Moje dokumenty'). Program oprócz pobrania listy wszystkich plików pobiera również listę katalogów. Wykonywana zostaje wówczas kluczowa operacja (rekurencja), czyli ponowne wywołanie procedury, ale ze zmienionym parametrem ? np. Search('C:\Moje dokumenty\www').

Praktyczny przykład

Jak dotąd zaprezentowałem jedynie teoretyczną wiedzę na temat wyszukiwania. Nie może być tak, aby Twoje przyszłe doświadczenia opierały się jedynie na teorii. Dlatego też na płycie CD-ROM w katalogu ../listingi/7/Searching umieściłem pełny kod programu realizującego wyszukiwanie.

Program w trakcie działania przedstawiłem na rysunku 7.6.

7.6.jpg
Rysunek 7.6. Wyszukiwanie plików z rozszerzeniem *.pas

Wyszukiwanie plików

Wyszukiwanie plików realizowane jest praktycznie za pomocą jednej procedury rekurencyjnej ? Search:

{ procedura rekurencyjna }
procedure TMainForm.Search(const StartDir : String; Ext : String);
var
  SR, DR  : TSearchRec;
  Found, FoundFile : Integer;
  ListItem : TListItem;
  Icon : TIcon;
  ExtNo : Word;

  { ta procedura sprawdza, czy na końcu zmiennej znajduje się znak \ ? jeżeli
    tak, nic nie jest wykonywane; jeżeli tego znaku brak, zostaje on dodany... }
  function IsDir(Value : String) : String;
  begin
    if Value[Length(Value)] <> '\' then  // jeżeli na końcu znajdziesz znak
      Result := Value + '\' else Result := Value; // dodaj go... w przeciwnym wypadku nie wykonuj nic
  end;

begin
  Icon := TIcon.Create;
  Found := FindFirst(IsDir(StartDir) + '*.*', faDirectory, DR); // następuje pobieranie katalogów z podanej lokalizacji
  while Found = 0 do // pętelka
  begin
    Application.ProcessMessages; // nie blokuj programu
    if ((DR.Attr and faDirectory) = faDirectory) and  // sprawdza, czy pozycja jest katalogiem
       ((DR.Name <> '.') and (DR.Name <> '..')) then
    begin
    { jeżeli jest to katalog, następuje pobranie plików w nim się znajdujących }
      FoundFile := FindFirst(IsDir(StartDir) + DR.Name + '\' + Ext, faAnyFile, SR);
      while FoundFile = 0 do
      begin
        Application.ProcessMessages;
        if ((SR.Name <> '.') and (SR.Name <> '..')) then
        begin
          Icon.Handle := ExtractAssociatedIcon(hInstance, PCHar(IsDir(StartDir) + DR.Name + '\' + SR.Name), ExtNO);

        { dodanie ścieżki pliku do listy plików }
          ListItem := lbFileBox.Items.Add;
          ListItem.ImageIndex := ImageList.AddIcon(Icon);
          ListItem.Caption := IsDir(StartDir) + DR.Name + '\' + SR.Name;
          ListItem.SubItems.Add(DateTimeToStr(FileDateToDateTime(SR.Time)));
          ListItem.SubItems.Add(IntToStr(SR.Size) + ' B');
        end;

        FoundFile := FindNext(SR); // kontynuuj przeszukiwanie
      end;
      FindClose(SR);  // zkończ
      
      Search(IsDir(StartDir) + DR.Name, Ext); // tutaj następuje rekurencja
    end;
    Found := FindNext(DR); // kontynuuj
  end;
  FindClose(DR);
  Icon.Free;
end;

Kod ten może wydać się zagmatwany i niezrozumiały, a w przekonaniu tym może nas utwierdzić obecność dużej ilości komentarzy, które powodują pewien zamęt.

Przejdźmy jednak do omówienia samego kodu. Jak można zauważyć, w procedurze Search znajduje się inna procedura ? IsDir. Sprawdza ona, czy na końcu podanej w parametrze ścieżki znajduje się znak . Jeżeli go nie ma, zostaje on dodany.

Na samym początku następuje pobranie listy wszystkich katalogów znajdujących się w katalogu startowym, określonym w parametrze StartDir. Pobranie odbywa się za pomocą pętli while, w której odczytujemy zawartość rekordu TSearchRec. Aby szukać dalej, należy wywołać funkcję FindNext.

Kolejnym etapem jest wywołanie kolejnego polecenia FindFirst, które ma tym razem pobrać listę wszystkich plików o określonym rozszerzeniu z aktualnego katalogu.

Następnie program dodaje nazwę każdego pliku do listy komponentu TListView. Użyta tu została funkcja ExtractAssociatedIcon, która ma za zadanie pobrać ikonę określającą dany plik.

Jak zapewne zauważyłeś, zawsze sprawdzam, czy nazwa znalezionego pliku nie jest znakiem . (kropka) lub .. (dwie kropki). W ten sposób system zaznacza, że istnieje katalog nadrzędny.

Pełny kod źródłowy programu znajduje się w listingu 7.6.

Listing 7.6. Kod źródłowy programu (wyszukiwanie)

{
   Copyright (c) 2002 ? Adam Boduch
}


unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, FileCtrl, ComCtrls, ImgList, ShellAPI;

type
  TMainForm = class(TForm)
    DirBox: TDirectoryListBox;
    edtMask: TEdit;
    lblMask: TLabel;
    btnFind: TButton;
    lblFoundResults: TLabel;
    lbFileBox: TListView;
    ImageList: TImageList;
    procedure btnFindClick(Sender: TObject);
    procedure lbFileBoxDblClick(Sender: TObject);
  private
    procedure Search(const StartDir : String; Ext : String);
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

{ procedura rekurencyjna }
procedure TMainForm.Search(const StartDir : String; Ext : String);
var
  SR, DR  : TSearchRec;
  Found, FoundFile : Integer;
  ListItem : TListItem;
  Icon : TIcon;
  ExtNo : Word;

  { ta procedura sprawdza, czy na końcu zmiennej znajduje się znak \ ? jeżeli
    tak, nic nie jest wykonywane; jeżeli tego znaku brak, zostaje on dodany... }
  function IsDir(Value : String) : String;
  begin
    if Value[Length(Value)] <> '\' then  // jeżeli na końcu znajdziesz znak
      Result := Value + '\' else Result := Value; // dodaj go... w przeciwnym wypadku nie wykonuj nic
  end;

begin
  Icon := TIcon.Create;
  Found := FindFirst(IsDir(StartDir) + '*.*', faDirectory, DR); // następuje pobieranie katalogów z podanej lokalizacji
  while Found = 0 do // pętelka
  begin
    Application.ProcessMessages; // nie blokuj programu
    if ((DR.Attr and faDirectory) = faDirectory) and  // sprawdza, czy pozycja jest katalogiem
       ((DR.Name <> '.') and (DR.Name <> '..')) then
    begin
    { jeżeli jest to katalog, następuje pobranie plików w nim się znajdujących }
      FoundFile := FindFirst(IsDir(StartDir) + DR.Name + '\' + Ext, faAnyFile, SR);
      while FoundFile = 0 do
      begin
        Application.ProcessMessages;
        if ((SR.Name <> '.') and (SR.Name <> '..')) then
        begin
          Icon.Handle := ExtractAssociatedIcon(hInstance, PCHar(IsDir(StartDir) + DR.Name + '\' + SR.Name), ExtNO);

        { dodanie ścieżki pliku do listy plików }
          ListItem := lbFileBox.Items.Add;
          ListItem.ImageIndex := ImageList.AddIcon(Icon);
          ListItem.Caption := IsDir(StartDir) + DR.Name + '\' + SR.Name;
          ListItem.SubItems.Add(DateTimeToStr(FileDateToDateTime(SR.Time)));
          ListItem.SubItems.Add(IntToStr(SR.Size) + ' B');
        end;

        FoundFile := FindNext(SR); // kontynuuj przeszukiwanie
      end;
      FindClose(SR);  // zkończ
      
      Search(IsDir(StartDir) + DR.Name, Ext); // tutaj następuje rekurencja
    end;
    Found := FindNext(DR); // kontynuuj
  end;
  FindClose(DR);
  Icon.Free;
end;

procedure TMainForm.btnFindClick(Sender: TObject);
begin
  lbFileBox.Clear; // wyczyść kontrolkę
  ImageList.Clear;
  lbFileBox.Items.BeginUpdate;
  btnFind.Enabled := False; // dezaktywuj komponent
  Search(DirBox.Directory, edtMask.Text); // wyszukaj
  btnFind.Enabled := True; // aktywuj komponent
  lblFoundResults.Caption := 'Rezultaty poszukiwań: ' + IntToStr(lbFileBox.Items.Count) + ' znalezionych plików...';
  lblFoundResults.Visible := True; // pokazanie komponentu
  lbFileBox.Items.EndUpdate;
end;

procedure TMainForm.lbFileBoxDblClick(Sender: TObject);
begin
  ShellExecute(Handle, 'open', PCHar(lbFileBox.Selected.Caption), nil, nil, SW_SHOW);
end;

end.

Informacja o dyskach

W tym podpunkcie zajmiemy się uzyskiwaniem podstawowych informacji o dyskach znajdujących się w systemie oraz danych dokładniejszych, takich jak pojemność partycji, ilość wolnego miejsca itp.

Pobieranie listy napędów

Dyski (napędy) w systemie Windows mogą mieć różne litery ? od A do Z. Aby pobrać listę wszystkich dysków, należy wykonać pętle:

for I := Ord('A') to Ord('Z') do { ... }

Przypominam, że funkcja Ord ma za zadanie zamianę znaku podanego w parametrze (typu Char) na kod ASCII.

W każdej iteracji pętli musimy pobrać informację na temat konkretnego dysku. Realizowane jest to poprzez funkcję GetDriveType, która na podstawie litery dysku jest w stanie podać jego typ (CD-ROM, stacja dyskietek itp.). Tabela 7.8 prezentuje wartości, które mogą zostać zwrócone przez funkcję GetDriveType.

Tabela 7.8. Wartości, jakie może zwrócić funkcja GetDriveType

WartośćOpis
0Typ dysku nie jest możliwy do określenia
1Napęd o tym oznaczeniu nie istnieje
DRIVE_REMOVABLEDyskietka lub napęd wymienny
DRIVE_FIXEDNapęd niewymienny
DRIVE_REMOTENapęd sieciowy
DRIVE_CDROMCD-ROM
DRIVE_RAMDISKRAM-dysk, czyli dysk wirtualny

W parametrze funkcji należy podać nazwę (literę) dysku:

GetDriveType('C:\');

Wówczas system zwróci informację na temat dysku C:

Pobieranie rozmiaru dysku

Pobranie ilości wolnego miejsca oraz pojemności konkretnego dysku jest bardzo proste dzięki dwóm funkcjom: DiskSize oraz DiskFree. W obu należy podać parametr w postaci numeru dysku ? np. Ord('C'), a zwrócona wartość będzie zawierała ilość wolnych bajtów oraz łączną ilość bajtów.

      { dodanie do komponentu danych (w kB) }
      ListItem.SubItems.Add(IntToStr(DiskSize(DriveByte) div 1024) + ' kB');
      ListItem.SubItems.Add(IntToStr(DiskFree(DriveByte) div 1024) + ' kB');

Powyższy przykład prezentuje dodanie do komponentu TListView informacji o pojemności oraz ilości wolnego miejsca wyrażonej w kilobajtach.

Pobieranie dodatkowych informacji

W celu pobrania dodatkowych informacji na temat dysku (takich, jak ilość klastrów, sektorów itp.) należy skorzystać z funkcji API ? GetDiskFreeSpace:

function GetDiskFreeSpace(lpRootPathName: PChar;
  var lpSectorsPerCluster, lpBytesPerSector, lpNumberOfFreeClusters, lpTotalNumberOfClusters: DWORD): BOOL; stdcall;

Podczas wywoływania funkcji wszystkie parametry z wyjątkiem pierwszego muszą być wskazaniem na zmienną typu DWORD. Znaczenie poszczególnych parametrów jest następujące:
*lpRootPathName ? ścieżka (litera dysku), z którego będą pobieranie informacje. Może to być np. C:
*lpSectorsPerCluster ? ilość sektorów przydających na klaster.
*lpBytesPerSector ? liczba bajtów przypadających na jeden sektor.
*lpNumberOfFreeClusters ? liczba wolnych klastrów.
*lpTotalNumberOfClusters ? łączna ilość klastrów.

Zastosowanie funkcji GetDiskFreeSpace może wyglądać tak:

{ pobieranie dodatkowych informacji do dysków }  
GetDiskFreeSpace(PChar(Chr(i) + ':\'), SectorsPerClusters, BytesPerSector, FreeClusters, TotalClusters);

Od tego momentu w zmiennych podanych w parametrach pojawią się odpowiednie wartości. To, co teraz należy zrobić, to przedstawić je użytkownikowi w odpowiedniej formie.

Cały kod źródłowy programu znajduje się w listingu 7.7.

Listing 7.7. Kod źródłowy modułu

unit MainFrm;

interface

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

type
  TMainForm = class(TForm)
    imgIcons: TImageList;
    ListView: TListView;
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var                  
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.FormCreate(Sender: TObject);
var
  i : Integer;
  Drive_Type : Integer;
  ListItem : TListItem;
  Volume : array[0..255] of char;
  MaxComponentLength, Flag : DWORD;
  SectorsPerClusters, BytesPerSector,
      FreeClusters, TotalClusters : DWORD;
  DriveByte : Byte;
begin

{ pętla po wszystkich literach }
  for I := Ord('A') to Ord('Z') do
  begin
  // uzyskanie litery dysku
    Drive_Type := GetDriveType(PChar(Chr(i) + ':\'));
    if (Drive_Type <> 0) and (Drive_Type <> 1) then
    begin
    { pobranie etykiety }
      GetVolumeInformation(PChar(Chr(i) + ':\'), Volume, SizeOf(Volume), nil, MaxComponentLength, Flag, nil, 0);

      ListItem := ListView.Items.Add;
      if Volume = '' then Volume := 'BRAK';
      ListItem.Caption := Chr(i) + ':\ (' + Volume + ')';  // wyświetlenie litery i etykiety dysku

    { pobieranie dodatkowych informacji do dysków }  
      GetDiskFreeSpace(PChar(Chr(i) + ':\'), SectorsPerClusters, BytesPerSector,
      FreeClusters, TotalClusters);

      ListItem.SubItems.Add(IntToStr(SectorsPerClusters));
      ListItem.SubItems.Add(IntToStr(BytesPerSector));
      ListItem.SubItems.Add(IntToStr(FreeClusters));
      ListItem.SubItems.Add(IntToStr(TotalClusters));

      { numer dysku }
      DriveByte := (i ? Ord('A') + 1);

      { dodanie do komponentu danych (w kB) }
      ListItem.SubItems.Add(IntToStr(DiskSize(DriveByte) div 1024) + ' kB');
      ListItem.SubItems.Add(IntToStr(DiskFree(DriveByte) div 1024) + ' kB');


      case Drive_Type of
      { w zależności od rodzaju dysku wyświetl ikonę }
        DRIVE_CDROM: ListItem.ImageIndex := 0;
        DRIVE_FIXED: ListItem.ImageIndex := 1;
        else ListItem.ImageIndex := ?1;
      end;
    end;
  end;
end;


end.

Praktycznie cały kod wykonywany jest w jednym zdarzeniu ? OnCreate. Rysunek 7.7 przedstawia program w trakcie działania.

7.7.jpg
Rysunek 7.7. Dyski zainstalowane w systemie

Podczas wykonywania pętli na starcie programu sprawdzane jest, czy dysk o danej literze istnieje ? jeżeli tak, pobierana jest jego etykieta oraz dodatkowe informacje. Następnie wszystko zostaje ładnie przedstawione na komponencie TListView w połączeniu z obiektem TImageList. Ostatnia instrukcja (case) sprawdza typ dysku i w zależności od niego wyświetla określoną ikonę:

      case Drive_Type of
      { w zależności od rodzaju dysku wyświetl ikonę }
        DRIVE_CDROM: ListItem.ImageIndex := 0;
        DRIVE_FIXED: ListItem.ImageIndex := 1;
        else ListItem.ImageIndex := ?1;
      end;

Podsumowanie

Mam nadzieję, że w tym rozdziale chociaż trochę zdołałem wzbudzić Twoją ciekawość i chęć tworzenia programów przeznaczonych dla Windows. Wiedza na temat plików na pewno przyda Ci się w przyszłości i będziesz z niej często korzystał, tym bardziej, że obsługa plików w Delphi nie jest przesadnie trudna.

Listingi:

de25kp.jpg Więcej informacji

Delphi 2005. Kompendium programisty
Adam Boduch
Format: B5, stron: 1048
oprawa twarda
Zawiera CD-ROM
[[Delphi/Kompendium|Spis treści]]

[[Delphi/Kompendium/Prawa autorskie|©]] Helion 2003. Autor: Adam Boduch. Zabrania się rozpowszechniania tego tekstu bez zgody autora.

3 komentarzy

Co oznacza znak '?' w poniższej linijce kodu -
else ListItem.ImageIndex := ?1;

No i w tej - { numer dysku }
DriveByte := (i ? Ord('A') + 1);
Proszę o chociaż jakąś podpowiedź.

Odnośnie kasowanie plików to - z tego co wyczytałem w helpie - służy do tego funkcja DeleteFile z SysUtils, natomiast removefile to metoda z IOTAProject.

Zrobiłem książkę adresową tak jak na listingu 7.2 i 7.3 i niby wszystko działa ; dodaje i kasuje wpisy jednak za kazdym razem kiedy to robi wyskakuje mi okienko z błędem "Read beyond end of file". O co chodzi?