Rozdział 5 - operacje na plikach

Spis treści:

Aplikacje konsolowe
Wyjątki

Selektywna obsługa wyjątków
Słowo kluczowe raise
Try, except oraz finally
Tworzenie własnych wyjątków
Obsługa wyjątków

Pliki tekstowe

Wczytywanie plików
Tworzenie plików
Dopisywanie do plików

Pliki amorficzne

Odczytywanie TAG'ów z plików mp3
Jeszcze trochę o plikach amorficznych

Pliki typowane
Podsumowanie


Delphi w prosty sposób umożliwia modyfikację, tworzenie wczytywanie plików. Wystarczy nauczyć się paru podstawowych komend i to wszystko. Ta umiejętność może Ci się bardzo przydać. Zacznijmy jednak od innych równie ważnych rzeczy...

Aplikacje konsolowe

Konsola - nie chodzi o konsole do grania. Jest to okienko MS-DOS, w którym mogą być wyświetlane rezultaty wykonania jakichś funkcji.

 

W Delphi można oczywiście tworzyć takie aplikacje. Nie jest to trudne. Pamiętasz jak w drugim rozdziale pisaliśmy bez wykorzystania formularzy? Teraz też tak zrobimy. Zamknij edytor kodu - zamknięty zostanie także formularz. Teraz z menu Project wybierz View Source. Zobaczysz kod pliku DPR. Teraz na samej górze po słowie kluczowym program napisz taką linię:

  {$APPTYPE CONSOLE}

Jest to tzw. dyrektywa. Nie myl tego z komentarzem. Mówi ona kompilatorowi o opcji aplikacji. Jeżeli wpiszesz taką linię to Delphi wyświetli program jako okno DOSa. Nie ma tu za wiele do opanowania. Istnieją dwie główne komendy, które wpisują tekst do okna. Doprowadź program to takiej postacii:

program Project1;

  {$APPTYPE CONSOLE}

uses
  Windows;

begin
  Writeln('To jest tekst, który zostanie wyświetlony w oknie DOS');
end.

Polecenie Writeln ( tak jak w Turbo Pascalu ) powoduje wyświetlenie tekstu, który wpisany jest w nawiasie. Nic nadzwyczajnego. Możesz uruchomić program. Niz nie zauważysz bo program zostanie otwarty, a później zamknięty. W tym celu po komendzie Writeln napisz jeszcze jedną - Readln; Program będzie wówczas czekał aż Ty naciśniesz klawisz Enter. Takie coś:

var
  Tekst : String;
begin
  Readln(Tekst);

Powoduje pobranie napisanego przez użytkownika tekstu, a następnie przypisanie go do zmiennej Tekst. 

Często można się spotkać z programami uruchamianymi wraz z parametrem. Np. trzeba program uruchomić w oknie MS-DOS, a następnie nie dość, że podać nazwę samego programu to jeszcze wpisać parametr, z jakim ma być uruchamiany. Jako przykład podam kompilator PERL. Jeżeli chcemy sprawdzić, czy napisany przez nas skrypt jest poprawny i nie zawiera błędów trzeba napisać:

perl.exe -w skrypt.cgi

Powoduje to uruchomienie programu Perl.exe. Następnie program sprawdza jaki parametr został wpisany - w tym wypadku -w. Kompilator wie teraz że my chcemy, aby on sprawdził, czy skrypt jest poprawny i to wykonuje. Takie rzeczy Ty także możesz zrobić w aplikacji konsolowej. Do tego służą polecenia ParamCount ( sprawdza, czy program jest uruchomiony z parametrem ) i ParamStr ( odczytuje wartość danego parametru ). Spójrz na poniższy przykład: 

{
  Copyright (c) 2001 - Adam Boduch
}

program param;

  {$APPTYPE CONSOLE}  { <-- aplikacja konsolowa }

uses
  Windows;

begin
  if ParamCount <> 0 then  // Jeżeli parametr jest różny od zera - jest parametr
  begin
    if ParamStr(1) = '-imie' then // odczytaj parametr pierwszy
    begin // jeżeli się zgadza...
      Writeln('Na imię mi Delphi'); //... wypisz odpowiedni tekst
      Readln;  // czkaj na reakcję 
    end;
  end;
end.

Na samym początku program sprawdza, czy został uruchomiony z parametrem. Później następuje odczytanie danego parametru. Jeżeli parametr się zgadza to następuje wyświetlenie jakiegoś tekstu. Jeżeli parametr się nie zgadza lub program nie został uruchomiony z parametrem to program nie wykonuje się. 

Teraz przyszedł czas na uruchomienie programu. Otwórz okno MS-DOS. Teraz za pomocą poleceń cd, cd.. przejdź do katalogu, w którym znajduje się nasz program. Wpisz taką linię komend:

Teraz naciskasz Enter i... program się wykona. Wyświetli się tekst. W programie możesz mieć o wiele więcej parametrów. Oto zmodyfikowana wersja poprzedniego projektu:

begin
  if ParamCount <> 0 then  // Jeżeli parametr jest różny od zera - jest parametr
  begin
    if (ParamStr(1) = '-imie') and (ParamStr(2) = '-nazwisko') then // odczytaj parametr pierwszy
    begin // jeżeli się zgadza...
      Writeln('Na imię mi Delphi'); //... wypisz odpowiedni tekst
      Writeln('Na nazwisko Borland');
      Readln;  // czkaj na reakcję 
    end;
  end;
end.

Tym razem program się wykona tylko wtedy gdy podczas dwa parametry - -imie oraz -nazwisko. Wpisz więc w linii komend okna DOS taki tekst:

param.exe -imie -nazwisko

Program zostanie wykonany. 

Wyjątki

Uwierz mi, że wyjątki są bardzo przydatne przy programowaniu. Wyobraź sobie taką sytuację: piszesz jakąś procedurę, w której pobierasz tekst z komponentu Edit. W Edit musisz mieć cyfry. Co jeżeli do wykonania procedury potrzebne Ci liczby z komponentu Edit, a użytkownik wpisze słowa? Wyświetlony zostanie błąd Delphi o nieprawidłowej operacji. Właśnie dzięki wyjątkom możesz takie sytuacje kontrolować - kontrolować sytuację, w której użytkownik popełni błąd. Zareagować możesz różnie - przeważnie wyświetlając odpowiednie okienko z informacją. 

