Elôzô számunkban elkezdtünk írni egy olyan kis programocskát mely adatnyilvántartást volt hivatott megvalósítani Turbo Vision-ben. Ezt most egy kicsit tovább fejlesztenénk, pontosabban az eddig még el nem magyarázott pár hátralévô trükköt ezen a példaprogramon kívánom bemutatni.

 

Adattárolási megoldások

Ha emlékszünk akkor elmondanám, hogy a múltkori számban az ADATNYIL.PAS forráskódú programban volt egy dialógus melyen szöveg-bekérômezôk voltak elhelyezve, a név - cím - tel. - stb. bekérésére. A beírt adatokat lemezre - file-ba el is mentettük. Az adatok közötti lapozás úgy történt hogyha eggyel elôrébb mentünk, PgUp-pab akkor a file-ban is egy késôbbi pozícióból olvastuk ki a hozzá tartozó adatokat. Ez pl. floppy esetén rettentôen lassú lenne így szükségessé válhatna egy dinamikus tömb melynek elemei az elôzô számban részletezett TAdatok rekord lenne. Ennek a megoldásnak is van hátránya: Még programozási idôben - fordítás elôtt - meg kell adni a tömb maximális méretét. Lássunk erre egy példát a könnyebb megértés végett:

type

PAdatok = ^TAdatok;

TAdatok = record

VNev,

KNev,

Varos : String[16];

Irszam : String[4];

Utca : String[16];

HazSzam : String[3];

EgyebHossz: Word;

Egyeb : Array[1..16] of Char;

Tel : String[16];

NoteHossz : Word;

Note : Array[1..80] of Char;

end;

PPAdatokArray = ^TPAdatokArray;

TPAdatokArray = Array[1..65534 div 4] of PAdatok;

Ezzel a típus definiálással létrehoztunk egy tömböt melynek elemei típusos-pointer-ek méghozzá a PAdatok típussal rendelkezik. Ha e tömböt létre akarnánk hozni és feltölteni akkor az alábbi jelképes műveletekre lenne szükség:

...

var AdatokA: PPAdatokArray;

begin

New(AdatokA);

AdatokA^[1]:=New(PAdatok);

AdatokA^[1]^.VNev:='Bérczi';

AdatokA^[1]^.KNev:='László';

AdatokA^[1]^.Varos:='PÉCS';

AdatokA^[1]^.Irszam:='9876';

AdatokA^[1]^.Utca:='NemMondomMeg!';

AdatokA^[1]^.HazSzam:='987';

AdatokA^[1]^.EgyebHossz:=0;

{AdatokA^[1]^.Egyeb:=.. Nincs Egyeb}

AdatokA^[1]^.Tel:='06-12-345-678';

AdatokA^[1]^.NoteHossz:=0;

{AdatokA^[1]^.Note:=.. Nincs Note}

end;

Tehát elôször létre kell hozni a PAdatok pointer-ét tartalmazó tömböt a PPAdatokArray-t majd ennek külön-külön egyes elemeit, s pl. a fenti módon feltölteni. Persze a feltöltés nem lenne ilyen maceráns, lehet egyszerűbben is, ha a TAdatNyilvDialog-ból olvassuk ki a begépelt adatokar akkor így néz ki:

...

var

AdatokA: PPAdatokArray;

Melyik : Word;

procedure ...

New(AdatokA);

end;

...

procedure Feltolt;

begin

AdatokA^[Melyik]:=New(PAdatok);

TAdatNyilvDialog.GetData(AdatokA^[Melyik]^);

end;

Persze ez a fenti program lista csak képletes parancsokat tartalmaz a megértést könnyítendô.

A Melyik Word tartalmazza, hogy a lista melyik elemébe kívánjuk elmenteni a dialógusban most begépelt adatokat. (Ha a legutolsóba akkor legyen az utolsó elem mindig eltárolva, s a Melyik:=Utolso+1;)

Még egy utolsó elvi lehetôséget elmondok arra, hogy hogyan lehetne tárolni e adatokat, hogy azután elmondhassam, hogy valójában hogy a legcélszerűbb az adatokat tárolni. Tegyük láncolt listába az adatokat !

