Spis treści:
Selektywna obsługa wyjątków
Słowo kluczowe raise
Try, except oraz finally
Tworzenie własnych wyjątków
Obsługa wyjątkówOdczytywanie TAG'ów z plików mp3
Jeszcze trochę o plikach amorficznych
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...