Rozdział 9. Praca z plikami

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ędą to jednak zagadnienia sprawiające duże problemy — obsługa plików nie jest trudna. Wystarczy tylko znać kilka podstawowych poleceń.

Rozdział zostanie podzielony na dwie części. Najpierw zajmiemy się obsługą plików w systemie Win32, dla biblioteki VCL. W drugiej części rozdziału natomiast omówię obsługę plików na platformie .NET.

     1 Definicja pliku
     2 Pliki tekstowe
          2.1 Inicjalizacja
          2.2 Tworzenie nowego pliku
          2.3 Otwieranie istniejącego pliku
          2.4 Odczyt plików tekstowych
          2.5 Zapis nowych danych w pliku
          2.6 Zapis danych na końcu pliku
     3 Pliki amorficzne
          3.7 Otwieranie i zamykanie plików
          3.8 Tryb otwarcia pliku
          3.9 Zapis i odczyt danych
          3.10 Przykład działania — kopiowanie plików
     4 Inne funkcje operujące na plikach
          4.11 FilePos
          4.12 FileSize
          4.13 Seek
          4.14 Truncate
          4.15 Rename
          4.16 RemoveFile
          4.17 Operacje na ścieżkach dostępu do plików
     5 Funkcje operujące na katalogach
          5.18 mkDir
          5.19 mkDir
          5.20 RemoveDirectory
     6 Pliki typowane
          6.21 Deklaracja
          6.22 Tworzenie pliku i dodawanie danych
          6.23 Odczyt rekordu z pliku
          6.24 Przykład działania — książka adresowa
               6.24.1 Projektowanie interfejsu
               6.24.2 Założenia programu
               6.24.3 Procedura ReadFile
               6.24.4 Kasowanie elementu
     7 Kopiowanie i przenoszenie plików
          7.25 Kopiowanie
          7.26 Przenoszenie pliku
          7.27 Struktura TSHFileOpStruct
     8 Strumienie
          8.28 Podział strumieni
          8.29 Prosty przykład na początek
          8.30 Konstruktor klasy TFileStream
          8.31 Pozostałe metody i właściwości klasy TStream
          8.32 Właściwości
               8.32.5 Position
               8.32.6 Size
          8.33 Metody
               8.33.7 CopyFrom
               8.33.8 Read, ReadBuffer
               8.33.9 Seek
               8.33.10 Write, WriteBuffer
          8.34 Praktyczny przykład
               8.34.11 Rzut okiem na interfejs programu
               8.34.12 Kod źródłowy programu
     9 Wyszukiwanie
          9.35 Rekord TSearchRec
          9.36 Jak zrealizować wyszukiwanie?
               9.36.13 FindFirst
               9.36.14 FindNext
               9.36.15 FindClose
          9.37 Rekurencja
          9.38 Praktyczny przykład
               9.38.16 Wyszukiwanie plików
     10 Informacja o dyskach
          10.39 Pobieranie listy dysków
          10.40 Pobieranie informacji o rozmiarze dysku
          10.41 Pobieranie dodatkowych informacji
     11 Obsługa plików w .NET
          11.42 Klasy przestrzeni nazw System.IO
          11.43 Praca z plikami
               11.43.17 Kopiowanie oraz przenoszenie
               11.43.18 Pobieranie informacji o plikach
          11.44 Praca z katalogami
          11.45 Strumienie
          11.46 Praca z plikamin
     12 Test
     13 FAQ
     14 Podsumowanie

W tym rozdziale:

*napiszę, w jaki sposób korzystać z podstawowych funkcji operujących na plikach tekstowych;
*opowiem, w jaki sposób można kopiować, kasować czy przenosić swoje pliki;
*przedstawię sposoby korzystania ze strumieni;
*pokażę, w jaki sposób utworzyć wyszukiwarkę oraz jak tworzyć listę plików znajdujących się w danym katalogu;
*określę, na czym polega algorytm rekurencji.

Definicja pliku

Plikiem nazywamy ciąg danych o skończonej długości, które są zapisane w module. Przeważnie jest to system plików na dysku lub innym nośniku danych.
Możemy wyróżnić kilka rodzajów plików, spośród których na uwagę zasługują:

*Katalog — specjalny plik zawierający spis odwołań do innych plików (w tym również do innych katalogów).
*Dowiązanie symboliczne — skrót, odwołanie do innego pliku. Wszelkie operacje dokonywane na tym pliku będą w rzeczywistości dotyczyły pliku, na który wskazuje skrót.
*Pliki wykonywalne — pliki z rozszerzeniem .exe, skrypty zawierające program do wykonania.

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

W Delphi (przypominam, że chodzi o Win32) operacje na plikach są wykonywane za pomocą kilku podstawowych funkcji. Aby edytować plik (zapisać lub odczytać dane), najpierw musimy ów plik otworzyć. Po zakończeniu operacji plik musi zostać zamknięty.

Przed utworzeniem nowego pliku lub otwarciem już istniejącego, wymagana jest jego inicjalizacja. W rzeczywistości jest to przypisanie konkretnego pliku do jakiejś zmiennej, co jest realizowane za pomocą polecenia 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 — @@TF@@ z plikiem C:\plik.txt. Taka konstrukcja jest wymagana, aby rozpocząć dalszą pracę z plikiem. Pierwszy parametr musi być zmienną typu TextFile, a kolejny — ścieżką do pliku.

Funkcja AssignFile, jak i inne tego typu, należy do funkcji wbudowanych. Oznacza to, że ich deklaracja nie znajduje się nigdzie, w żadnym module, ale są obecne w języku Delphi.

Tworzenie nowego pliku

Do tworzenia nieistniejącego wcześniej pliku służy 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 zmienna typu TextFile. Drugim parametrem na razie nie trzeba się przejmować, gdyż jest on opcjonalny. Tak się składa, że jedna funkcja (w tym przypadku — 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 — zostanie wykonana instrukcja CloseFile, zwalniająca zmienną @@TF@@.

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 ] );

Pierwszym parametrem jest — tak samo jak w przypadku polecenia Rewrite — nazwa zmiennej typu TextFile. Drugi parametr jest parametrem opcjonalnym, znów 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 otwierany plik istnieje. Jeśli takiego pliku nie ma, 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