PAdatokElem = ^ TAdatokElem;

TAdatokElem = record

Adat : TAdatok;

Elozo,

Kovetkezo: PAdatokElem;

end;

Minden egyes TAdatokElem rekord tartalmazza az tényleges adatokat melyekre a TAdatokElem.Adat módon hivatkozhatunk. A rekord Elozo mezôje a sorrendben (pl. abc) ôt megelôzô elemre mutat, míg a Kovetkezo a ô utána követezôre. Miért jó ez ? Mert NINCS megszabva, mint a tömbös megoldásnál, hogy csak egy szegmensnyi pointer-t tárolhatunk egyszerre az-az 65534 div 4 = 16383-at. HANEM pl. a protected módban dolgozunk a láncolt listában csak a memória mérete szabja meg mennyi elemünk van. Tegyük fel, hogy pl. 32 MB RAM van a gépünkben, s ebbôl szabad 25 MB. A védett programban belépéskor ennyit is látunk szabad Heap-nek. Számoljuk ki hány TAdatokElem rekordot tárolhatunk el. 25 MB = 25 * 1024 * 1024 Byte ezt osszuk a TAdatokElem hosszával: 202 Byte az így kapott a memóriában jelen esetben elhelyezhetô maximális TAdatokElem rekordok száma: 129774 db. Ez mégis több mint a tömbös megoldásnál.(Tegyük fel, hogy csak 4 MB RAM-unk van ekkor utánaszámolhatunk, hogy csak 16221 db fér el a memóriában. Akkor rosszabb ez esetben a láncolt-listás megoldás ? Dehogy-is logikus, ha 4 MB-k van semmilyen megoldással sem fér el benne több adat mint amennyi, hiába van a tömb 16383 db-ra deklarálva ha nem elég a memória akkor ennél kevesebb fér csak bele.) A láncolt-listás megoldás feltöltése se nehezebb, ugyan úgy létre kell hozni a New()-val a PAdatokElem rekordot, SetData()-val beállítani az adatokat (vagy HDD-rôl elôször beolvasni), és az Elozo nevű mezôt az alábbiak szerint feltölteni. Kell lennie egy TempAdatokElem: PAdatokElem rekordnak meny az utolsó elem címét tartalmazza, s ha új elemet hozunk létre ennek Elozo mezôjét ezzel egyenlôvé tenni. Ha az új elem kész akkor a TempAdatokElem.Kovetkezo mezôjét pedig az új elem címével egyenlôvé tenni. Az utolsó elem Kovetkezo mezôje mindaddig nil míg az új elem ezt fel nem tölti. Továbbá az elsô elem Elozo nevű mezôje is nil hisz nincs elôzô, hacsak nem akarunk zárt-láncolt-listát létrehozni amin körbe lehet menni. Lássuk a megvalósítást képletesen:

var TempAdatokElem: PAdatokElem; {az elôzô PAdatokElem címe!}

{...}

procedure UjElem;

var UjAdat: PAdatokElem;

begin

New(UjAdat);

TAdatNyilDialog.GetData(UjAdat^.Adat);

UjAdat.Elozo:=TempAdatokElem;

UjAdat.Kovetkezo:=nil;

TempAdatokElem.Kovetkezo:=UjAdat;

end;

Ezzel kimerítettük az ilyen jellegű adattárolási elveket KIVÉVE EGYET a legjobbat !

Miért beszéltem ennyit, ha az elôzô 2 oldalon keresztül nem a leghatékonyabb algoritmusok - ill. tárolási elvek vannak felsorolva ?

  1. Mert az elôzô formulák máskor máshol NAGY segítséget jelenthet azoknak akik még nem ismerték eddig
  2. Mert a legjobb megoldás is részint ezekre épül
  3. Végül, hogy lássuk mitôl a legjobb megoldás a legjobb !

Mi a hátránya az elôzô megoldásoknak ? (Van elônye is !)

Láthattuk, hogy egy adatösszegyűjtés során pl. a tömbös megoldás során szükséges a tömbbe rakandó rekord típusának programozás idôben való ismerete. Azért a tömbös megoldást vettem elô mert leginkább erre hasonlítanak (sajnos - lásd alább) a kollekciók.

 