Umieść na formie komponent Edit oraz komponent Button. Teraz wygeneruj procedurę OnClick komponentu Button. 

procedure TForm1.Button1Click(Sender: TObject);
begin
  try
    StrToInt(Edit1.Text);
  except
    ShowMessage('Hej, przecież masz wpisać cyfry, nie?');
  end;
end;

Po słowie kluczowym try następuje wykonywanie poleceń. Teraz program będzie "obserwowany", czyli tutaj następuje sprawdzanie, czy nie ma błędu. Słowo try od angielskiego - spróbuj. W naszym przykładzie program próbuje przekształcić tekst na cyfry. Jeżeli mu się udaje dokonać konwersji - ok. Jeżeli nie zostają wykonane instrukcje znajdujące się po słowie except, a przed słowem end. W naszym wypadku zostaje wyświetlona informacja z tekstem. Ogólnie możesz przyjąć, że jeżeli w bloku try pojawią się błędy to zostaną wykonane operacje w bloku except.

Teraz zmodyfikujemy trochę nasz program View 1.0, który pisaliśmy w poprzednim rozdziale. Była w nim procedura obslugująca otwarcie pliku graficznego. Teraz możesz w komponencie OpenPictureDialog dodać nowy filtr: Wszystkie pliki (*.*). Spowoduje to, że po wybraniu tego filtra w oknie będą wyświetlane wszystkie pliki obojętnie jakiego typu. Załóżmy, że użytkownik wybierze nie ten typ co trzeba? Np. plik z rozszerzeniem *.exe. Co wtedy? Delphi wygeneruje standardową obsługę błędu - czyli komunikat. My możemy to zrobić lepiej - oto zmodyfikowana wersja tej procedury:

procedure TMainForm.FileOpenClick(Sender: TObject);
begin
  if OpenPictureDialog.Execute then // jeżeli okienko zostanie wyświetlone...
  begin
    ChildForm := TChildForm.Create(Self);  //...stwórz okno Child
    { załaduj obrazek }
    try  // spróbuj załadować obrazek
      ChildForm.Image.Picture.LoadFromFile(OpenPictureDialog.FileName);
    except  // w razie błędu...
      on EInvalidGraphic do
        raise Exception.Create('Błąd! Nie mogę załadować obrazka o niewłaściwym rozszerzeniu!');
    end;

    with ChildForm do
    begin
    { dopasuj rozmiary obszaru roboczego do rozmiarów obrazka }
      ClientWidth := Image.Picture.Width;
      ClientHeight := Image.Picture.Height;
    end;
    ChildForm.Caption := OpenPictureDialog.FileName; // do tytułu okna przypisz wybrany plik
    ChildForm.Show; // wyświetl okno
  end;
end;

Tutaj zostanie wyświetlone nasze okno jeżeli załadowany obrazek będzie niewłaściwego rozszerzenia. Możesz zauważyć tutaj niezrozumiałe dla Ciebie słowa jak EInvalidGraphic, czy słowo kluczowe raise. Exception.Create tworzy standardowe okno z informacją. Dodatkowo po prawej stronie okna wyświetlona jest ikona symbolizująca błąd. 

Selektywna obsługa wyjątków

Jeżeli wystąpi jakiś błąd to możesz napisać kod, który będzie wyświetlał okno w zależności od rodzaju błędu. Przykładowo jeżeli format pliku, który próbujesz otworzyć jest niewłaściwy wyświetli się jeden komunikat, a jeżeli nastąpi jakiś inny błąd otwarcia pliku to wyświetli się inny. Stąd w poprzednim kodzie słowo EInvalidGraphic. Selektywną obsługę wyjątków zapisujemy z użyciem słów on oraz do.  Oto przykład obsługi dwóch wyjątków:

    except  // w razie błędu...
      on EInvalidGraphic do
        raise Exception.Create('Błąd! Nie mogę załadować obrazka o niewłaściwym rozszerzeniu!');
      on EInvalidImage do
        raise Exception.Create('Błąd związany z zasobami pliku! Nie mogę go odczytać!');
    end;

Błąd EInvalidImage występuje wtedy gdy uszkodzone są zasoby pliku graficznego i nie można go odczytać. Inne dość popularne błędy to:

Błąd Opis błędu
EPrinter Błąd związany z błędem podczas próby drukowania.
ERegistryException Wyjątek ten wiąże się z błędem spowodowanym podczas edycji rejestru Windows lub plików INI.
EDivByZero Występuje podczas próby dzielenia przez 0.
EOutOfMemory Brak pamięci.

To oczywiście tylko niektóre błędy - jest ich znacznie więcej i więcej przypadków możesz obsłużyć w swoim programie. Pytanie skąd wiedzieć jakie są jeszcze możliwe do obsłużenia wyjątki? Albo z pomocy Delphi ( po wczytaniu pomocy na temat jednego wyjątku są odnośniki na temat innych ) lub w pliku Classes.pas znajdującym się w katalogu Sources. 

Standardowo już jeżeli po słowie kluczowym do oprócz komunikatu o błędzie będzie jeszcze inna instrukcja musisz wszystko wsiąść w słowa begin i end. 

   on EInvalidGraphic do
      begin
        raise Exception.Create('Błąd! Nie mogę załadować obrazka o niewłaściwym rozszerzeniu!');
        (ActiveMDIChild as TChildForm).Close; // zamknięcie okna
      end;

 

Słowo kluczowe raise

Jest to bardzo ważne słowo. Powoduje ono wyświetlenie komunikatu z błędem. Zauważ, że zawsze je stosowałem podczas wyświetlania komunikatu ( Exception.Create ). Jeżeli napiszesz samo słowo raise; zakończone średnikiem to program wyświetli standardowy komunikat o błędzie Windowsa. 

Polecenia tego możesz urywać nie tylko po słowie except. Także w dowolnym miejscu programu gdzie chcesz wyświetlić okno z informacją o błędzie. 

if CosTamCosTam = TRUE then
  raise Excception.Create('Aaaaaa! Błąd');