Większość Czytelników zapewne pamięta, że we wcześniejszych fragmentach tej książki (a dokładnie w rozdziale 3.) wspominałem o funkcjach Read i Readln. Te dwie funkcje służą także do odczytywania informacji z plików tekstowych. Spójrzmy 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 zostanie przypisana zawartość wiersza.

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

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

Prawdopodobnie taki zapis będzie rzadko używany, 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 Delphi.

Zapis nowych danych w pliku

Aby zapisać nowe dane w pliku, należy skorzystać z funkcji Writeln i Write. Wspominałem o nich już w rozdziale 2., ale tym razem sposób ich użycia — choć istnieje podobieństwo — nieznacznie się różni.

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

Writeln(TF, S);

Oto przykład udoskonalenia programu, który został zaprezentowany nieco wcześniej. 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 liczbę wierszy znajdujących się w komponencie.

Zamiast instrukcji Writeln można zastosować także Write. Różnica pomiędzy tymi dwoma poleceniami polega na tym, że to drugie 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 trzeba zapisać jakieś dane do pliku, ale dopisując je tylko do jego końca. W takim przypadku przydatna staje się funkcja Append. Umożliwia ona 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 tekstowych, choć istnieją polecenia specyficzne właśnie dla plików amorficznych.

Otwieranie i zamykanie plików

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

Polecenie Append, prezentowane w poprzednim podpunkcie, może być używane jedynie w odniesieniu do plików tekstowych.

Oto przykładowy kod, służący do otwarcia i zamknięcia pliku wybranego przez użytkownika:

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. Wcześniej wspomniana procedura Reset posiada drugi parametr (opcjonalny), który określa wielkość rekordu używanego podczas operacji na plikach. Drugi parametr procedury Reset ma znaczenie jedynie w stosunku do 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 Delphi 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ślną wartością tej zmiennej jest 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 uzyskiwania dostępu do plików amorficznych — nie tekstowych!

Tabela 10.1 zawiera wartości, które mogą być przypisane do zmiennej FileMode lub które będą wykorzystywane podczas omawiania strumieni.
Tabela 10.1. Wartości określające dostęp do pliku

Wartość Opis
fmCreate Jeżeli plik nie istnieje — zostanie utworzony, a w przeciwnym wypadku zostanie otwarty.
fmOpenRead Plik zostanie otwarty jedynie do odczytu.
fmOpenWrite Plik zostanie otwarty do zapisu.
fmOpenReadWrite Plik zostanie otwarty zarówno do zapisu, jak i do odczytu.
fmShareExclusive Dostęp do pliku jest niemożliwy z poziomu innych programów.
fmShareDenyWrite Zapis z poziomu innych programów jest zabroniony.
fmShareDenyRead Odczyt z poziomu innych programów jest zabroniony.
fmShareDenyNone Peł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ójrzmy 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 zostały zadeklarowane 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 do 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, następnym — nazwa bufora, a trzeci to rozmiar danych zapisywanych w pliku. Funkcja SizeOf zwraca rzeczywisty rozmiar, w tym przypadku — tablicy.

Po uruchomieniu programu można się zorientować, że na dysku powstanie plik — na pozór — tekstowy. Jednak po sprawdzeniu jego rozmiaru okaże się, że niepozorny plik tekstowy zajmuje aż 256 bajtów! Jest to spowodowane tym, że tablica Buffer zajmuje 256 bajtów (pamiętajmy — 0 też jest elementem tablicy).

Przykład działania — kopiowanie plików

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

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 służącej do kopiowania pliku.

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

10.2.jpg
Rysunek 10.2. Wygląd programu

Po naciśnięciu przycisku zostanie otwarte 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 jest przedstawiony na listingu 10.1. Po zapoznaniu się z nim należy również przeczytać zamieszczone poniżej omówienie.

Listing 10.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.

Powyższy 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żmy, że wykorzystałem funkcję ExtractFileName. Służy ona do pobrania samej nazwy pliku z pełnej ścieżki.

Wcześniej zastosowałem nieomawianą 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ż jest wymagane co najmniej jednokrotne wykonanie pętli (co gwarantuje właśnie ta pętla). Fragment danych jest przypisywany 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 porcji informacji jest możliwe dzięki następującemu wierszowi kodu:

BlockWrite(DstFile, Buffer, Bytes);

Drugi parametr (@@Buffer@@) jest wskazaniem bufora, a trzeci — oznacza 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.

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 opowiem jednak w dalszej części rozdziału.

Inne funkcje operujące na plikach

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

FilePos

function FilePos(var F): Longint;

Funkcja FilePos zwraca rezultat w postaci pozycji, której właśnie dotyczą 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);

Funkcja Seek (jak zresztą wszystkie powyższe funkcje) działa jedynie w odniesieniu do plików amorficznych i służy do wyszukiwania określonego miejsca w 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 wyszukujemy środek 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 usunięcia pojedynczego pliku można wywołać procedurę RemoveFile. Parametr @@AFileName@@ powinien zawierać ścieżkę do kasowanego pliku.

Operacje na ścieżkach dostępu do plików

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

Tabela 10.2. Funkcje do uzyskiwania danych ze zmiennej String

Funkcja Opis
ExtractFileDir Z kompletnej ścieżki pliku pobiera jedynie nazwę katalogu, w którym znajduje się plik — np.: C:\Windows\System.
ExtractFileDrive Funkcja ze ścieżki zwraca jedynie literę dysku, na którym znajduje się dany plik.
ExtractFileExt Funkcja zwraca rozszerzenie nazwy pliku.
ExtractFileName Z podanej w parametrze ścieżki jest zwracana jedynie nazwa pliku.
ExtractFilePath Funkcja działa podobnie jak ExtractFileDir, z tą różnicą, że zwraca nazwę katalogu ze znakiem \ na końcu: *C:\Windows\System*.
ExtractShortPathName Zwraca skróconą ścieżkę — np. C:\Progra1\MyComp1\MyApp\MyApp.exe.

Funkcje operujące na katalogach