Kollekciók

Tehát valami azt súghatja az Olvasóknak ha már ennyire erôltetjük a kollekciókat, akkor annak valami elônye lehet. Emlékszünk még a Stream-ekre ??? A hűséges Olvasók elôtt már ebben a minutában minden megvilágosodott, de akinek nem volt lehetôsége végig figyelemmel kísérni minket az se marad ‘bajban'. (A régi számok megtalálhatóak: :\MELYVIZ\PC-XUSER\!OLDUSER\*.arj formában.) Térjünk vissza, miért - s mire is jók a stream-ek ? Nem szükséges a típus ismerete a stream-re íráskor, csak az adott típust kell regisztrálni (ha ??? mi az a regisztráció akkor lásd az elôzô számokat !), sôt még az sincs kikötve - tehát egyszerűen, s nagyszerűen megoldható, hogy különbözô de egy absztrakt ôssel rendelkezô típusú elemeket írjunk a kollekcióba. (Azt már említeni se merem, hogy nem is szükséges objektum-nak lennie a kollekció elemének hanem lehet rekord, vagy más adatszerkezet - ha csak EGY típust kívánunk kollekcióba foglalni. Erre, legjobb példa a TStringCollection.)

Nézzük a leggyakoribb kollekciók tervezési lépéseit:

  • Hozzunk létre egy adat objektumot, amit tárolni - rendszerbe foglalni kívánunk
  • Töltsük be az adatot egy stream-rôl
  • Most dolgozhatunk az adat-rekordokkal (kiírhatjuk - módosíthatjuk - újat hozhatunk létre)

Láttuk az elôzô részben, hogy az egy ablakon lévô kontrollok egy rekordban tárolják az adatot, pontosabban egy az ablak kontroll felépítéséhez hasonló rekordot definiáltunk, hogy a kontrollokban lévô adatokkal műveletet végezhessünk. Ezen rekordot, ill. ezen rekordok összességét kívánjuk kollekcióban tárolni. A fent elmondottak szerint létre kell hoznunk egy objektumot (a TObject-bôl származtatva) ami részint tartalmazza az ablak kontrolljai által meghatározott rekordot, ill. az ezen adatokat kezelô metódusokat. Pontokba szedve az objektumnak tartalmaznia kell:

  • az adat(okat) rögzítô mezô(ket)
  • egy Store metódust mely az adatokat egy stream-re írja
  • ugyanígy egy Load konstruktort mely a hasonló stream-rôl az adatokat olvassa be
  • s nem utolsó sorban a létre kell hozni az objektum-hoz tartozó regisztrációs rekordot !

type

PAdatok = ^TAdatok;

TAdatok = record

VNev,

KNev,

Varos : String[16];

Irszam : String[4];

Utca : String[16];

HazSzam : String[3];

EgyebHossz: Word;

Egyeb : Array[1..16] of Char;

Tel : String[16];

NoteHossz : Word;

Note : Array[1..80] of Char;

end;

PAdatokObj = ^TAdatokObj;

TAdatokObj = Object(TObject)

AdatRec: TAdatok;

constructor Load(var S: Tstrem);

procedure Store(var S: Tstrem);

end;

constructor TAdatokObj.Load(var S: TStream);

begin

Inherited Init;

S.Read(AdatRec, SizeOf(AdatRec));

end;

procedure TAdatokObj.Store(var S: TStream);

begin

S.Write(AdatRec, SizeOf(AdatRec));

end;

const

RAdatokObj: TStreamRec = (

ObjType: 15000;

VTMLink: Ofs(TypeOf(TAdatokObj)^);

Load : @TAdatokObj.Load;

Store : @TAdatokObj.Store;

);

