Delphi - Egyedi állomány típus készítése

forráskód letöltése
Ebben a példában létrehozunk egy olyan komponenst, melynek segítségével egyszerűen létrehozhatunk és kezelhetünk egy általunk definiált állomány típust. Ebbe az állományba tetszőleges számú blokkot helyezhetünk el. Minden blokk tetszőleges típusú adatot tartalmazhat. A komponens segítségével ezeket a blokkokat írhatjuk illetve olvashatjuk az állományból. A mellékelt példaprogram megnyitása előtt az AsFile.pas-ban lévő komponenst telepítenie kell a Delphi alá.

A komponens működése két fő területre osztható: az állomány írása és olvasása.

Nézzük először az írást, mely egyben egy új állomány létrehozására is alkalmazható. Első lépésként meg kell adnunk az állomány szerkezetét, vagyis azt, hogy hány blokk lesz benne és ezeknek mi a mérete.

Ehhez az Items property-n belül kell létrehoznunk annyi objektumot, ahány blokk lesz. Mellékelt példában az AsFile1 komponenst használjuk az új állomány létrehozására, így ennek az Items property-e lett kitöltve.

Három blokkot veszünk fel. A blokk méretét bájtokban a Size property-nél kell megadni, minden egyes blokknál. Az első kettőnél 8-ra állítjuk, míg az utolsónál 0-ra, vagyis az utolsó blokkba nem írunk adatot, ez most csak a teszt miatt kerül létrehozásra.

A BlockType property jelenleg még nem használt, későbbi továbbfejlesztésre lett csak létrehozva, így értéke tetszőleges lehet.

Ha felvettük az Items-be az állományunk blokkjait, akkor még be kell állítanunk a komponensnél a MajorVersion és a MinorVersion property-ket, ahol az állományunk fő és alverzió számát adhatjuk meg.

A Signature property-ben megadhatunk egy maximum 8 karakteres egyedi azonosítót, mely alapján eldönthető a későbbiek folyamán, hogy egy adott állomány a mi típusunk-e vagy sem függetlenül az állomány kiterjesztésétől.

Végül, ami a leglényegesebb: a FileName property-ben meg kell adnunk az állomány nevét, elérési útvonallal együtt. Az állomány kiterjesztése teljesen egyedi lehet.

Ha ez is megvan, akkor létre kell hoznunk az OnWrite eseményhez egy kezelő eljárást. Amikor megkezdődik majd az állomány írása, akkor a komponens ezen az eseményen keresztül kéri be az egyes blokkok tartalmát.

Fenti beállítások után már meghívhatjuk a komponens Write eljárását, mely elvégzi az állomány létrehozását és az adatok (blokkok) kiírását. Ha a FileName property-ben már létező állományt adtunk meg, akkor a Write ezt szó nélkül felülírja az új adatokkal, így ha ezt szeretnénk elkerülni, akkor figyeljünk erre.

Állomány írásához az Active property legyen hamis, ezt csak olvasásnál kell majd használnunk.

Nézzük most közelebbről a Write-ot. A property-k beállítása után egy gomb lenyomásakor meghívjuk.
procedure TForm1.Button1Click(Sender: TObject);
begin
  AsFile1.Write;
end;
A Write egy TFileStream osztály segítségével végzi az állomány létrehozását és írását. Ha minden szükséges property ki lett töltve ehhez a művelethez, csak akkor kezd munkába az eljárás. Ekkor első lépésként a WriteHeader belső eljárás hívásával az állomány fejléc adatai kerülnek kiírásra, majd egy ciklus segítségével végigmegyünk az Items tömb property összes elemén és ahány blokk van, annyiszor meghívásra kerül az OnWrite eseményhez rendelt eljárás. Amennyiben az esemény által valós memória területre mutató pointer-t kaptunk vissza, akkor a FileStream-al elvégezzük ennek a területnek az állományba írását is.
procedure TAsFile.Write;
var
  st: TFileStream;
  i: integer;
  buffer: pointer;
begin
  if Assigned(FOnWrite) and (FFileName<>'') 
       and not FActive then begin
    st:=TFileStream.Create(FFileName, fmCreate);
    try
      WriteHeader(st);
      for i:=0 to FItems.Count-1 do begin
        if FItems[i].Size>0 then begin
          buffer:=nil;
          FOnWrite(i, buffer);
          if buffer<>nil then begin
            st.Write(buffer^, FItems[i].Size);
          end;
        end;
      end;
    finally
      st.Free;
    end;
  end;
end;
Az OnWrite eseményt az alábbiak szerint kezeljük le: az esemény első paramétereként kapjuk meg, hogy hányadik blokkról van szó. A második paraméterben kell visszaadnunk azt a pointert, mely az állományba írandó memória területre mutat.
procedure TForm1.AsFile1Write(BlockID: Integer;
  var Buffer: Pointer);
begin
  case BlockID of
    0: begin
      a:='abcdefgh';
      Buffer:=@a;
    end;
    1: begin
      b:='12345678';
      Buffer:=@b;
    end;
  end;