W Delphi w prosty sposób możemy operować na folderach, korzystając z poniższych funkcji. Co prawda nie ma ich zbyt dużo, ale do wykonywania 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;

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;

RemoveDirectory

function RemoveDir(const Dir: string): Boolean;

Funkcja RemoveDir także służy do usuwania katalogu podanego w parametrze. 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ąć folderu.

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

Funkcja nie zwraca komunikatu o błędzie w razie nieudanej próby usunięcia katalogu, 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. Informacje potrzebne do wykonania tego zadania zostaną zaprezentowane w dalszej części tego rozdziału.

Pliki typowane

Pliki typowane są kolejnym rodzajem plików. Mogą okazać się bardzo przydatne podczas pisania programów. Dotąd przedstawiłem 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żna dowolnie odczytywać lub zapisywać, co pozwala nawet na zbudowanie prostej bazy 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 będą się składały 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.

Pliki typowane nie działają na platformie .NET (w VCL.NET).

Tworzenie pliku i dodawanie danych

Oto pierwsza zasada: w odniesieniu do plików typowanych nie można używać polecenia 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 znajduje się aplikacja. 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 Czytelnik mógł lepiej utrwalić sobie wiadomości dotyczące plików typowanych, proponuję wykonanie ćwiczenia, którego celem jest utworzenie 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.

Okno programu przedstawiłem na rysunku 10.3.

10.3.jpg
Rysunek 10.3. Program — książka adresowa

Projektowanie interfejsu

Opisywany program będzie się składał z dwóch formularzy: jeden jest przedstawiony na rysunku 10.3, drugi natomiast służy 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 na dopasowanie rozmiaru 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@@.
</dfn>

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

10.4.jpg
Rysunek 10.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 niczego nadzwyczajnego.

Założenia programu

Program ma być prostą bazą danych, składającą się z paru rekordów. Po każdym uruchomieniu programu jest wywoływana 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 jeśli 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 odczytywanie w pętli kolejnych rekordów. W tym przypadku funkcja FileSize zwraca liczbę rekordów znajdujących się w pliku, tak więc w każdej iteracji jest wykonywane 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

Delphi nie posiada procedury umożliwiającej kasowanie konkretnego rekordu z pliku — należy tę funkcję zaprogramować samemu. Jak to wygląda w praktyce? W miejsce pliku z bazą danych należy 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 dość prosta — jest to dodawanie poszczególnych elementów z komponentu TListView do pliku, z pominięciem elementu zaznaczonego.

Na listingu 10.2 przedstawiłem kod źródłowy całego modułu MainFrm.pas, a listing 10.3 zawiera kod źródłowy drugiego modułu, służącego do dodawania nowych pól.

Listing 10.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 takiego samego numeru, 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 10.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ą wpisania 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 wygląda następująco:

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

Pierwszym parametrem musi być ścieżka dostępu 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żna 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ć istniejąca ś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 ten jest używany jedynie w połączeniu z odpowiednimi funkcjami, co prezentuję poniżej:

  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@@ — wykonywana funkcja — patrz tabela 10.3.
*@@pFrom@@ — ścieżka dostępu do 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 10.4.
*@@fAnyOperationsAborted@@ — jeżeli użytkownik przerwie operacje przed jej zakończeniem, parametr ten będzie zawierał wartość True.
*@@hNameMappings@@ — parametr jest uwzględniany 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 10.3. Możliwe wartości parametru wFunc
Parametr ||Opis

FO_COPY Kopiowanie plików. Używane są wówczas parametry wFrom i wTo.
FO_DELETE Usuwanie plików. Parametr wFrom określa ścieżkę do usunięcia. wTo jest ignorowany.
FO_MOVE Przeniesienie pliku. Używane są wówczas parametry wFrom i wTo.
FO_RENAME Zmienia nazwę pliku określonego parametrem wFrom. wTo zawiera nową nazwę.

Tabela 10.4. Możliwe wartości parametru fFlags

Parametr Opis
FOF_ALLOWUNDO Umożliwia cofnięcie operacji w razie konieczności.
FOF_FILESONLY Zezwala na wykonywanie operacji jedynie na plikach.
FOF_NOCONFIRMATION Nie wyświetla przycisku Tak na wszystkie, jeżeli jest to konieczne.
FOF_NOCONFIRMMKDIR Nie wyświetla zapytania o utworzenie katalogu, jeżeli jest to konieczne (tworzy go automatycznie).
FOF_RENAMEONCOLLISION Jeżeli plik istnieje, zmienia jego nazwę.
FOF_SILENT Nie wyświetla okna z paskiem postępu operacji.
FOF_SIMPLEPROGRESS Wyświetla okno postępu, ale nie pokazuje nazwy pliku.
FOF_WANTMAPPINGHANDLE Bardziej 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 działać poprawnie, należy na końcu wywołać funkcję SHFileOperation, której parametrem będzie wskazanie struktury TShFileOpStruct.

Oto przykład programu kopiującego plik 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 wyświetli zapytanie, czy należy utworzyć katalog C:\kopia (jeżeli nie istnieje), a dopiero później skopiuje dane.

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ć szczegółowe objaśnienia.

Dzięki strumieniom można 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 przenoszenie 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 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 (ciągi znakowe).
*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.

Prosty przykład na początek

Pierwszy przykład to zapis danych — właściwości danego komponentu. Wystarczy w tym przypadku 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 tu podać nazwę komponentu, którego właściwości zostaną zapisane w pliku.

Odczytanie 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 jest realizowane przez procedurę ReadComponent. W parametrze musi znaleźć się nazwa komponentu, do którego program przypisze dane z pliku (listing 10.4).

Listing 10.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 podawania 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 10.5.

Tabela 10.5. Możliwe tryby otwarcia pliku

Tryb Opis
fmCreate Plik ma zostać utworzony. Jeżeli istnieje, zostanie otwarty do zapisu.
fmOpenRead Plik zostanie otwarty do odczytu.
fmOpenWrite Plik zostanie otwarty do zapisu.
fmOpenReadWrite Plik zostanie otwarty zarówno do zapisu, jak i do odczytu.
fmShareExclusive Inne aplikacje nie będą mogły otworzyć pliku, podczas gdy dana aplikacja korzysta z owego pliku.
fmShareDenyWrite Inne aplikacje mogą otwierać plik, ale tylko do odczytu.
fmShareDenyRead Inne aplikacje mogą otwierać plik jedynie do zapisu.
fmShareDenyNone Brak 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 +.