Try, except oraz.... finally

Zamiast except funkcjonuje także słowo finally. Po tym słowie będą wykonywane instrukcje bez względu, czy w programie wystąpi błąd, czy też nie. Instrukcje te będą występowały ZAWSZE. 

Klasa := TKlasa.Create;
try
  { jakieś instrukcje klasy }
finally
  Klasa.Free; // zwolnienie klasy
end;

W takim wypadku słowo finally stosuje się najczęściej. Teraz klasa będzie zwalniana za każdym razem ( za każdym razem będzie zwalniana pamięć ) bez względu na to, czy zaistnieją jakieś błędy, czy też nie. 

Istnieje możliwość połączenia obydwu słów - except oraz finally:

Klasa := TKlasa.Create;
try
  try
     { jakieś instrukcje }
  except
    { wyświetlenie błędu }
  end;
finally
  Klasa.Free; // zwolnienie klasy
end;

Tworzenie własnych wyjątków 

Jest to dziecinnie łatwe. Deklarujesz po prostu klasę, która dziedziczy z klasy Exception. 

type
 { nowe nasze wyjątki }
  ELowException = class(Exception);
  EMiddleException = class(Exception);
  EHighException = class(Exception);
  

Przyzwyczajaj się, że kolejną regułą jest to, że każdy wyjątek winien rozpoczynać się od litery E. Teraz możesz np, generować takie wyjątek, czy go obsługiwać:

raise ELowException.Create('Treść komunikatu');

Obsługa wyjątków

Możesz napisać swoją procedurę, która obsługiwać będzie wyjątki. W praktyce odbywa się to tak, że wyjątek "przepływa" przez naszą procedurę. Możesz z nim zrobić wszystko. Zaraz napiszemy przykładową aplikacje podsumowującą wiedzę o wyjątkach. 

Umieść na formularzu komponent TPopupMenu. ( paleta Standard ). Jest to komponent  - menu rozwijalne. Tworzenie nowych pozycji odbywa się tak samo jak w wypadku komponentu TMainMenu. Ja stworzyłem trzy pozycje, które symbolizują wyjątki:

Teraz na formie umieść komponent Button. Po naciśnięciu przycisku wyświetlone ( rozwinięte ) zostanie menu. Nie jest to skomplikowane. Oto kod procedurze zdarzenia OnClick komponentu Button:

procedure TMainForm.btnErrorClick(Sender: TObject);
var
  GetCursor : TPoint;  // nowy typ danych
begin
  GetCursorPos(GetCursor);  // pobierz aktualną pozycję kursora
  pmPopupMenu.Popup(GetCursor.X, GetCursor.Y); // wyświetl menu
		      end;

Wykorzystałem tutaj nowy typ zmiennej - TPoint. W rzeczywistości jest to rekord, który zawiera "w sobie" dwie zmienne - X i Y. Polecenie GetCursorPos to procedura, która pobiera pozycje kursora i przypisuje ją do zmiennej GetCursor. Tak więc GetCursor.X to pozycja X kursora, a GetCursor.Y to pozycja Y kursora. To wszystko. pmPopupMenu to nazwa komponentu TPopupMenu. Zawiera on metodę Popup, która powoduje rozwinięcie menu. Trzeba podać dwa parametry - pozycję X oraz Y w którym menu ma się rozwinąć. Podajemy w tym miejscu pobrane współrzędne. Tą procedurę już mamy. 

Teraz poszczególnym pozycją w PopupMenu trzeba nadać właściwość Tag od 1 do 3. Właściwość Tag służy do niczego. Jest to dodatkowa metoda, którą programista może wykorzystać jak chce. My napiszemy jedną procedurę dla wszystkich pozycji i w zależności od tego jaka będzie właściwość Tag wykonywana będzie określone zadanie. Oto jak wygląda teraz nasza klasa i wyjątki:

type
 { nowe nasze wyjatki }
  ELowException = class(Exception);
  EMiddleException = class(Exception);
  EHighException = class(Exception);
  
  TMainForm = class(TForm)
    btnError: TButton;
    pmPopupMenu: TPopupMenu;
    pmiELowError: TMenuItem;
    pmiEMiddleError: TMenuItem;
    pmiEHighError: TMenuItem;
    lblMessage: TLabel;
    procedure btnErrorClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
  { procedura obsługi wyjątków }
    procedure ProccException(Sender: TObject; E : Exception);
  published
  { procedura obsługująca kliknięcia w poszczególne pozycje }
    procedure PopupClick(Sender: TObject);
  end;

Na samej górze oczywiście wyjątki. W sekcji private procedura, która obsługiwać będzie przepływ wyjątków. W sekcji published procedura, która obsługiwać będzie kliknięcia w pozycję w komponencie PopupMenu. 

Oto procedura przepływu wyjątków:

procedure TMainForm.ProccException(Sender: TObject; E: Exception);
begin
{
   Procedura obsługi wyjątku. Nie wyświetlaj błędu w okienku, ale wiadomość
   o błędzie wyświetl na komponencie typu TLabel. Jeżeli błąd jest typu EHighException
   to wyświetl dodatkowo komunikat.
}
  lblMessage.Caption := E.Message;
  if E.ClassType = EHighException then
    MessageBox(Handle, 'Jejejej! Coś się źle dzieje!', 'Uwaga! Błąd', MB_OK + MB_ICONERROR);
end;

Zamiast wyświetlać informację o wyjątkach treść będzie wpisywana na komponencie TLabel ( lblMessage ). Jak już zapewne się domyśliłeś E.Message oznacza treść komunikatu. Następnie następuje sprawdzenie, czy typ wyjątku to EHighException. Jeżeli tak to następuje wyświetlenie stosowanego komunikatu. W okienku, które się wyświetli oprócz standardowego przycisku będzie ikonka błędu. Tak, nie mówiłem o tym wcześniej, ale można takie bajer umieścić. Zamiast MB_ICONERROR może być także:

    MB_ICONWARNING                                                    ostrzeżenie
    MB_ICONINFORMATION                                            informacja 

Oto cały kod programu: 

{
  Copyright (c) 2001 - Adam Boduch
}