Dióhéjban a fentiekrôl:

  • A TAdatok rekord már az elôzô számból ismert, hogy miért így építettük így fel ...
  • A konstruktor Load-ban nyilvánvaló, hogy meghívjuk az elôd konstruktorát ami nem más, mint az Init.
    S ez után mivel a Load metódusról (itt konstruktorról) van szó a cím szerint átadott S stream-bôl beolvassuk az objektum (TAdatokObj) publikus változójába az AdatRec-be a stream-en csücsülô hozzánk szóló (nekünk küldött) adatot.
  • A Store metódusnál meg épp fordítva, a stream-re írjuk az adatokat
  • Ami még kérdéses lehet az az objektum regisztrációja - igaz már volt róla szó a stream-eknél - itt az az egy kikötés van, hogy az ObjType-nak Word értéknek kell lennie kivéve 0-99 között nem lehet, mert ott a TV beépített objektumainak regisztrációja van. (Persze az is logikus, hogy ne írjuk rá az egyik objektumnak a regisztrációját a másikra, mert ekkor futás közben stream hibát ad vissza.) A regisztrálás többi lépése hasonlóan - értelemszerűen adódik.
  • Persze NE felejtsük el a RegisterType() procedure-ával regisztráltatni a típust !

 

A kollekció betöltése

Mivel mi rendes gyerekek vagyunk, s úgy használjuk elsô nekifutásból a TCollection-t ahogy Borland-ék kitalálták, ezért könnyű dolgunk lesz s nyugodtan dolgozhatunk a TCollection-nel anélkül, hogy át kellene írnunk. (Akkor kellene átírnunk, ha nem objektumokkal, s nem ilyen egyszerű dolgokkal játszódnánk.) Ugyanis a TAdatokObj regisztrálva van, s így íráskor - olvasáskor a regisztrációs számból tud a kollekció olyan messzemenô következtetéseket levonni, hogy mi is a típusa azon becses elemnek. (Itt az elem, mint a kollekció eleme szerepel.)

Elôzô számunkban egy ADATOK.DAT file-be mentettük a TAdatok rekordokat. Most is egy ADATOK.DAT nevű file-ba mentjük az adatokat, de nyilván stream-el mentjük le az egész kollekciót, így szükségszerűen különbözni fog a rekordos megoldású ADATOK.DAT a kollekcióstól. NE keverjük össze ezeket a file-okat ! Lássuk a kollekciós beolvasást, s utána a szükséges egyebeket ...:

var

CurrentAdat: Integer;

AdatColl : PCollection;

procedure LoadAdatok;

var AdatFile: TBufStream;

begin

AdatFile.Init('ADATOK.DAT', stOpenRead, 1024);

AdatColl := PCollection(AdatFile.Get);

AdatFile.Done;

end;

Láthatjuk, hogy a programban globálisnak deklaráltunk egy CurrentAdat integer-t mely az aktuális (melyen éppen műveletet végzünk) kollekció elem számát tartalmazza.

A lényeg az AdatColl, mely PCollection típusos pointer objektum, mely maga a kollekció.

A LoadAdatok; procedure valósítja meg a beolvasását a TAdatokObj objektumok kollekciójának egy stream-rôl. Nem kell feltétlenül egy TBufStream-nek (File- alapú stream, A TDOSStream leszármazottja) lennie lehet akár egy TXMSStream avagy egy TEMSStream, csak a gép kikapcsolása után a háttértárolón lehet tárolni az adatokat, melyek a stream-et alkotják.

  • Ugye deklaráljuk a stream-et, ahonnan beolvassuk a kollekciót
  • Initializáljuk a stream-et, megadjuk mit - honnan - meddig ...
  • A stream Get parancsával olvassuk be arról, a memóriába a kollekciót. (A TStream Get metódusa csak egy PObject-et ad vissza - egy memória címet ahova az objektumot, a kollekciót beolvasta -, így típus konverziót kell végrehajtanunk, hogy az AdatColl-lal egyenlôvé tehessünk a beolvasottakat.)
  • Végül lezárjuk a stream-et, hisz már beolvastuk mire szükségünk volt.

Láthatjuk az elônyét az adatok kollekciókban való tárolásának, csak pár sor és bent van a memóriában, nem kell nekünk kézileg helyet lefoglalni az esetlegesen beolvasandó rekordoknak, vagy a file végét várni. Ezt helyettünk megteszi a stream és a kollekció.

 

A kollekció elemeinek képernyôre varázslása