Należy pamiętać 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 niniejszym podrozdziale 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 sposób deklarowania owych metod i właściwości. Ich wykorzystanie praktyczne zaprezentuję nieco dalej.

Właściwości

Position

property Position: Int64;

Właściwość @@Position@@ określa położenie w danym strumieniu. Zwracana przez nią wartość określa liczbę bajtów, jakie zostały już odczytane. Przykładowo, jeżeli odczytywany plik ma wielkość 1 000 bajtów, można przejść do określonego miejsca tego pliku. Do tego służy metoda Seek. Jeżeli przesuniemy się np. na 300. bajt, właściwość @@Position@@ zwróci wartość 300 (położenie w pliku).

Size

property Size: Int64;

Właściwość Size zwraca wielkość odczytywanego pliku — strumienia. Wartość jest podawana w bajtach.

Metody

CopyFrom

function CopyFrom(Source: TStream; Count: Int64): Int64;

Funkcja CopyFrom jest używana wtedy, gdy trzeba skopiować dane z jakiegoś strumienia do innego strumienia. Pierwszy parametr musi być nazwą klasy typu TStream lub pochodną. Natomiast drugi parametr oznacza liczbę bajtów, które mają być przypisane drugiemu strumieniowi.

Read, ReadBuffer

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 liczbę bajtów, które zostały dotychczas odczytane.

Seek

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 nawigowania w obrębie pliku i do ustawiania znacznika odczytu na konkretnej pozycji. Funkcja ta działa tak samo jak funkcja systemowa Seek, która była omawiana w trakcie opisywania plików amorficznych i typowanych.

Jak widać, są dostępne dwie funkcje — w zależności od rodzaju parametrów system wybierze jedną z nich. Pierwszym parametrem musi być liczba bajtów, o jaką zostanie wykonany „skok”. Natomiast kolejny parametr określa sposób interpretowania wartości @@Offset@@ (wartości możliwe do użycia w tym parametrze znajdują się w tabeli 10.6).

Tabela 10.6. Możliwe wartości parametru Origin

Wartość Opis
soFromBeginning Przesunięcie odbędzie się od początku zasobu.
soFromCurrent Wartość Offset określa liczbę bajtów, o jaką nastąpi przesunięcie — począwszy od dotychczasowej pozycji.
soFromEnd Przesunięcie odbędzie się od tyłu.

Write, WriteBuffer

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 — liczbę bajtów, która ma zostać dołączona do strumienia.

Jedna różnica dzieląca te polecenia polega na tym, że Write zwraca liczbę bajtów zapisanych w strumieniu.

Praktyczny przykład

Jak dotąd, o strumieniach pisałem raczej teoretycznie, nie podając praktycznych przykładów wykorzystania. Zaprezentuję wobec tego teraz taki przykład — dzielenie plików. Aplikacja, 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 został przedstawiony na rysunku 10.5.

10.5.jpg
Rysunek 10.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 istnieje możliwość wyświetlania struktury katalogów na danej partycji oraz zawartość zaznaczonego folderu.

Aby ustalić rozmiar 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 omawianym programie są 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 druga spowoduje połączenie wszystkich plików, znajdujących się w określonym katalogu, w tym przypadku określonym parametrem @@Dir@@.

Najpierw przyjrzyjmy 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 nowo utworzonym 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 liczba 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 pierwszy rzut oka 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 liczby iteracji pętli for. Liczbę tę uzyskuje się po podzieleniu rozmiaru strumienia przez rozmiar buforu :

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 jest tworzony nowy strumień (plik), do którego program dodaje kolejne 500 bajtów danych.

Łączenie plików jest sprawą o tyle skomplikowaną, że niektórych instrukcji jeszcze nie opisywałem w tej książce. Zaprezentuję 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 liczby plików .temp znajdujących się
  w określonej lokalizacji – liczbę 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 utworzenie 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 następuje wyszukanie wszystkich plików znajdujących się w danym katalogu. Następnie, po pobraniu liczby plików (opowiem o tym nieco dalej), następuje wykonanie pętli odczytującej zawartość tych wszystkich plików i kolejno dodającej dane do jednego strumienia (listing 10.5).

Listing 10.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 liczbę 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 nowo utworzonym 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 liczba 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 liczby plików .temp znajdujących się
  w określonej lokalizacji – liczbę 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 utworzenie 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;

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

end.

Wyszukiwanie

W poprzednim podpunkcie podczas omawiania strumieni zamieściłem przykład, w którym zostały wykorzystane funkcje, których dotychczas nie przedstawiłem. 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 utworzenie listy wszystkich plików.

Rekord TSearchRec

Przed rozpoczęciem korzystania z funkcji wyszukiwania trzeba zapoznać się ze strukturą rekordu TSearchRec. Po wykonaniu odpowiednich instrukcji w owym rekordzie zostaną zapisane 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 można użyć instrukcji FindFirst oraz FindNext. Wówczas należy w parametrze podać zmienną wskazującą rekord TSearchRec. Ów rekord dostarcza informacji o rozmiarze pliku, czasie jego utworzenia oraz atrybutach.

Jak zrealizować wyszukiwanie?

Zrealizowanie procedury przeszukania dysku wcale nie jest takie trudne. W tym celu można się posłużyć trzema podstawowymi funkcjami z modułu SysUtils, które w połączeniu z rekordem TSearchRec dają 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ą zostać uwzględnione (tabela 10.7). Parametr ostatni — @@F@@ — to wskazanie na rekord TSearchRec.

Tabela 10.7. Atrybuty plików

Nazwa atrybutu Opis
faReadOnly Pliki tylko do odczytu.
faHidden Pliki ukryte.
faSysFile Pliki systemowe.
faDirectory Katalogi.
faArchive Archiwa.
faAnyFile Dowolne pliki.

Atrybuty plików mogą być ze sobą połączone za pomocą operatora +. Przykładowo, jeżeli program ma 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 przypadku zostanie zwrócony numer błędu.

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