unit MainFrm;

interface

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

type
 { nowe nasze wyjatki }
  ELowException = class(Exception);
  EMiddleException = class(Exception);
  EHighException = class(Exception);
  
  TMainForm = class(TForm)
    btnError: TButton;
    pmPopupMenu: TPopupMenu;
    pmiELowError: TMenuItem;
    pmiEMiddleError: TMenuItem;
    pmiEHighError: TMenuItem;
    lblMessage: TLabel;
    procedure btnErrorClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
  { procedura obsługi wyjątków }
    procedure ProccException(Sender: TObject; E : Exception);
  published
  { procedura obsługująca kliknięcia w poszczególne pozycje }
    procedure PopupClick(Sender: TObject);
  end;

var
  MainForm: TMainForm;

implementation

{$R *.DFM}

procedure TMainForm.btnErrorClick(Sender: TObject);
var
  GetCursor : TPoint;  // nowy typ danych
begin
  GetCursorPos(GetCursor);  // pobierz aktualną pozycję kursora
  pmPopupMenu.Popup(GetCursor.X, GetCursor.Y); // wyświetl menu
end;

procedure TMainForm.PopupClick(Sender: TObject);
begin
{
   odczytaj wartość Tag pozycji menu, która została naciśnięta.
   W zależności o wartości Tag wygeneruj odpowiedni komunikat błędu.
}
  case (Sender as TMenuItem).Tag of
    1: raise ELowException.Create('Nastąpił niegroźny błąd');
    2: raise EMiddleException.Create('Taki sobie błąd. Trochę groźny. Zawiadom szefa');
    3: raise EHighException.Create('Wołaj tego szefa do cholery!!!');
  end;
end;

procedure TMainForm.ProccException(Sender: TObject; E: Exception);
begin
{
   Procedura obsługi wyjątku. Nie wyświetlaj błędu w okienku, ale wiadomość
   o błędzie wyświetl na komponencie typu TLabel. Jeżeli błąd jest typu EHighException
   to wyświetl dodatkowo komunikat.
}
  lblMessage.Caption := E.Message;
  if E.ClassType = EHighException then
    MessageBox(Handle, 'Jejejej! Coś się źle dzieje!', 'Uwaga! Błąd', MB_OK + MB_ICONERROR);
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
{ przypisz procedurę obsługi wyjątku }
  Application.OnException := ProccException;
end;

end.

Nie omówiłem wcześniej dwóch procedur. W OnCreate następuje przypisanie zdarzenia OnException do procedury, którą wcześniej napisaliśmy. W procedurze PopupClick natomiast następuje "wyciągnięcie" właściwości Tag z pozycji komponentu PopupMenu. Poszczególne pozycji w tym komponencie są typu TMenuItem. Tak więc w zależności od właściwości Tag wykonywany zostaje określony wyjątek. 

Pliki tekstowe

Wczytywanie plików

Tak jak powiedziałem na początku tego rozdziału nie jest to nic nadzwyczajnego. Najpierw należy napisać zmienną, która będzie wskazywała na plik. Robi się to tak:

var
  TF  : TextFile;

Teraz musisz skojarzyć zmienną z plikiem:

AssignFile(TF, 'C:\Autoexec.bat');

W tym momencie możesz stworzyć plik na dysku ( ReWrite(TF); ), albo otworzyć już istniejący ( Reset(TF); ). 

Napiszmy prosty program. Umieść na formie komponent Memo - służy on do przechowywania tekstów. Nazwij go Memo. Jego właściwość Align zmień na alClient. We właściwości Font możesz zmienić czcionkę używaną przez komponent. We właściwości Lines możesz edytować linie tekstu, które pojawią się po uruchomieniu programu w tym komponencie. Możesz pozostawić to bez zmian. Zmień tylko właściwość ScrollBars - zmień na ssVertical. Właściwość ta określa jakie i czy paski przewijania będą wyświetlane. 

Dobra, teraz wygeneruj procedurę OnCreate formy:

procedure TMainForm.FormCreate(Sender: TObject);
var
  TF: TextFile; // zmienna wskazująca na plik tekstowy
  S : String; // łańcuch przechowujący kolejne linie tekstu
begin
  Memo.Clear;
  AssignFile(TF, 'C:\Autoexec.bat');
  Reset(TF); // otwarcie pliku
  while not Eof(TF) do
  begin
    Readln(TF, S); // odczytaj kolejne linie tekstu
    Memo.Lines.Add(S);
  end;
  CloseFile(TF); // zamknięcie pliku
end;

Na samym początku należy wyczyścić zawartość Memo. Następnie następuje otwarcie pliku. Funkcja Eof określa koniec pliku. Czyli w tej procedurze wykonywana jest pętla dopóki program nie odnotuje zakończenia pliku. W pętli następuje odczytanie kolejnych lini pliku i przypisanie ich do zmiennej S. Następnie zmienna S dodawana jest do komponentu Memo. W rezultacie po uruchomieniu programu w komponencie zobaczysz zawartość pliku Autoexec.bat. Na samym końcu plik zostaje zamknięty poleceniem ( CloseFile ).

Inna sprawa, że cały ten kod dałoby się zastąpić jedną linią ( komendą ) VCL:

Memo.Lines.LoadFromFile('C:\Autoexec.bat');

Warto jednak umieć dokonywać operacji na plikach bez pomocy VCL. 

Tworzenie plików

Tworzenie także nie jest trudne. Realizuje się to poleceniem ReWrite. Zapisz natomiast przy pomocy polecenia Writeln. 

var
  TF: TextFile; // zmienna wskazująca na plik tekstowy
begin
  AssignFile(TF, 'C:\plik.txt');
  try
    ReWrite(TF); // utwórz plik
    Writeln(TF, 'Cześć!'); // dopisz linię tekstu
    Writeln(TF, 'Ten plik został właśnie stworzony. Co Ty na to?'); // dopisz kolejną
  finally
    CloseFile(TF); // zamknij plik
  end;
end;

Jak widzisz wszystko wziąłem w linie try, finally. Tak samo to zadanie można by było zrealizować za pomocą polecenia VCL: Memo.Lines.SaveToFile('C:\plik.txt'); ale w takim wypadku zapisana by była zawartość Memo.