end;
Nézzük most a WriteHeader eljárást, mellyel az állomány fejléc adatait írjuk ki. Az egyedi állományunkba először egy THeadRecord struktúra kerül kiírásra. Ez tartalmazza a komponens Signature, MajorVersion, MinorVersion property-einek értékét, valamint egy számot, mely megmondja, hogy az Items property-nek hány eleme van. Mivel ennek a struktúrának fix a mérete, így ezt könnyedén írhatjuk, olvashatjuk az állományból.
Második lépésként ki kell írnunk az állományba az Items property értékeit. Ennek száma viszont tetszőleges lehet, így nem tudhatjuk előre azt sem, hogy mekkora lesz a kiírandó memória terület mérete sem. Ezért is kellett az előbb eltárolni az Items elemeinek számát.
A ReallocMem függvénnyel foglalunk egy akkora memória területet, ahol az Items összes adata elfér, majd egy ciklussal fel is töltjük ezt. Végül már csak az állományba kell írnunk.
procedure TAsFile.WriteHeader(st: TFileStream);
var
  i, size: integer;
  buffer: PHeadTableRecordArray;
  head: THeadRecord;
begin
  size:=SizeOf(THeadRecord);
  FillChar(head, size, 0);
  StrPCopy(head.Signature, FSignature);
  head.MajorVersion:=FMajorVersion;
  head.MinorVersion:=FMinorVersion;
  head.HeadTableCount:=FItems.Count;
  st.Write(head, size);

  if FItems.Count>0 then begin
    size:=SizeOf(THeadTableRecord)*FItems.Count;
    buffer:=nil;
    ReallocMem(buffer, size);
    try
      FillChar(buffer^, size, 0);
      for i:=0 to FItems.Count-1 do begin
        with buffer^[i] do begin
          BlockType:=FItems[i].FBlockType;
          Size:=FItems[i].Size;
        end;
      end;
      st.Write(buffer^, size);
    finally
      ReallocMem(buffer, 0);
    end;
  end;
end;
Fentiek alapján tehát az egyedi állomány három nagy részre bontható:

1. Egy THeadRecord típusú struktúra, melynek fix a mérete és az minden állományban egyforma lesz. Ide kerülnek az állomány alapvető adatai.

2. Egy változó méretű terület (tömb) következik, melyen az Items property adatai kapnak helyet. Hogy ennek mekkora lesz a területe, az az Items property elemeinek számától függ. Egy-egy elemhez egy THeadTableRecord struktúra tartozik. Mivel az elemek száma tetszőleges lehet (egy állományban annyi blokkot hozunk létre, amennyit akarunk), így a THeadTableRecord struktúrából egy tömböt kell eltárolnunk.

3. Ide jönnek egymás után a felhasználói adatok, vagyis a blokkok.


Ezzel az állományt létre is hoztuk, az adatainkat kiírtuk, nézzük most, hogy miként szerezhetjük vissza a kiírt állományokat.

Mellékelt példában az AsFile2 komponenst használjuk az imént kiírt állomány adatainak visszaolvasására.

Olvasáshoz csupán a FileName property-t kell beállítanunk a komponensnél miután feltettük a Form-ra.

Ha ezek után az Active property-t igazra állítjuk, akkor a MajorVersion, MinorVersion, Signature property-k rögtön megkapják az állományban tárolt értéküket. Az Items property szintén feltöltésre kerül és annyi eleme lesz, ahány blokkot tárolunk az állományban.

Az Active property helyett programból használhatjuk az Open és Close eljárásokat is az állomány nyitására, zárására.

Az egyes blokkokban tárolt adatok kiolvasása az alábbiak szerint történhet.

Az állomány megnyitása után rendelkezésünkre áll az Items tömb property-ben, hogy hány és mekkora méretű blokk található az állományban. Így egy ciklus segítségével végig mehetünk az összes blokkon és Read eljárás hívásával beolvashatjuk őket. A Read-nél első paraméterként a kért blokk sorszámát kell megadnunk, másodikként egy pointert kell átadni. Ez utóbbira kerül beolvasásra az állományban lévő blokk.
procedure TForm1.Button2Click(Sender: TObject);
var
  i: integer;
  buffer: array[0..99] of char;
begin
  AsFile2.Open;
  for i:=0 to AsFile2.Items.Count-1 do begin
    FillChar(buffer, 100, 0);
    AsFile2.Read(i, @buffer);
    ListBox1.Items.Add(buffer);
  end;
  AsFile2.Close;
end;
A Read és Write között nagy különbség, hogy olvasni az állományból tetszőleges blokkot lehet, nem kell az összest egyszerre és a blokkok olvasási sorrendje is tetszőleges, vagyis lehet, hogy csak az ötödik blokkot olvassuk és a többit nem is. Írás esetén viszont minden esetben az első blokktól az utolsóig az összest ki kell írni, nincs lehetőség arra, hogy csak a második blokkot írjuk ki és a többit érintetlenül hagyjuk. Erre azért van szükség, mert a blokkok mérete állandóan változhat a program során, így ha csak a másodikat írnánk ki az állományba, akkor lehet, hogy az ott lévő hely kisebb már, mint amit kiírnánk, így felülírnánk a következő blokk elejét, ami adatvesztést okozna.