Powyższy wpis 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 nazwy plików. W takim przypadku będą uwzględniane 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 musi zostać podany 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 Czytelnik zapewne będzie często spotykał się z terminem rekurencja. 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 może zostać wykorzystana podczas tworzenia kodu służącego do wyszukiwania. Wygląda to tak: użytkownik wykonuje określone czynności, które mają na celu 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 uzyskuje również listę katalogów. Następuje wtedy wykonanie kluczowej operacji (rekurencji), 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 umiejętności Czytelnika opierały się jedynie na teorii. Dlatego też na płycie CD-ROM w katalogu listingi/10/Searching umieściłem pełny kod programu realizującego wyszukiwanie.
Program w trakcie działania przedstawiłem na rysunku 10.6.

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

Wyszukiwanie plików

Wyszukiwanie plików jest realizowane 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);  // zakoń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ę Czytelnikowi zagmatwany i niezrozumiały, a w przekonaniu tym może utwierdzić duża liczba 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. Służy do sprawdzenia, czy na końcu podanej w parametrze ścieżki znajduje się znak . Jeżeli tego znaku nie ma, zostanie 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 następuje odczytanie zawartości rekordu TSearchRec. Aby szukać dalej, należy wywołać funkcję FindNext.

Kolejnym etapem jest wywołanie kolejnego polecenia FindFirst, które tym razem ma 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.

Zapewne łatwo zauważyć, że zawsze sprawdzam, czy nazwa znalezionego pliku nie jest znakiem kropki (.) lub dwoma kropkami (..). W ten sposób system zaznacza, że istnieje katalog nadrzędny.

Pełny kod źródłowy programu znajduje się na listingu 10.6.
Listing 10.6. Kod źródłowy wyszukiwarki