Dopisywanie do plików

Dopisywanie na końcu realizuje się komendą Append. Ustawia ona kursor na końcu pliku - wtedy można dokonywać zapisu treści. 

var
  TF : TextFile;
begin
  AssignFile(TF, 'C:\plik.txt');
  try
    Append(TF);
    Writeln(TF, ''); // jedna linia przerwy
    Writeln(TF, 'Oto kolejna linia');
  finally
    CloseFile(TF);
  end;
end;

Widzisz, że zamiast polecenia Reset zastosowałem Append. Jest to konieczne - program ustawia kursor na samym końcu pliku i wtedy dopiero jest możliwe dopisywanie. W kolejnej linii stworzyłem w pliku tekstowym pustą pozycję, aby oddzielić tekst oryginalny od tego co dopisujemy w tejże procedurze. To właściwie najważniejsze informacje dotyczące plików tekstowych - taka wiedza będzie Ci wystarczać. 

Pliki amorficzne

Ten rodzaj plików służy do operacji na bajtach. Umożliwia on odczytywanie, wczytywanie bajtów pliku, ustawianie na odpowiednim miejscu gdzie rozpoczynać się będzie odczyt. Komendy są bardzo podobne jak w przypadku zwykłych plików tekstowych. Na samym początku musisz zadeklarować zmienną typu File:

var
  F : File;

Tak jak powiedziałem wcześniej pliki amorficzne mogą posłużyć do operowania większymi porcjami bajtów lub pojedynczymi. Także, przy otwieraniu takiego pliku należy podać porcję bajtów jaka będzie odczytywana:

Reset(F, 1);

Jak widzisz komenda otwierająca jest taka sama jak w przypadku plików tekstowych - podajesz tylko o jeden parametr więcej. Jeżeli pominiesz drugi parametr to Delphi nie potraktuje tego jako błąd, ale przyjmie w takim wypadku wartość domyślną, czyli 128. Pliki amorficzne w przeciwieństwie do tekstowych oferują parę dodatkowych, bardzo przydatnych funkcji. Możesz bowiem pobrać rozmiar pliku w bajtach ( polecenie FileSize ), ustawić pozycję pliku na odpowiedniej pozycji ( Seek ) oraz odczytać pozycje pliku ( FilePos ). Przykładowo chcąc pobrać rozmiar pliku w bajtach piszesz:

var
  F : File;
  FSize : Integer; // przechowuje rozmiar pliku
begin
  AssignFile(F, 'C:\plik.exe');
  Reset(F, 1);
  FSize := FileSize(F); // pobranie rozmiaru 
  ShowMessage('Rozmiar pliku wynosi: ' + IntToStr(FSize) + ' bajtów');
  CloseFile(F);
end;

Pliki amorficzne nie posiadają za to polecenia Append ustawiającego pozycje do zapisu na końcu pliku. Można to łatwo ominąć dzięki funkcji Seek:

Seek(F, FileSize(F));

I tym sposobem ustawiasz pozycje do zapisu na samym końcu. 

Odczytanie Tagu z pliku mp3

Osoby lubiące słuchać muzykę w mp3 zapewne wiedzą co to jest tag. Jeżeli nie to wyjaśniam. Jest to specjalna informacja zapisana w pliku mp3, w której znajdują się informacje o wykonawcy utworu, albumie,  roku wydania itp. Właśnie dzięki plikom amorficznym możemy tę informację "wyłuskać" z pliku mp3. Potrzebna nam będzie wiedza na temat budowy samego formatu mp3. Wiedzę tę możesz nabyć chociażby na stronie www.4programmers.net gdzie znajdziesz informacje na ten temat budowy pliku mp3. Cały tag zajmuje 128 bajtów i znajduje się na samym końcu pliku mp3. Autorzy formatu mp3 udostępnili informacje ile bajtów jest przeznaczonych na wykonawcę, tytuł itp. 

Na samym początku będziesz musiał umieścić w sekcji Implementation taki rekord:

{
 Oto rekord, który zawiera elementy, które będziemy odczytywać z pliku mp3.
 Każda zmienna rekordu ma przeznaczoną prawidłową ilość znaków na dany element.
}

type
  TTag = packed record
    ID: String[3]; // czy Tag istnieje?
    Title : String[30]; // tytuł
    Artist : String[30]; // wykonawca
    Album : String[30]; // album
    Year : String[4]; // rok wydania
    Comment : String[30]; // komentarz
    Genre : Byte; // typ - np. POP, Techno, Jazz itp.
  end;

Do elementów tego rekordu będą przydzielane informacje. Zauważ, że każdy element ma określoną max. ilość znaków ( bajtów - każdy znak to 1 bajt ) jaką może mieć. Pierwsza pozycja może mieć max. 3 znaki. Informuje ona, czy Tag w pliku mp3 istnieje, czy też nie. Jeżeli istnieje i jest prawidłowo zapisany to ta zmienna powinna mieć wartość "TAG". Na kolejne 3 elementy przydzielone jest 30 znaków. Na rok oczywiście 4 znaki; komentarz to także 30 znaków. I ostatnia pozycja to jeden bajt ( w formie cyfry ), który określa typ utworu. Np. jeżeli element ma wartość 0 to utwór jest Blues'owy. Żeby tę informacje móc odczytać w programie musisz ( także w sekcji Implementation ) zadeklarować tablicę:

const
{  oto tablica zawierająca typy utworów }
  Genre : array[0..79] of ShortString = (
  ('Blues'), ('Classic Rock'), ('Country'), ('Dance'), ('Disco'),
  ('Funk'), ('Grunge'), ('Hip-Hop'), ('Jazz'), ('Metal'), ('New Age'),
  ('Oldies'), ('Other'), ('Pop'), ('R&B'), ('Rap'), ('Reggae'),
  ('Rock'), ('Techno'), ('Industrial'), ('Alternative'), ('Ska'),
  ('Death Metal'), ('Pranks'), ('Soundtrack'), ('Euro-Techno'), ('Ambient'),
  ('Trip-Hop'), ('Vocal'), ('Jazz+Funk'), ('Fusion'), ('Trance'),
  ('Classical'), ('Instrumental'), ('Acid' ), ('House'), ('Game'),
  ('Sound Clip'), ('Gospel'), ('Noise'), ('AlternRock'), ('Bass'),
  ('Soul'), ('Punk'), ('Space'), ('Meditative'), ('Instrumental Pop'),
  ('Instrumental Rock'), ('Ethnic'), ('Gothic'), ('Darkwave'),
  ('Techno-Industrial'), ('Electronic'), ('Pop-Folk'), ('Eurodance'),
  ('Dream'), ('Southern Rock'), ('Comedy'), ('Cult'), ('Gangsta'),
  ('Top 40'), ('Christian Rap'), ('Pop/Funk'), ('Jungle'), ('Native American'),
  ('Cabaret'), ('New Wave'), ('Psychadelic'), ('Rave'), ('Showtunes'),
  ('Trailer'), ('Lo-Fi'), ('Tribal'), ('Acid Punk'), ('Acid Jazz'),
  ('Polka'), ('Retro'), ('Musical'), ('Rock & Roll'), ('Hard Rock')
  );

Na formie umieść 6 komponentów typu Edit, jeden przycisk oraz komponent OpenDialog. Potrzebna będzie jedna procedura:

procedure TMainForm.btnOpenClick(Sender: TObject);
var
  mpFile : File;
  Buffer : array[1..128] of char; // tutaj przechowywane będą wszystkie dane
  Tag : TTag;  // zmienna wskazująca na rekord
begin
  if OpenDialog.Execute then
  begin
    AssignFile(mpFile, OpenDialog.FileName);
    Reset(mpFile, 1);  // otwórz plik
    Seek(mpFile, FileSize(mpFile) -128); // ustaw na ostatnich 128 bajtach
    BlockRead(mpFile, Buffer, SizeOf(Buffer)); // odczytaj bajty i przypisz do zmiennej Buffer
    CloseFile(mpFile);  // można już zamknąć plik
    with Tag do
    begin
 { tutaj następuje rozdzielenie tekstu i przypisanie odpowiednim elementom rekordu }
      ID := Copy(Buffer, 1, 3);
      Title := Copy(Buffer, 4, 30);
      Artist := Copy(Buffer, 34, 30);
      Album := Copy(Buffer, 64, 30);
      Year := Copy(Buffer, 94, 4);
      Comment := Copy(Buffer, 98, 30);
      Genre := Ord(Buffer[128]);
    end;
    if TAG.ID = 'TAG' then // czy wougle tak został odczytany?
    begin
      edtTitle.Text := Tag.Title;
      edtArtist.Text := Tag.Artist;
      edtAlbum.Text := Tag.Album;
      edtYear.Text := Tag.Year;
      edtComment.Text := Tag.Comment;
      edtGenre.Text := Genre[Tag.Genre];
    end else Application.MessageBox('Tag w tym pliku mp3 NIE ISTNIEJE!', 'Nie istnieje...', MB_OK + MB_ICONINFORMATION);
  end;
end;

Po naciśnięciu przycisku otwierane będzie okno, w który można będzie wybrać dowolny plik mp3. Kluczowym elementem w tej procedurze stanowi polecenie Seek:

Seek(mpFile, FileSize(mpFile) -128);

Następuje tutaj przesunięcie na sam koniec pliku, a następnie odjęcie od tego 128 bajtów co w rezultacie daje przesunięcie na ostatnie 128 bajtów. Kolejne polecenie powoduje odczytanie wszystkich 128 bajtów i przypisanie do zmiennej Buffer.

BlockRead(mpFile, Buffer, SizeOf(Buffer));

Zmienna Buffer to tablica składająca się ze 128 elementów typu Char ( Char może zawierać tylko jeden bajt ( znak )). Kolejne polecenie mają za zadanie porozdzielać tablice Buffer na poszczególne elementy rekordu - dokonuje to polecenie Copy. Pierwszy parametr to oczywiście zmienna, której dotyczyć będzie operacja. Kolejne to miejsce od którego dokonywane będzie "wycinanie" liter. Ostatnie to ilość bajtów do wycięcia. Rezultat wykonania tej operacji przypisywany jest do zmiennej. Tak po kolei aż dojdziemy to elementu Genre. Zastosowałem tu funkcję Ord, która zamienia literę na cyfrę. Polecenie Ord jest przydatne do określania znaków ASCII. Jeżeli chcesz się dowiedzieć jaki jest numer ASCII danego znaku to piszesz Ord('A'). Dobrze, zboczyłem trochę z tematu. Odczytujemy typ utworu, ale trzeba go przedstawić w formie słownej. Korzystamy tutaj z tablicy, która wcześniej napisaliśmy. Jeżeli już wszystkie elementy są przypisane do rekordu trzeba jeszcze wyświetlić wszystko w komponentach. Nim to nastąpi trzeba sprawdzić, czy Tag dało się odczytać - jeżeli tak to w zmiennej ID będzie wartość 'TAG'. 

Oczywiście źródła tego programu są dołączone do książki. Oto cały program w działaniu:

 

Jeszcze trochę o plikach amorficznych...

Przy okazji budowania aplikacji odczytującej tag plików mp3 zastosowałem polecenie BytesRead. Powodowało to odczytanie bajtów. Jeżeli można odczytać to można także zapisać i czyni to procedura BytesWrite. Zaraz napiszemy procedurę odpowiedzialną za kopiowanie plików z jednego do drugiego. W rzeczywistości będziemy kopiowali bajty pliku - z jednego do drugiego porcjami 500 bajtowymi. Najpierw przypatrz się tej procedurze, a później ją omówię:

procedure TForm1.Button1Click(Sender: TObject);
var
  Src, Dst: File;
  Buff : array[0..500] of char; // kopiowanie bezie się odbywało porcjami 500 bajtowymi
  Read : Integer;
