Oki doki, dzisiaj postaram się pokazać jak w prosty sposób zrobić plik podobny do archiwum np. zipa czy rara. Wprawdzie w naszym formacie nie będziemy stosowali kompresji (aby niepotrzebnie na początek nie komplikować) ale pokaże jedno z możliwych podejść do tego zagadnienia. Na początek może napisze po krótce do czego może się przydać taki plik, otóż można w nim trzymać np. spakowane levele do gry, save gamy, mapy oraz inne zasoby do programów. Ok, koniec marudzenia, co będziemy potrzebowali do szczęścia ?
1. Podstawy programowania obiektowego.
2. Podstawy wiadomości o strumieniach.
3. kilka minut czasu aby przeczytać i zrozumieć.
Na początek, trzeba się zastanowić jaka będzie wewnętrzna struktura naszego archiwum ? Ja wybrałem bardzo prosty układ, pliki w archiwum są przechowywane kolejno w następującej postaci:
Rozmiar nazwy[int], nazwa[tablica bajtów], rozmiar danych[int], dane plik[tablica bajtów]
Jakie są główne plusy:
1. Prosta budowa pliku.
2. Łatwość dodawania kolejnych danych
minusy:
1. Czasochłonne usuwanie plików z archiwum.
2. Czasochłonna zmiana kolejności plików w archiwum.
3. Aby uzyskać listę plików trzeba przeskanować całe archiwum.
Jak widać jest więcej minusów jak plusów, ale w moim przypadku przeważyła dla mnie prostota pliku, poza tym kasowanie/reorganizacja archiwum nie są zbyt częstymi operacjami więc nie były dla mnie dużym minusem.
Ok. wiemy już, co chcemy oraz jakimi środkami to uzyskamy, teraz wypisze metody, które zaimplementowałem w naszej klasie obsługującej archiwum, i opisze z grubsza (szczegóły można zrozumieć analizując dołączony kod) idee działania tych metod:
1 |
constructor Create(const filename:String; const Mode: Word); |
Tworzymy nasze archiwum, w zależności od trybu mode (patrz TFileStream + F1), tworzymy nowe archiwum lub otwieramy już istniejące. Inicjujemy wewnętrznego streama który będzie obsługiwał nasz plik.
1 |
destructor Destroy;override; |
Bez komentarza 😉
1 |
procedure PrepareFileList; |
Głównym zadaniem tej procedury jest przeskanowanie archiwum i odnalezienie nazw oraz rozmiarów plików przechowywanych w archiwum. Ponieważ układ w naszym arch. jest sekwencyjny wystarczy abyśmy pobrali nazwę i rozmiar pliku na początku którego obecnie znajduje się nasz wewnętrzny stream, zapisujemy te dane, a następnie przeskakujemy o wielkość właśnie zbadanego pliku i trafiamy na początek następnego. Czynność tą powtarzamy aż dojdziemy do końca.
1 2 3 |
procedure AddFile(const name:string; const ExtractName:boolean = true);overload; procedure AddFile(const name:string; const stream:TStream);overload; |
Dzięki tej procce dodajemy nowe dane do naszego archiwum, jak widać dane mogą być pobierane z dowolnego streama (lub pliku), który zwróci nam swój rozmiar. Metoda na początku przeskakuje na koniec naszego archiwum, a później dopisuje do niego przekazane dane. Zanim jednak to zrobi sprawdza czy w naszym archiwum nie ma już pliku o podanej nazwie. W tej implementacji możliwe jest przekazanie całej ścieżki jako nazwy pliku. Nazwa jest Case sensitive !
1 |
procedure DeleteFile(const name:string; const imediate:boolean); |
Ponieważ jak wiemy usuwanie pliku jest czasochłonne ta metoda ma dodatkowy parametr imediate, który mówi czy usunięcie ma zostać wykonane zaraz po wywołaniu tej metody. Po co takie coś ? Ano jeśli ustawimy imediate na false, każde wywołanie tej procedury zapisze tylko nazwę pliku, który ma zostać usunięty. Na sam koniec, gdy już wybierzemy wszystkie pliki do skasowania wywołujemy ExecutePendingDeletion i całe kasowanie następuje dużo szybciej (w jednym przebiegu).
1 |
procedure ExecutePendingDeletion; |
Funkcja dokonuje właściwego kasowania z archiwum (polecam przyjrzeć się metodzie DeleteFile od środka). Idea jest bardzo prosta:
1. Tworzymy tymczasowy stream, do którego będziemy kopiowali dane.
2. Ustawiamy się na początku naszego archiwum
3. Pobieramy nazwę pliku z archiwum, jeśli ta nazwa jest na liście plików do skasowania przeskakujemy do punktu 5
4. Kopiujemy plik (nazwa + dane) do tymczasowego streama
5. Przechodzimy do kolejnego pliku w naszym archiwum, i wykonujemy punkt 3. Całość powtarzamy tak długo aż skończą się dane w archiwum.
6. Zamykamy tymczasowy stream, zamykamy nasze archiwum.
7. Kasujemy nasze archiwum, dokonujemy rename tymczasowego staremu (pliku) do nazwy jaką miało nasze stare archiwum i ponownie je otwieramy
1 |
function GetNextFileName:string; |
Funkcja zapamiętuje aktualna pozycje w streamie, następnie pobiera nagłówek pliku na początku którego ustawiony jest stream po czym powraca do pozycji zapamiętanej na początku. Jako rezultat zwraca pobraną nazwę pliku.
1 |
procedure SkipNextFile; |
Funkcja pobiera rozmiar pliku, na początku którego ustawiony jest stream po czym przeskakuje o tą wartość dalej, w rezultacie trafia na początek kolejnego pliku.
1 |
function GetNextFile(out FileSize:integer; const skipHeader:boolean=true):TStream; |
Funkcja zwraca pointer do streama z którego można pobrać plik na początku którego znajduje się stream. Jeśli skipHeader jest ustawiony na true, zwrócony stream będzie pokazywał na początek danych pliku. Jeśli zaś skipHeader = false stream będzie pokazywał na nagłówek pliku. W zmiennej FileSize zostanie zwrócona wielkość pliku. UWAGA: przeczytanie mniejszej lub większej ilości danych ze zwróconego streama niż FileSize powoduje, że nasze wewnętrzny stream jest gdzieś poza nagłówkami plików. Aby móc ponownie z niego korzystać należy wywołać procedure FindFileNamed, procedury które mają w nazwie NextFile nie będą poprawnie działać !
1 |
function FindFileNamed(const name: string; out FileSize:integer; const skipHeader:boolean=true):TStream; |
Działanie jest identyczne jak w GetNextFile, jedyna różnica polega na tym, że zwrócony stream będzie ustawiony na początku pliku o zadanej nazwie.
1 |
function GetNextFileSize:integer; |
Zasada działania identyczna jak w GetNextFileName, jedyna różnica polega na tym, że zwracane są inne dane z nagłówka.
1 |
function DataAvail:boolean; |
Funkcja zwraca true jeśli nie jesteśmy na końcu archiwum. Aby to sprawdzić wystarczy porównać czy pozycja w naszym wewnętrznym streamie jest mniejsza od wielkości pliku.
1 |
procedure ReorderFiles(const OrderedNames:TStrings); |
Procedura zmienia kolejność plików w archiwum. Zasada jest zbliżona do kasowania pliku, czyli tworzymy tymczasowy stream, do którego kopiowane są kolejno dane z naszego archiwum. Różnica polega na tym, że kolejne dane które będą skopiowane są wyszukiwane metodą FindFileNamed na podstawie zmiennej OrderedNames.
Ok. To tyle, trochę zwięźle i może lakonicznie, ale kod jest naprawdę prosty, proponuje poeksperymentować i zobaczyć jak to wygląda w praktyce. Wszelakie pytania mile widziane.
Autor: Toster