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.
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.
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:\Progra |
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.
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.
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_WANTMAPPINGHANDL
E. 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.
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.
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.
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
- W rzeczywistości do tworzenia nowego pliku na dysku służy funkcja:
a)Reset
,
b)AssignFile
,
c)Rewrite
. - Do korzystania z plików amorficznych można użyć typu:
a)TextFile
,
b)File
,
c)file of
. - Funkcja
Truncate
powoduje:
a) Wyczyszczenie zawartości pliku,
b) Obcięcie zawartości pliku od podanego miejsca,
c) Przeskoczenie na odpowiednią pozycję w pliku. - 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.