begin
  AssignFile(Src, 'C:\101.jpg');
  AssignFile(Dst, 'C:\202.jpg');
  try
    Reset(Src, 1);  // otwórz plik
    try
      Rewrite(Dst, 1); // stwórz plik, który będzie skopiowany
      repeat
        BlockRead(Src, Buff, SizeOf(Buff), Read); // odczytaj pierwszą porcję bajtów
        if Read > 0 then // jeżeli odczytano te bajty i przypisano zmiennej Buff...
          BlockWrite(Dst, Buff, Read); //...zapisz je do pliku - przeznaczenia
      until Read = 0;  // dopóki program nie odczyta 0 bajtów
    finally
      CloseFile(Dst);
    end;
  finally
    CloseFile(Src);
  end;
end;

Trzeba było zastosować tutaj pętle, która będzie wykonywana w zależności od rozmiaru pliku - im plik większy tym pętla wykonywana będzie dłużej. Jeżeli plik, który kopiujemy byłby mały ( do 500 bajtów ) to nie trzeba by było stosować nawet pętli - wszystko dałoby się przypisać do tablicy i zapisać do kolejnego pliku. Ale w naszym wypadku tablica ma tylko 500 bajtów, a jeżeli plik ma 1 kB to nie można pliku skopiować - trzeba zastosować pętle. Oczywiście można by było zastosować tablice o wielkości 1024 bajtów ( 1 kB ) i znowu nie stosować pętli no, ale ile tak można? Co jeśli plik będzie zajmował 1 MB? Będziemy wtedy tworzyć tak dłużą tablice? Nie, trzeba to zrobić z wykorzystaniem pętli. 

Polecenie BlockRead ma w naszym wypadku 4 parametry - ostatni jest opcjonalny. To do zmiennej Read przypisana zostanie ilość bajtów, która będzie odczytana. Następnie należy sprawdzić, czy w zmiennej tej znajduje się wartość większa od 0. Jeżeli nie to znaczy, że cały plik został już skopiowany. No i na końcu polecenie BlockWrite, które zapisuje do pliku zawartość zmiennej buff.

Pliki typowane

Pliki typowane są tym co najbardziej lubię. Służą one bowiem do zapisywania całych rekordów do pliku. Przypatrz się temu rekordowi:

{ w tym rekordzie przechowywane będą dane  }
  TDatBase = packed record
    Name  : String[30]; // imię
    Mail  : String[30]; // e-mail
    Number: Int64; // telefon
  end;

Dla każdej pozycji określona jest max. ilość znaków. Z wyjątkiem pozycji ostatniej, która przechowywać będzie numer telefonu ( liczbę ). Cały ten rekord można zapisać do pliku, a następnie odczytać jego poszczególne pozycje. Oto jak wygląda plik, w którym zapisane są dwa takie rekordy:

Jan Kowalski           h Ló2 
kow@serwer.pl r5˝Ľ
          Z›‡*    Krzysztof Nowak        h Ló2 nowakk@polska.pl5˝Ľ
          ŕ5     

Obsługa takich plików także nie powinna sprawić Ci problemów. W przypadku plików typowanych, amorficznych oraz tekstowych komendy są prawie takie same. W tym rozdziale napiszemy aplikację, która będzie bazą danych. Doprowadź formularz do takiej postaci:

Oto jak powinna wyglądać sekcja Interface:

{
  Copyright (c) Adam Boduch 2001
}

unit MainFrm;

interface

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

type
  TMainForm = class(TForm)
    GroupBox1: TGroupBox;
    lblName: TLabel;
    edtName: TEdit;
    lblMail: TLabel;
    edtMail: TEdit;
    lblTel: TLabel;
    edtTel: TEdit;
    lblRecords: TLabel;
    cmCounts: TComboBox;
    btnAdd: TButton;
    procedure btnAddClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure cmCountsChange(Sender: TObject);
  private
    procedure ShowRecords;
    procedure LoadRecord(Num : Longint);
  end;

{ w tym rekordzie przechowywane będą dane  }
  TDatBase = packed record
    Name  : String[30]; // imię
    Mail  : String[30]; // e-mail
    Number: Int64; // telefon
  end;
const DatName = 'data.dbt';  // w tym pliku przechowywane będą dane

var
  MainForm: TMainForm;

implementation

{$R *.DFM}

Na początek napiszemy procedurę, która wykonywana będzie po naciśnięciu przycisku. Będzie najłatwiejsza i powodować będzie oczywiście dodawanie nowych rekordów do pliku. 

procedure TMainForm.btnAddClick(Sender: TObject);
var
  DatBase : file of TDatBase;  // plik typowany
  DB : TDatBase;
  FSize : Integer;
begin
  AssignFile(DatBase, DatName);
  if not FileExists(DatName) then // jeżeli plik nie istnieje...
    ReWrite(DatBase){ stworz go } else ReSet(DatBase); // w przeciwnym wypadku - otwórz

  FSize := FileSize(DatBase); // odczytaj rozmiar pliku
  if FSize > 0 then  // jeżeli cos już w nim jest zapisane...
    Seek(DatBase, FSize); // przesuń na sam koniec

 { przypisz wszystkie dane z komponentów do rekordu  }
  DB.Name := edtName.Text;
  DB.Mail := edtMail.Text;
  DB.Number := StrToInt(edtTel.Text);
  Write(DatBase, DB); // zapisz rekord do pliku
  CloseFile(DatBase); // zamknij
  ShowRecords; // odśwież listę rekordów
end;

Zwróć uwagę na deklaracje zmiennych. W przypadku plików typowanych należy stosować sekwencję file of. Deklarujemy nową zmienną DatBase, która będzie zmienną plikową, ale jeżeli ma to być zmienna typowana to musi wskazywać na rekord. Pierwsze trzy linijki sprawdzają, czy plik data.dbt istnieje ( przypominam, że data.dbt podstawiona jest pod stałą DatName ) - jeżeli tak to ją otwiera - jeżeli nie tworzy nowy plik. Każdy rekord dopisywany będzie na samym końcu pliku. Następnie do rekordu TDatBase przypisane zostają dane z komponentów. Później kluczowe słowo - Write, które dopisuje rekord do pliku. Na końcu następuje wywołanie procedury ShowRecords, która odczytuje ile jest w pliku rekordów. Oto ta procedura: 

procedure TMainForm.ShowRecords;
var
  DatBase : file of TDatBase;
  I : Integer;