Dolgunk innen már csak egyszerűsödni fog, hisz már a memóriában csücsül a kollekció, elemeit szabadon másoljuk - kiírjuk. A fenti LoadAdatok; proecedure-át nyílván még akkor kel lefuttatni (tehát a beolvasó rutint akkor kell lefuttatni) mielôtt dolgoznánk az általa beolvasandó adatokkal, legyen ez az applikáció konstruktorában, hisz ennél elôbb csak a mazoista dolgozik vagy akar dolgozni ezen adatokkal.

Regisztráltassuk a TAdatObj típust, hogy a stream nehogy maga alá essen. Íme:

var TempAdatok: TAdatok; {A program globális részében}

constructor TMyApp.Init;

{var ...}

begin

{... ide a régebben tárgyalt meghívások jönnek, lásd elôzô szám !}

RegisterType(RAdatObj);

LoadAdatok;

CurrentAdat := 0;

end;

  • deklaráljuk az a változót melynek segítségével az adatcserét végezzük a kollekció és az adat-ablak között
  • regisztráltatjuk az új típust, s persze az elôzô szám szerint a beépített TV objektumokat is, pl.: a RegisterView, stb. procedure-ákkal.
  • betöltjük a kollekciót a stream-rôl
  • az elsô elem jelen esetben a 0.

Futtassuk le a programot, s ezt látjuk:

Ha a programnak ilyen állapotában klikkantanánk az Elment gombra nem sokat érnénk, csak teljesen összekavarnánk szegény programot, így írjuk meg az új elmentô metódust mely nem rekord hanem kollekció alapon menti el az ablakban látható adatokat. (Pontosabban a kollekcióba) :

procedure TAdatnyilvDialog.SaveAdatokColl;

begin

if Valid(cmClose) then

begin

GetData(TempAdatok);

PAdatokObj(AdatColl^.At(CurrentAdat))^.AdatRec := TempAdatok;

SaveAdatok;

end;

end;

procedure SaveAdatok;

var AdatFile: TBufStream;

begin