{
   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 podrozdziale zajmiemy się uzyskiwaniem podstawowych informacji o dyskach znajdujących się w systemie, a także dokładniejszych danych, takich jak pojemność partycji, ilość wolnego miejsca itp.

Pobieranie listy dysków

Dyski, napędy i partycje w systemie Windows mogą być oznaczane różnymi literami — od A do Z. Aby pobrać listę wszystkich dysków, należy wykonać pętlę:

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

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

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

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

Wartość Opis
0 Typ dysku nie jest możliwy do określenia.
1 Napęd o tym oznaczeniu nie istnieje.
DRIVE_REMOVABLE Dyskietka lub napęd wymienny.
DRIVE_FIXED Napęd niewymienny.
DRIVE_REMOTE Napęd sieciowy.
DRIVE_CDROM CD-ROM.
DRIVE_RMADISK RAM-dysk, czyli dysk wirtualny.

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

GetDriveType('C:\');

W takim przypadku system zwróci informację na temat dysku C:.

Pobieranie informacji o rozmiarze dysku

Pobranie informacji o ilości wolnego miejsca oraz o pojemności konkretnego dysku jest bardzo proste dzięki dwóm funkcjom: DiskSize oraz DiskFree. Obydwie przyjmują parametr w postaci oznaczenia dysku — np. Ord('C'), a zwrócona wartość będzie zawierała liczbę wolnych bajtów oraz łączną liczbę 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ładowy fragment kodu dodaje do komponentu TListView informacje o pojemności oraz ilości wolnego miejsca, wyrażonej jako liczba kilobajtów.

Pobieranie dodatkowych informacji

W celu pobrania dodatkowych informacji na temat dysku (takich jak liczba 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ą wskazywać na zmienną typu DWORD. Znaczenie poszczególnych parametrów jest następujące:

*lpRootPathName — ścieżka (litera oznaczająca dysk), z którego będą pobieranie informacje. Może to być np. C:.
*lpSectorsPerCluster — liczba sektorów przypadających na klaster.
*lpBytesPerSector — liczba bajtów przypadających na jeden sektor
*lpNumberOfFreeClusters — liczba wolnych klastrów.
*lpTotalNumberOfClusters — łączna liczba 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ę na listingu 10.7.

Listing 10.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 przeszukująca wszystkie oznaczenia literowe }
  for I := Ord('A') to Ord('Z') do
  begin
  // uzyskanie litery oznaczającej dysk
    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 o dyskach }  
      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 jest wykonywany w jednym zdarzeniu — OnCreate. Rysunek 10.7 przedstawia okno programu w trakcie działania.

10.7.jpg
Rysunek 10.7. Dyski zainstalowane w systemie

Podczas wykonywania pętli na początku programu, następuje sprawdzenie, czy dysk oznaczony daną literą 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;

Obsługa plików w .NET

Do tej pory omawiałem obsługę plików w systemie Win32. Oczywiście można wykorzystać te mechanizmy w aplikacjach .NET, korzystając z biblioteki VCL.NET.
Niekiedy można próbować zaimplementować obsługę plików czy strumieni bez korzystania z biblioteki VCL.NET, uwzględniającej jedynie klasy dostępne w .NET Framework. Pokrótce omówię więc podstawowe klasy, związane z obsługą plików i strumieni w .NET.

Klasy związane z obsługą plików, strumieni i katalogów są zawarte w przestrzeni nazw System.IO.

Klasy przestrzeni nazw System.IO

Do operowania na plikach i katalogach możemy wykorzystać klasy opisane w tabeli 10.9. Są to klasy zawierające metody statyczne, stąd nie trzeba tworzyć egzemplarza klasy, aby na nich operować.

Tabela 10.9. Klasy z przestrzeni nazw System.IO

Klasa Opis
Path Klasa służy do przetwarzania informacji o ścieżkach katalogów oraz plików.
File Klasa udostępnia podstawowe mechanizmy, pozwalające na tworzenie, usuwanie oraz przenoszenie plików.
Directory Udostępnia metody służące do przenoszenia, kopiowania czy usuwania katalogów.

Przestrzeń System.IO udostępnia także klasy, które wymagają utworzenia instancji klasy, takie jak FileInfo oraz DirectoryInfo.

Praca z plikami

Jednym ze sposobów tworzenia pliku i zapisania w nim treści, jest użycie klasy File i związanej z nią metody CreateText. Oto przykład utworzenia na dysku C: pliku o nazwie plik.txt, i zapisania w nim zdania Hello World!:

var
  S : StreamWriter;

begin
  S := &File.CreateText('C:\plik.txt');
  S.Write('Hello World!');
  S.Close;
end.

Metoda CreateText zwraca obiekt klasy StreamWriter, który umożliwia zapisywanie treści w pliku. Metoda Write zapisuje dane do pliku. Istnieje możliwość użycia metody WriteLine, która zapisze dane i umieści na końcu znak nowej linii. Po zakończeniu pracy nad plikiem należy go zamknąć metodą Close.

Każde wywołanie powyższego kodu spowoduje nadpisanie pliku plik.txt, jeżeli takowy istnieje. Dlatego należy wykorzystać metodę Exists która sprawdza, czy plik o podanej nazwie istnieje:

var
  S : StreamWriter;

begin
  if &File.Exists('C:\plik.txt') then
    { otwarcie }
  else
  S := &File.CreateText('C:\plik.txt');
  { utworzenie pliku }

  S.Write('Hello World!');
  S.Close;
end.

Aby odczytać zawartość pliku, zamiast metody CreateText trzeba użyć funkcji OpenText. Funkcja zwraca obiekt klasy StreamReader. O strumieniach w .NET będę jednak pisał w dalszej części rozdziału.

Kopiowanie oraz przenoszenie

Kopiowanie oraz przenoszenie plików można zrealizować za pomocą metod Move oraz Copy. Oto przykład:

  if &File.Exists('C:\plik.txt') then
    &File.Copy('C:\plik.txt', 'D:\plik.txt');

Pierwszy parametr określa ścieżkę pliku docelowego, a drugi — ścieżkę docelową. Metoda Move działa podobnie:

&File.Move('C:\plik.txt', 'D:\plik.txt');

W równie prosty sposób można usunąć plik, wywołując metodę Delete:

&File.Delete('C:\plik.txt');

W tabeli 10.10 są przedstawione najważniejsze metody klasy File.
Tabela 10.10. Najważniejsze metody klasy File

Metoda Opis
AppendText Metoda otwiera plik i umożliwia dopisanie treści. Zwraca obiekt klasy StreamWriter.
Copy Metoda kopiuje plik do podanej lokalizacji.
CreateText Metoda tworzy plik i umożliwia zapisywanie treści. Zwraca obiekt klasy StreamWriter.
Delete Kasuje plik znajdujący się w podanej lokalizacji.
GetAttributes Zwraca atrybuty dotyczące danego pliku.
GetCreationTime Zwraca czas utworzenia danego pliku. Wartość zwraca w formie typu DateTime.
GetLastAccessTime Zwraca czas ostatniego dostępu do pliku. Wartość zwraca w formie typu DateTime.
GetLastWriteTime Zwraca czas ostatniego zapisu pliku. Wartość zwraca w formie typu DateTime.
Move Metoda przenosi plik do podanej lokalizacji.
OpenRead Metoda otwiera plik do odczytu i zwraca obiekt klasy FileStream.
SetAttributes Ustawia atrybuty danego pliku.
SetCreationTime Metoda ustawia datę utworzenia pliku. W parametrach należy podać ścieżkę do pliku oraz datę w formie DateTime.
SetLastAccessTime Metoda ustawia datę ostatniej modyfikacji pliku.
SetLastWriteTime Metoda ustawia datę ostatniego zapisu do pliku.

Pobieranie informacji o plikach

Podsumowując to, co zostało zaprezentowane w tabeli 10.10, możemy zaprezentować prostą aplikację pobierającą informacje o danym pliku. Realizuje to poniższy kod:

procedure TWinForm2.btnGetInfo_Click(sender: System.Object; e: System.EventArgs);
var
  FI : FileInfo;
begin
  if OpenFileDialog1.ShowDialog = System.Windows.Forms.DialogResult.Ok  then
  begin
    FI := FileInfo.Create(OpenFileDialog1.FileName);

    lbInfo.Items.Clear;
    lbInfo.Items.Add(System.Object('Ścieżka pliku: ' + FI.FullName));
    lbInfo.Items.Add(System.Object('Rozmiar: ' + FI.Length.ToString));
    lbInfo.Items.Add(System.Object('Data utworzenia: ' + FI.CreationTime.ToString));
    lbInfo.Items.Add(System.Object('Ostatnia modyfikacja: ' + FI.LastAccessTime.ToString));
    lbInfo.Items.Add(System.Object('Data zapisu: ' + FI.LastWriteTime.ToString));
    lbInfo.Items.Add(System.Object('Rozszerzenie: ' + FI.Extension));
    lbInfo.Items.Add(System.Object('Atrybuty: ' + Enum(FI.Attributes).ToString));
  end;
end;

Informacje o pliku są przedstawione w komponencie ListBox (nazwałem go lbInfo).

Warto zwrócić uwagę, iż w tym przykładzie skorzystałem z klasy FileInfo i utworzyłem jej egzemplarz. W tej klasie nie posługiwałem się metodami, które zostały zaprezentowane w tabeli 10.10, ale dane odczytywałem z właściwości klasy.

Praca z katalogami

Praca z katalogami w .NET jest równie łatwa jak praca z plikami. Do dyspozycji pozostaje klasa Directory oraz DirectoryInfo, które posiadają metody i właściwości podobne do tych z klasy File oraz FileInfo.

Przykładowo, utworzenie katalogu polega na wywołaniu metody CreateDirectory z klasy Directory:

  Directory.CreateDirectory('C:\katalog');

Klasa Directory także posiada metodę Move, lecz nie posiada funkcji Copy. W celu skopiowania zawartości katalogu trzeba napisać własną funkcję, która odczyta wszystkie pliki znajdujące się w katalogu oraz skopiuje każdy z osobna do nowej lokalizacji.

Główne metody z klasy Directory znajdują się w tabeli 10.11.
Tabela 10.11. Metody klasy Directory

Nazwa metody Opis
CreateDirectory Tworzy katalog określony w parametrze.
Delete Metoda usuwa katalog podany w parametrze.
Exists Metoda sprawdza, czy katalog o podanej nazwie istnieje.
GetCreationTime Pobiera czas utworzenia katalogu.
GetDirectories Metoda pobiera nazwy podkatalogów danego folderu.
GetFiles Metoda pobiera listę plików z danego katalogu.
Move Metoda realizuje przenoszenie katalogu do innej lokalizacji.
GetParent Metoda pobiera informacje na temat katalogu nadrzędnego.
GetFileSystemEntries Metoda pobiera zarówno katalogi jak i pliki z danego folderu.

Wydaje mi się, że opis zaprezentowany w tabeli 10.11 jest jasny i Czytelnik nie będzie miał problemów z posługiwaniem się metodami klasy Directory.

Zaprezentuje teraz przykład kopiowania katalogów wraz z podkatalogami w .NET. Takiej funkcji nie ma w .NET, więc trzeba napisać własną:

procedure TWinForm2.CopyDir(SourceDir, TargetDir: String);
var
  Files : array of String;
  i : Integer;
  Attr : FileAttributes;
  srcFile : String;
begin
  { tutaj sprawdzamy, czy na końcu ścieżek znajdują się znaki \ }
  if SourceDir[SourceDir.Length-1] <> Path.DirectorySeparatorChar then
    SourceDir := SourceDir + Path.DirectorySeparatorChar;

  if TargetDir[TargetDir.Length-1] <> Path.DirectorySeparatorChar then
    TargetDir := TargetDir + Path.DirectorySeparatorChar;

  { pobranie listy plików i katalogów }
  Files := Directory.GetFileSystemEntries(SourceDir);

  { utworzenie katalogu docelowego }
  Directory.CreateDirectory(TargetDir);

  for I := Low(Files) to High(Files) do
  begin
    { pobranie atrybutów pliku }
    Attr := &File.GetAttributes(Files[i]);

    { pobranie nazwy pliku, bez pełnej ścieżki }
    srcFile := Path.GetFileName(Files[i]);

    { sprawdzenie, czy plik jest katalogiem }
    if (Attr and FileAttributes.Directory) = FileAttributes.Directory then
    begin
      if not Directory.Exists(TargetDir + srcFile) then
        Directory.CreateDirectory(TargetDir + srcFile);

      { wywołanie rekurencyjne }
      CopyDir(Files[i], TargetDir + srcFile);
    end else &File.Copy(Files[i], TargetDir + srcFile);
  end;
end;

Procedura posiada dwa parametry — @@SourceDir@@ (katalog źródłowy) oraz @@TargetDir@@ (katalog docelowy). Pierwsze dwie instrukcje warunkowe sprawdzają, czy na końcu ścieżki znajduje się znak ukośnika (). W kolejnych wierszach kodu pobieramy listę katalogów i plików znajdujących się w danym folderze. Dalej w pętli są pobierane atrybuty pliku, gdyż trzeba sprawdzić, czy kopiowany plik nie jest katalogiem. Jeżeli jest, stosujemy rekurencję i kopiujemy zawartość podkatalogu. Jeżeli nie — po prostu kopiujemy plik z katalogu źródłowego do docelowego.

Strumienie

Klasy obsługi strumieni w .NET znajdują się w przestrzeni nazw System.IO. Ich obsługa jest podobna do obsługi strumieni w VCL/VCL.NET. Tak samo można korzystać z różnych klas, w zależności od typu danych. Przykładowo, pracując z danymi znajdującymi się w pliku, korzystamy z klasy FileStream, natomiast w przypadku danych umieszczonych w pamięci — z MemoryStream.

Ponadto klasy strumieni w większości posiadają takie same metody, więc nie powinno być problemów z ich wykorzystaniem.

Praca z plikamin

Na samym początku zaprezentuję najprostszy przykład tworzenia pliku z wykorzystaniem klasy StreamWriter. Utworzenie i zapisanie treści do pliku za pomocą tej klasy jest bardzo podobne jak w przypadku klasy File. Oto fragment kodu:

var
  MyStreamWriter : StreamWriter;
begin
  MyStreamWriter := StreamWriter.Create('C:\plik.txt', False);
  try
    MyStreamWriter.Write('Hello World!');
    MyStreamWriter.Close;
  finally
    MyStreamWriter.Free;
  end;

W konstruktorze klasy StreamWriter podajemy ścieżkę do pliku. Drugi parametr określa, czy do pliku będą dopisywane jakieś informacje, jeżeli ten plik istnieje (wartość True). W opisywanym przykładzie, każde wykonanie tego kodu, spowoduje nadpisanie istniejącego już pliku. Następnie za pomocą metody Write zapisujemy treść do pliku, aby w końcu go zamknąć (metoda Close).

Innym sposobem tworzenia pliku jest powiązanie klasy StreamWriter z klasą FileStream:

var
  MyStreamWriter : StreamWriter;
  MyFileStream : FileStream;
begin
  MyFileStream := FileStream.Create('C:\plik.txt', FileMode.OpenOrCreate);
  MyStreamWriter := StreamWriter.Create(MyFileStream);
  try
    MyStreamWriter.Write('Hello World!');
    MyStreamWriter.Close;
  finally
    MyStreamWriter.Free;
    MyFileStream.Free;
  end;

Na samym początku należy wywołać konstruktor klasy FileStream. W pierwszym parametrze podajemy ścieżkę do pliku, a w drugim — tryb pracy. Wartość FileMode.OpenOrCreate oznacza, że plik zostanie otwarty jeżeli istnieje lub utworzony — jeżeli nie istnieje. Inne wartości FileMode są przedstawione w tabeli 10.12.

Tabela 10.12. Wartości FileMode

Wartość Opis
Append Otwiera lub tworzy plik, a następnie przechodzi na jego koniec.
Create Tworzy nowy plik, a jeżeli ten istnieje — nadpisuje go.
CreateNew Tworzy nowy plik, a jeżeli ten istnieje — generuje wyjątek.
Open Otwiera istniejący plik. Jeżeli plik nie istnieje — generuje odpowiedni wyjątek.
OpenOrCreate Otwiera dany plik, jeżeli istnieje. Jeżeli plik nie istnieje — tworzy nowy.
Truncate Otwiera istniejący plik i zeruje jego rozmiar.

Kolejnym, opcjonalnym parametrem, który może zostać przekazany do konstruktora klasy jest parametr FileAccess — np.:

MyFileStream := FileStream.Create('C:\plik.txt', FileMode.OpenOrCreate, FileAccess.Write);

Możliwe wartości parametru FileAccess przedstawiono w tabeli 10.13.

Tabela 10.13. Wartości FileAccess

Wartość Opis
Read Dane mogą być jedynie odczytywane.
ReadWrite Dane mogą być zarówno odczytywane, jak i zapisywane.
Write Dane mogą być jedynie zapisywane.

Aby odczytać dane z pliku tekstowego, można użyć klasy StreamReader zamiast dotychczasowej StreamWriter. Oto przykład odczytania pliku oraz wyświetlenia jego zawartości na konsoli:

program MyApp;

{$APPTYPE CONSOLE}

uses
  System.IO;

var
  MyStreamReader : StreamReader;
  MyFileStream : FileStream;
begin
  MyFileStream := FileStream.Create('C:\config.txt', FileMode.Open, FileAccess.Read);
  MyStreamReader := StreamReader.Create(MyFileStream);
  try
    while (MyStreamReader.Peek >= 0) do
      Console.WriteLine(MyStreamReader.ReadLine);
  finally
    MyStreamReader.Free;
    MyFileStream.Free;
  end;
  Console.ReadLine;
end.

Metoda Peek zwraca następny znak, który ma zostać odczytany. Jeśli nie ma żadnego znaku, zwraca wartość –1. Metoda ReadLine odczytuje kolejną linię z pliku. W przypadku mniejszych plików, łatwiejsze będzie skorzystanie z metody ReadToEnd, która zwraca całą zawartość pliku:

Console.WriteLine(MyStreamReader.ReadToEnd);

Test

  1. W rzeczywistości do tworzenia nowego pliku na dysku służy funkcja:
    a) Reset,
    b) AssignFile,
    c) Rewrite.
  2. Do korzystania z plików amorficznych można użyć typu:
    a) TextFile,
    b) File,
    c) file of.
  3. Funkcja Truncate powoduje:
    a) Wyczyszczenie zawartości pliku,
    b) Obcięcie zawartości pliku od podanego miejsca,
    c) Przeskoczenie na odpowiednią pozycję w pliku.
  4. W jakim module znajduje się klasa TStream?
    a) SysUtils,
    b) Classes,
    c) Dialogs.