begin
  AssignFile(DatBase, DatName);
  if not FileExists(DatName) then Exit else
    Reset(DatBase);

  cmCounts.Clear; // czyść listę rekordów, które są w pliku

  {  odczytaj ilość rekordów i wczytaj do listy }
  for I := 0 to FileSize(DatBase) -1 do
    cmCounts.Items.Add('Rekord nr ' + IntToStr(i+1));

  lblRecords.Caption := 'Ilość rekordów: ' + IntToStr(i); // na komponencie wyświetl ilość

  CloseFile(DatBase);
end;

Do komponentu TComboBox zostają dopisane linie, które symbolizują kolejne rekordy. Gdy użytkownik kliknie na którąś z linii to cały rekord zostaje załadowany do komponentów. Cóż, na dole w komponencie Label wyświetlony jest także napis, który mówi o ilości rekordów w pliku. 

W końcu ostatnia procedura, która ładuje cały rekord ( jego pozycje ) do komponentów. 

procedure TMainForm.LoadRecord(Num: Integer);
var
  DatBase : file of TDatBase;
  DB : TDatBase;
begin
  AssignFile(DatBase, DatName);
  Reset(DatBase);

  Seek(DatBase, Num); // przesuń na rekord, który chcesz odczytać
  Read(DatBase, DB);  // odczytaj do rekordu
  { przypisz dane z rekordu do komponentów }
  with DB do
  begin
    edtName.Text := Name;
    edtMail.Text := Mail;
    edtTel.Text := IntToStr(Number);
  end;

  CloseFile(DatBase);
end;

Procedura posiada jeden parametr, który symbolizuje rekord, który zostanie załadowany z pliku do poszczególnych kontrolek typu TEdit. Po prostu pozycja pliku zostaje ustalona na rekordzie, który chcemy odczytać. Następnie za pomocą polecenia Read przypisujemy rekord z pliku do rekordu TDatBase. Końcówka jest już prosta bo przypisanie danych z rekordu do komponentów. 

Oto cały kod programu:

{
  Copyright (c) Adam Boduch 2001
}

unit MainFrm;

interface

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

type
  TMainForm = class(TForm)
    GroupBox1: TGroupBox;
    lblName: TLabel;
    edtName: TEdit;
    lblMail: TLabel;
    edtMail: TEdit;
    lblTel: TLabel;
    edtTel: TEdit;
    lblRecords: TLabel;
    cmCounts: TComboBox;
    btnAdd: TButton;
    procedure btnAddClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure cmCountsChange(Sender: TObject);
  private
    procedure ShowRecords;
    procedure LoadRecord(Num : Longint);
  end;

{ w tym rekordzie przechowywane będą dane  }
  TDatBase = packed record
    Name  : String[30]; // imię
    Mail  : String[30]; // e-mail
    Number: Int64; // telefon
  end;

const DatName = 'data.dbt';  // w tym pliku przechowywane będą dane

var
  MainForm: TMainForm;

implementation

{$R *.DFM}

procedure TMainForm.btnAddClick(Sender: TObject);
var
  DatBase : file of TDatBase;  // plik typowany
  DB : TDatBase;
  FSize : Integer;
begin
  AssignFile(DatBase, DatName);
  if not FileExists(DatName) then // jeżeli plik nie istnieje...
    ReWrite(DatBase){ stwórz go } else ReSet(DatBase); // w przeciwnym wypadku - otwórz

  FSize := FileSize(DatBase); // odczytaj rozmiar pliku
  if FSize > 0 then  // jeżeli cos już w nim jest zapisane...
    Seek(DatBase, FSize); // przesuń na sam koniec

 { przypisz wszystkie dane z komponentów do rekordu  }
  DB.Name := edtName.Text;
  DB.Mail := edtMail.Text;
  DB.Number := StrToInt(edtTel.Text);
  Write(DatBase, DB); // zapisz rekord do pliku
  CloseFile(DatBase); // zamknij
  ShowRecords; // odśwież listę rekordów
end;

procedure TMainForm.LoadRecord(Num: Integer);
var
  DatBase : file of TDatBase;
  DB : TDatBase;
begin
  AssignFile(DatBase, DatName);
  Reset(DatBase);

  Seek(DatBase, Num); // przesuń na rekord, który chcesz odczytać
  Read(DatBase, DB);  // odczytaj do rekordu
  { przypisz dane z rekordu do komponentów }
  with DB do
  begin
    edtName.Text := Name;
    edtMail.Text := Mail;
    edtTel.Text := IntToStr(Number);
  end;

  CloseFile(DatBase);
end;

procedure TMainForm.ShowRecords;
var
  DatBase : file of TDatBase;
  I : Integer;
begin
  AssignFile(DatBase, DatName);
  if not FileExists(DatName) then Exit else
    Reset(DatBase);

  cmCounts.Clear; // czyść listę rekordów, które są w pliku

  {  odczytaj ilość rekordów i wczytaj do listy }
  for I := 0 to FileSize(DatBase) -1 do
  { rekordy liczone są od 0 - my wyświetlimy cyfrę 1 zamiast 0, itd.}
    cmCounts.Items.Add('Rekord nr ' + IntToStr(i+1));

  lblRecords.Caption := 'Ilość rekordów: ' + IntToStr(i); // na komponencie wyświetl ilość

  CloseFile(DatBase);
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  ShowRecords; // wyświetl rekordy zapisane w pliku
end;

procedure TMainForm.cmCountsChange(Sender: TObject);
begin
// załaduj rekord, który został wybrany z listy
// właściwość ItemIndex komponentu określa numer pozycji, która została kwiknięta
  LoadRecord(cmCounts.ItemIndex);
end;

end.

Podsumowanie

Ten rozdział miał być zdominowany przez pliki. Nie udało się do końca. Na samym początku słowo o aplikacjach konsolowych, później o wyjątkach. Mniej więcej w połowie dokumentu zająłem się omawianiem plików i przerobiliśmy pliki tekstowe, amorficzne oraz typowane. Z plikami możesz pracować już spokojnie. Mam nadzieje, że przy okazji tego rozdziału nauczyłeś się czegoś i ten rozdział Cię zainteresował bo często się to przydaje, a trudne nie jest...