AdatFile.Init(‘ADATOK.DAT', stOpenWrite, 1024);

AdatFile.Put(AdatColl);

AdatFile.Done;

end;

A fentiek az eddig elmondottakra épülve egyértelműek, lássuk milyen megvalósítás szükséges ahhoz, hogy az elôzô és következô elemét láthassuk a kollekciónak (az AdatnyilvDialog Következô - Elôzô gombjait használhassuk).

Ezt az elôzô számban elmondottak szerint a HandleEvent()-ben oldjuk meg hisz itt kezeljük le azon eseményeket melyeket az Elôzô - Következô gombok adnak ki. S ezen eseményeket most kollekció orientáltan kívánjuk megoldani. Íme:

procedure TAdatnyilvDialog.HandleEvent(var Event: TEvent);

var B: Boolean;

begin

B:=True;

Inherited HandleEvent(Event);

if (Event.What = evBroadcast) and (Event.Command = cmAdatnyilvDialogLetezel)

then ClearEvent(Event);

if (Event.What = evCommand) then

begin

case Event.Command of

cmClose : Close;

cmTHatra: ShowAdatRec(CurrentAdat - 1);

cmTElore: ShowAdatRec(CurrentAdat + 1);

{Lásd még lent a cmSaveUj és cmUjAdat parancsok lekezelését ! Ide kell beszúrni !}

{cmXXX : {... a többi marad a régi}

end;

end;

if B then ClearEvent(Event);

end;

Ha ilyen elegánsan sikerült bújtatni a kérdést akkor el kell árulnom hogyan kell a ShowAdatRec() proecedure-t megírni.

procedure TMyApp.ShowAdatRec(AAdatNum: Integer);

begin

CurrentAdat := AAdatNum;

TempAdatok := PAdatObj(AdatColl^.At(CurrentAdat))^.AdatRec;

AdatnyilDialog^.SetData(TempAdatok);

if CurrentAdat > 0

then EnableCommands([cmTHatra])

else DisableCommands([cmTHatra]);

if AdatColl^.Count > 0 then EnableCommands([cmTElore]);

if CurrentAdat >= AdatColl^.Count - 1 then DisableCommands([cmTElore]);

if CanWrite then EnableCommands([cmSaveUj, cmUjAdat]); {Ha nem read-only a file!}

end;

Rövidke magyarázat:

Elôzô számunkban már taglaltuk a cmTElore és cmTHatra parancsokat. Most is ugyanúgy felhasználjuk ôket, de más műveleteket, kollekció műveleteket csoportosítunk hozzájuk.

  • Lehet pár kikötést tenni: Ha a kollekció utolsó elemét írjuk be - vagy módosítjuk akkor le kell tiltani a cmTElore parancsot, hisz nincs következô elem,
  • ugyanígy az elsônél is, ha az elsô elemet editáljuk - vagy csak rajta állunk - akkor letiltjuk a cmTHatra parancsot, s ez által a letiltott parancshoz tartozó gomb szürke lesz, nem lehet megnyomni.
  • Ez a legegyszerűbb módja annak, hogy a felhasználóval közöljük, hogy nincs elôrébb - vagy hátrább adat. Sokkal kényelmetlenebb lenne mindenkinek, pl.: egy message-et küldeni vagy stb.

Persze módosítani kell az AdatnyilvántartóDialógus-t megnyitó metódust is:

Ez nem áll másból, mint hogy kitöröljük a múlt szám beli file-rekordos megoldási metódus hívásokat, s a TAdatnyilDialog.Init()-je végére (a kontrollok létrehozása után !) odabiggyesztjük:

ShowAdatRec(CurrentAdat);

Setdata(TempAdatok);

CheckCanWeWrite;

  • Az elsô a TempAdatok adat rekordba másolja az éppen aktuális kollekció elemet. Ez initializáláskor 0, mert a TMyApp.Init-ben ezt állítottuk be !
  • A második a kontrollokat tölti fel az elôzôekben megszerzett adatokkal
  • Majd a 3. metódus megvizsgálja, hogy nem read-only -e a stream amibôl olvasunk, mert akkor nem lehet új adatot elmenteni ! (A CheckCanWeWrite ugyan az, mint a múlt számban a FileOpen metódus hasonló rutinja !)

Már csak egy lépés van hátra az új adatok kollekcióba másolása - ill. elmentése. Ezt a múlt számhoz hasonlóan nem külön metódusban írjuk meg, hanem simán a TAdatnyilDialog HandleEvent()-jében, hisz ide érkeznek be a parancsok, s itt kezeljük le ôket. Az alábbi forráskódot a a fenti HandleEvent()-be a megfelelô helyre kell beszúrni:

{... elôzô cmXXXX utasítások a case-n belűl !}

cmUjAdat: begin {új adat bevitele}

ClearEvent(Event); {az esemény törlése}

Event.What:=evKeyDown; {esemény generálása, mintha billentyűt nyomtunk volna}

Event.KeyCode:=kbAltZ; {mintha az Alt+Z nyomtuk volna meg}

PutEvent(Event); {ennek hatására a Vezetéknév kontrollra ugrik}

B:=False; {beállítjuk, hogy nem kell a case után ClearEvent()-et

hívni, mert már megtörtént}

CurrentAdat:=AdatColl^.Count; {az utolsó elemre mutat}

TempAdatokObj:=New(PAdatokObj, Init); {az átmeneti AdatObj-et létrehozzuk}

TempAdatok:=TempAdatokObj^.AdatRec; {az adatrekord = az új AdatObj rekordjával}

SetData(TempAdatok); {az ablak kontrolljainak átadjuk az üres adatot}

end;

cmSaveUj: begin {az új adat elmentése}

AdatColl^.Insert(TempAdatokObj); {most, hogy már elmentjük a kollekció

eleme lesz az eddig átmeneti TempAdatokObj.}

SaveAdatokColl; {meghívjuk az adatelmentô metódust}

end;

{... folytatódik a case}

Azt hiszem mára ennyi, következô számunkban minden valószínűség szerint saját view-ot hozunk létre s lehet, hogy elkezdjük átírni a Turbo Vision-t ‘Graph Like Turbo Vision'-re az-az valami hasonlóra, mint a Norton Utilities. (Ennek példájára, lásd a TVDEMO.EXE-t !)

Ha a ma elmondottak nem lennének egyértelműek, akkor erôsen nézd meg a példaprogramot és cikket újra, ha így se érthetô akkor eMail-ezz, örömmel állok segítségedre !