FAQ

Jak odczytać rozmiar pliku?

Najlepiej będzie skorzystać z modułu SysUtils, a konkretnie z rekordu TSearchRec. Oto przykładowa funkcja:

uses SysUtils;

function GetFileSize(FileName : String) : Cardinal;
var
  SearchRec: TSearchRec;
begin
  Result := 0;

  if FindFirst(FileName, faAnyFile, SearchRec) = 0 then
    Result := SearchRec.Size;
  FindClose(SearchRec);
end;

Jak wyświetlić właściwości pliku/katalogu?

Poniższy kod umożliwia wyświetlanie okna właściwości pliku, katalogu lub dysku naszego komputera.

uses
  ShellAPI;

function ShowProperties(FileName: string): Boolean;
var
  ShellInfo: TShellExecuteInfo;
begin
  with ShellInfo do
  begin
    cbSize := SizeOf(ShellInfo);
    fMask := SEE_MASK_NOCLOSEPROCESS or SEE_MASK_INVOKEIDLIST or SEE_MASK_FLAG_NO_UI;
    Wnd := 0;
    lpVerb := 'Properties';
    lpFile := PChar(FileName);
    lpParameters := nil;
    lpDirectory := nil;
    nShow := SW_SHOW;
    hInstApp := 0;
    lpIDList := nil;
  end;

  Result := ShellExecuteEx(@ShellInfo);
end;

W parametrze funkcji należy podać ścieżkę do pliku lub katalogu. Można także użyć tej funkcji w następujący sposób:

ShowProperties('C:');

W takim wypadku są pobierane właściwości dysku C:.

Jak usunąć plik do kosza?

Aby usunąć plik do kosza, należy skorzystać z modułu ShellAPI:

uses
  ShellAPI;

var
  R : TSHFileOpStruct;
begin
  with R do
  begin
    Wnd := 0;
    wFunc := FO_DELETE;
    pFrom := 'c:\video.log';
    fFlags := FOF_ALLOWUNDO;
  end;
  SHFileOperation(R);

  Readln;
end.

Powyższy kod przeniesie plik video.log do kosza (wcześniej system wyświetli żądanie potwierdzenia operacji).

Jak pobrać ścieżkę katalogu Windows i System?

Poniższy kod działa w środowisku Win32. Do uzyskiwania takiej informacji służą funkcje GetWindowsDirectory i GetSystemDirectory:

 var
  WDir : array[0..255] of char;
begin
  GetSystemDirectory(WDir, SizeOf(WDir));
  Label1.Caption := WDir;

Żeby uzyskać ścieżkę katalogu System, należy po prostu zamiast GetWindowsDirectory podstawić GetSystemDirectory.

Istnieje także funkcja GetTempPath — oto sposób jej wykorzystywania:

var
  Buffer: array[0..255] of char;
begin
  GetTempPath(SizeOF(Buffer), Buffer);
  ShowMessage(Buffer);

Jak sprawdzić, czy dany katalog istnieje?

Należy skorzystać z funkcji DirectoryExists, która znajduje się w module SysUtils:

  if DirectoryExists('C:\Windows') then { }

Podsumowanie

Jestem pewien, że w trakcie pisania programów Czytelnik wiele razy będziesz musiał zaprogramować obsługę plików. Mam nadzieję, że wówczas treść zawarta w niniejszym rozdziale okaże się pomocna. Przede wszystkim skupiłem się w nim na przedstawieniu zasad obsługi plików w Win32, lecz myślę, że dzięki lekturze tego rozdziału wykorzystanie odpowiednich klas .NET również nikomu nie sprawi żadnych problemów.

[[Delphi/Vademecum|Spis treści]]

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

0 komentarzy