OOP 10 - Dialógus tervezés II
Elôzô számunkban odáig jutottunk el, hogy volt már egy adatmezôs ablakunk, ahol nevet - lakcímet -telefont kérhettünk be. Most tovább kellene lépni s, megtanulni használni ezeket a vezérlôket (controls = kontollok). Nem tudunk még értéket adni neki, írni / olvasni. Három lépésbôl áll e folyamat:
Az adat rekord létrehozása
Hogy írjuk vagy olvassuk az ablak adatmezôit, létre kell hozni egy buffer-t - ez leggyakrabban egy adat rekord - ami tárolja annak adatait. (Az adatmezôk, s egyéb kontrollok állapotát.)
A rekord egyes mezôit az ablakon látható kontrollok SetData() GetData()-ája határozza meg. Ezek a metódusok ( SetData() és GetData() ) valamilyen adatot várnak. Pl.: az InputLine - szöveg bekérô sor - logikusan String-et vár, de hossz nem mindegy egyenlônek kell lennie az initializálásor megadott AMaxLen: Integer értékével.
A TRadioButton.SetData() leírja, hogy egy numerikus változót a Value mezôt állítja be, de a hosszára még rá kell jönnünk. Ha elôszörre nem írják le az azt jelenti, hogy az elôdjét kell megnézni, s ott a Value mezôt. Az elôdje a TCluster s itt található meg a Value mezô melynek most már kiderült, hogy Word a típusa. Ezeket az információkat a Help-bôl szerezhetjük meg. (A TVISION.TPH file-ban vannak. Help/Files menüpont)
Ezek után az adott típust a rekord újabb mezôjének illesztjük be.
FONTOS: Milyen sorrendben vesszük a kontrollok típusait ?
A TAB sorrendben, ami általában megegyezik a létrehozás sorrendjével.
Nézzünk példát a rekord létrehozására (az elôzô szám alapján folytatom):
Tehát a kép ez volt, az initializálások pedig a forráskódból kiválogatva ezek:
New(Vezetek, Init(R, 16)); {Vezetek: TInputLine}
New(Kereszt, Init(R, 16)); {Kereszt: TInputLine}
New(Varos, Init(R, 16)); {Varos: TInputLine}
New(Iranytszam, Init(R, 4)); {Iranytszam: TInputLine}
New(Utca, Init(R, 16)); {Utca: TInputLine}
New(Hazszam, Init(R, 3)); {Hazszam: TInputLine}
New(Egyeb, Init(R, nil, nil, nil, 16)); {Egyeb: TMemo}
New(Tel, Init(R, 16)); {Tel: TInputLine}
New(Note, Init(R, nil, nil, nil, 80)); {Note: TMemo}
Nos itt a TAB sorrend (FIGYELEM meg kell nézni a futó programban a TAB sorrendet is !), a kontrollok típusa - ami nem annyira fontos, mint - az általuk kívánt adattípus - hossz is meg van. A TMemo-ról még annyit kell tudni, hogy a TEditor utóda és a beleírható szöveg max. hossza 64KB, de a tényleges hosszt az initializáláskor adtuk meg. Így ilyen hosszú karaktertömböt (Array[1..hossz] of Char) csinálunk. De lényeg most jön: minden karakter tömb elôtt amit TMemo használ egy Word-nek kell állnia amibe a TMemo-ban lévô ill. leendô szöveg hossza kerül !!! Jöjjön a rekord:
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;
Íme kész a rekord ami leírja az ablak kontroljainak adatcsere mezôit.
Összefoglalom milyen kontroll milyen milyen típusú adatot használ fel az adatcserére:
kontroll típus |
Az adatcseréhez felhasznált típus: |
TInputLine |
String[AMaxLen] |
TMemo |
Word az aktuális szöveghosszra, és a szöveg tárolására Array[1..AMaxLen] of Char |
TRadioButton és |
|
TCheckBoxes |
Word mely az aktuális bekapcsolt állapotot hivatott tartalmazni |
TLabel és |
|
TButton |
Nincs szükség adat mezôre ! |
TListBox |
TCollection megfelelô sorrendjét |
Az ablak kontrolljainak feltöltése adatokkal
Most, hogy tudjuk - ismerjük a Pelda01 program TProbaDialog ablakának kotroljait leíró adatcserés rekordot felettéb egyszerûen írhatunk be adatot. Ehhez az alábbi lépésekre van szükség:
A SetData()-ról kicsit bôvebben:
A TGroup()tól öröklôdik, s meghívódik minden subview-ra (al-view-ra) a Z-order szerint mely már megbeszéltük a desktop-ra vagy applikációra való inzertálási (insert) sorrend.
Most jön a kontrollok feltöltése adatokkal. Az okosok azt mondták (pl.: a tutor09.pas - a BP TV Tutoral-ja), hogy hozzunk létre egy globális változót a fent definiált rekord számára. Nos én nem ezt mondom, sôt valami szerintes sokkal szebbet mondok: hozzunk létre a TProbaDialog-ba egy public - mindenki számára elérhetô - Adatok mezôt melynek típusa a Tadatok. Miért jó ez ? Mert az Adatok változó nyílván ahhoz az ablakhoz tartozik mely ki írja - kezeli ôt. Gondoljunk bele 23 másik fajta ablakunk van s mindegyiket a programban globálisnak deklaráljuk, ha a programot debuggoljuk akkor csak kapkodnánk a fejünket, hogy melyik Adat változó melyik. Jöhet a kérdés most akkor hogy kezeljük az Adatok változót, pl. az applikációból ?
Egyszerû az Applikációnak egy private mezôje a TProbaDialog melynek viszont publikus mezôje az Adatok változó. Így hivatkozunk rá:
ProbaDialog^.Adatok.VNev:=‘Bérczi’;
lehet, hogy kacifántosnak tûnik, de: így is mindenki eléri akinek szüksége van | lehet rá (ha valamelyik metódus nem éri el, akkor az biztos nem az applikáció része, tehát nem szabványos logikus az OOP-s programunk.), s még késôbb is áttekinthetô a program.
Lássuk a konkrét adat feltöltést:
A TProbaDialog konstruktor init-jében a kontrollok (avagy még magyartalanabbul vezérlôk) behelyezése (insert-álása) után helyezzük el az ablak kezdeti érték megadását az alábbi forma szerint:
with Adatok do
begin
VNev:='Bérczi';
KNev:='László';
Varos:='PÉCS';
Irszam:='7632';
Utca:='NemMondom meg tér';
HazSzam:='99';
EgyebHossz:=14;
Egyeb:='R4s Technology'#0#0;
Tel:='06-72-999-999';
NoteHossz:=19;
Note:='Nincs más mondandóm'#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0+
#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0#0;
end;
SetData(Adatok);
Mit miért csináltunk így ?
Mivel a TProbaDialog initjében vagyunk ez azt jelenti hogyha az objektumunk saját metódusait hívjuk meg akkor nem kell az objektum megnevezés, elég a metódus név.
A rekord elemeire, meg úgy hivatkozhatunk legegyszerûbben, - ennek köztudottnak kell lennie - ha with-do-t használunk. A VNEV a vezetéknév míg a KNEV logikusan a keresztnév. A Varos, Irszam, Utca, HazSzam neveket egyértelmûen rendeltetésüknek megfelelôen használjuk. Az EgyebHossz jelenti a helyileg utána kövekezô Egyeb Char (karakter) tömb-ben lévô szöveg hosszát Word-ben ! (Úgy, mint a String 0. eleme ! csak az Byte). A Tel változó is egy String, melyet ÍGY TÖLTÜNK fel: 06-xx-yyy-zzz. Még pedig azárt, mert késôbb így fogjuk használni. (Lásd validating !) A Note a megjegyzés hossza alias Egyeb, míg a Note char array-be megy a tényleges megjegyzés.
Miért raktuk oda azt a rengeteg 0-át ?
Mert egy tömb minden elemét fel kell sorolnunk értékadáskor. Ezt a úgy kerülhetjük ki, hogy írunk egy procedure-át, melynek egy String-et adunk meg amiben a szöveg van, FELTÉVE, hogy nem ekarunk 255 karakternél hosszabb TMemo-t használni: míg a Word változót és a KarakterTömböt cím szerint. A rutin mit fog csinálni ? Átmásolja a String-bôl a KarakterTömbbe a szöveget, és a Word-be pedig a String hosszát másolja, lássuk:
procedure CopyStringToTMemo(AText: String; var ArraysLength: Word; var CharArray);
function Min(A, B: Word): Word;
begin
if A < B then Min:=A
else Min:=B;
end;
var MinLen: Word;
begin {IN: ArraysLength = SizeOf(CharArray), OUT = Length(AText) !!!}
MinLen:=Min(Word(Ord(AText[0])), ArraysLength);
ArraysLength:=MinLen;
Move(Mem[Seg(AText):Ofs(AText)+1], CharArray, MinLen);
end;
A példa procedure forráskódja (ConvStr.PAS) !
Ez szerintem elvileg érthetô: de azért elmondom:
Egy aprócska dolog a TAdatok rekord felépítésével kapcsolatban:
Ha valakit zavar, hogy a TMemo két típus definiálást igényel használjon egymásba ágyazott ciklusokat e probléma megoldására:
TEgyebMemo = record
EgyebHossz: Word;
Egyeb : Array[1..16] of Char;
end;
TNoteMemo = record
NoteHossz : Word;
Note : Array[1..80] of Char;
end;
TAdatok = record
VNev,
KNev,
Varos : String[16];
Irszam : String[4];
Utca : String[16];
HazSzam : String[3];
EgyebMemo : TEgyebMemo;
Tel : String[16];
NoteMemo : TNoteMemo;
end;
Így szerintem már mindenkinek kell, hogy tessék (tetszôdjék), hogyan is hivatkozunk a TProbaDialog-on belûl e adatokra, pl. a NoteMemo Note karaktertömbjére ?
(TProbaDialog.)Adatok.NoteMemo.Note:=‘akármiislehetideemekétaposztrofközéírvaakutyamegamacska!’;
Nos így hivatkozhatunk rá, lehet, hogy így elegánsabb ? Nekem mindegy így is jó meg úgy is !
Hasznosítsuk az initializáláskor a fenti procedure-át !
with Adatok do
begin
VNev:='Bérczi';
KNev:='László';
Varos:='PÉCS';
Irszam:='7632';
Utca:='NemMondom meg tér';
HazSzam:='99';
EgyebHossz:=SizeOf(Egyeb);
CopyStringToTMemo(‘R4s Technology’, EgyebHossz, Egyeb);
Tel:='06-72-999-999';
NoteHossz:=SizeOf(Note);
CopyStringToTMemo(‘Nincs más mondandóm’, NoteHossz, Note);
end;
SetData(Adatok);
Látható, hogy a sok nulla char beírásától megszabadultunk, s azt is, hogy a CopyStringToTMemo() elôtt az elmondottak szerint meg kell azni karakterömb fizikai méretét.
Elegáncsosabbá lehet tenni a dolgot az elôbb elmondott rekord-ba zárással, s a rekord által definiált változót cím szerint átadni, s úgy kezelni a hosszt, s karaktertömböt. Ezt már rátok bízom legyen ennyi kreativitásotok !
Az adatok kiolvasása - lementése a kontrollokból
Ez hasonlóan egyszerû, mint a SetData() függvény használata, ugyanazt a rekordot használhatjuk az adatok elmentésére, mint a bevitelhez hiszen ugyanúgy Z-order szerint menti le ahogy beolvassa az adatokat.
Tegyük fel, hogy a kedves felhasználó, bepötyög valami a mezôkbe lenyomja az elment gombot, s nekünk az Adatok rekordba kell másolnunk, hogy tehessünk vele valamit a késôbbiekben, valahogy így:
A TProbaDialog objektumban implentáljuk az adat lekérô utasítást:
GetData(Adatok);
Az ablakon látható gombokhoz tartozó események lekezelése
Láttuk a TProbaDialog init-jében, hogy mint ahogy az lenni szokott a gombokhoz most is eseményt társítottunk, mégpedig rendre:
Ezekhez a gomb-feliratokhoz ezeket a cmXXXX esemény-konstansokat társítottuk még az applikáció elején.
A gomb legyomásakor e események létrejönnek, s le kell ôket kezelni !
De ki és hol kezelje le ? Nyilván - hát nem is biztos, hogy nyilván mondjuk inkább, hogy most én szerintem inkább az - a TProbaDialog HandleEvent()-jében fogom lekezelni, már csak azért is mert: 1 teljesüljön az OOP elsô alaptétele (AT) az adat és kód egységbe zárásáról, 2 ill. hogyha az applikáció kezelne le 23 különbözô ablak különbözô pl. 23*5 eseményét szerintem kicsit belezavarodnánk. Nézzük meg mit is tehetünk általánosságban a HandleEvent() meghívásakor ? (Ugye nem felejtettük el, hogy ez a HandleEvent() már létezik, hisz maga a TProbaDialog jelzett vissza az applikációnak, hogy nem kell ProbaAblakot létrehozni, hisz ha visszajelzett akkor létezik, hanem csak elôtérbe helyezni ! LÁSD elôzô szám !) Tessék:
procedure TProbaDialog.HandleEvent(var Event: TEvent);
begin
Inherited HandleEvent(Event);
if (Event.What = evBroadcast) and (Event.Command = cmProbaDialogLetezel)
then ClearEvent(Event);
if (Event.What = evCommand) then
begin
case Event.Command of
cmTHatra: ; {az elôzô adat beolvasása}
cmTElore: ; {begin Adatok:= ???valami; SetData(Adatok); end;}
cmSaveUj: GetData(Adatok); {Az kontroll-adatok bekérése késôbbre!}
cmUjAdat: begin FillChar(Adatok, SizeOf(Adatok), 0); SetData(Adatok); end;
end;
ClearEvent(Event);
end;
end;
Ez csak egy formális bemutatása az események többágú-szelekcióval való szétválasztására, de konkrét megoldás a továbbiakban, hisz még nincs ami e leolvasott adatokkal valamit kezdene !
Mit is lehetne e adatokkal (Név, cím, Tel.) kezdeni ???
Írjunk egy abszolút kezdetleges nyilvántartó programot !
Az Adatok nyilvántartása, s elmentése file-ba
Dolgunk nem nehéz, hisz meg van a felhasználói környezet - az adat bekérô kontrollok (inputline), a gombok a dialóguson melyek biztosíthatják a nevek - címek elôre | hátra lapozását. Most már csak annyit kell megoldani, hogy egy file-ba legyenek eltárolva a nevek és címek rekordonként. Nézzük a megvalósítást lépésenként:
>
constructor TProbaDialog.Init;
var R : TRect;
begin
Inherited Init(R, ‘Adat nyilvántartó dialógus’);
{a kontrollok - gombok definiálása}
MelyikRekord:=0;
FileOpen;
ReadRecord(MelyikRekord);
SetData(Adatok);
end;
Mi az a: MelyikRekord:=0; ? Nyilván a 0. rekordot az-az az elsô adatot írjuk ki a dialógusba megnyitáskor.
A FileOpen; megnyitja a file-t, a ReadRecord(MelyikRekord); sor pedig jelen esetben a 0. rekordot olvassa be az Adatok változóba - lásd alább - és a SetData(Adatok); a dialógusba másolja ezen rekord adatait - mezôit.
destructor TProbaDialog.Done;
begin
Inherited Done;
FileClose;
end;
Ez a metódus az objektum megszüntetésekor hívódik meg, s ha felszabadítottunk minden az objektummal kapcsolatos memória-területet akkor végül zárjuk le a file-t:
procedure TProbaDialog.FileClose;
begin
System.Close(AdatFile);
end;
Mely így történik. A System szó azért van ott, mert a Close egy view-ban saját magának s nem a file-nak a bezárását jelenti, ezért meg kell adni, hogy a System Unit Close() rutinját akarjuk használni.
A file-megnyitását is nézzük meg:
procedure TProbaobaDialog.FileOpen;
begin
MelyikRekord:=0;
Assign(AdatFile, 'adatok.dat');
Reset(AdatFile);
end;
A MelyikRekord változót lenullázzuk, az AdatFile file-típushoz hozzárendeljük az ‘adatok.dat’ file-t, s megnyitjuk. (A CD-n a forráskódban nem ugyanezt találod, mert nekem még arra is figyelnem kellet hogyha CD-rôl indítják el a programot, akkor ne lehessen írni, s read-only-val kell megnyitni !)
Lássuk az olvasási mûveletet:
procedure TProbaobaDialog.ReadRecord(var x: Word);
begin
if Word(FileSize(AdatFile)-1) >= x then
begin
Seek(AdatFile, x);
Read(AdatFile, Adatok);
end else Dec(x);
end;
Egyáltalán akkor olvasunk, ha a beolvasandó rekord mutatója nem mutat az utolsó rekord mögé, tehát ha még létezik. A FileSize() típusos file-nál a rekordok számát adja vissza 1-tôl számolva így ki kell belôle egyet vonni, mert az x 0-tól van számolva a Seek() file-kezelô parancs miatt. Ha jó az x értéke akkor olvasunk, ha nem akkor csökkentjük az x-et, hogy ne engedjük a maximális rekordok száma után menni.
Ha már olvastunk nézzük meg az írást is:
procedure TProbaobaDialog.SaveRecordToLastPos;
begin
Seek(AdatFile, FileSize(AdatFile));
Write(AdatFile, Adatok);
end;
Láthatjuk, hogy ez se ördöngôs, a file legvégére pozícionálunk, s oda írjuk az Adatok rekordot. A fogas kérdés az, hogy hogyan kerül az Adatok rekordba a dialógus adata. Ezt a HandleEvent() intézi el a GetData()-paranccsal:
procedure TProbaobaDialog.HandleEvent(var Event: TEvent);
var B: Boolean;
begin
B:=True;
Inherited HandleEvent(Event);
if (Event.What = evBroadcast) and (Event.Command = cmProbaDialogLetezel)
then ClearEvent(Event);
if (Event.What = evCommand) then
begin
case Event.Command of
cmClose : Close;
cmTHatra:
begin
if MelyikRekord <> 0 then Dec(MelyikRekord);
ReadRecord(MelyikRekord);
SetData(Adatok);
end;
cmTElore:
begin
Inc(MelyikRekord);
ReadRecord(MelyikRekord);
SetData(Adatok);
end;
cmSaveUj: begin GetData(Adatok); SaveRecordToLastPos; end;
cmUjAdat:
begin
ClearEvent(Event);
Event.What:=evKeyDown;
Event.KeyCode:=kbAltZ;
PutEvent(Event);
FillChar(Adatok, SizeOf(Adatok), 0);
SetData(Adatok);
B:=False;
end;
end;
if B then ClearEvent(Event);
end;
end;
Lássuk az elejétôl:
Ezzel vége lenne az adatok file-ba elmentésének, jöjön az utolsó téma:
Az adatnyilvántartó program forráskódja (ADATNYIL.PAS) !
Az adatnyilvántartó program EXE-je !
Validating data entry - A begépelt adatok ellenôrzése
Cikkünk utolsó témájához érkeztünk, a begépelt adatok helyességét ellenôrizzük le. De mit is jelent ez ? Leellenôrizhetjük, hogy jó -e az irányító szám meg van -e XXXX az-az négy számjegyû, vagy van e a házszám helyén adat, le lehetne ellenôrizni azt is ha itt pl. dátumot kérnénk be, hogy helyes e a dátum, meg még sok mindent ...
Általánosságban a Turbo Vision egy negyon kényelmes eszközt ad kezünkbe az adatok leellenôrzését illetôen a Validate Unit formájában. Le ellenôrizni a TInputLine-t és örökített utódait lehet legegyszerûbben.
Két apró lépésbôl áll e ellenôrzés létrehozása:
A Validator objektumok InputLine-hoz rendelése
Minden InputLine rendelkezik egy Validator mezôvel. Ha ilyen ellôrzô objektumot - validator-t - akarunk hasznáni akkor ezzek kell egyenlôvé tenni. Ezt a SetValidator() metódussal tehetjük meg mellyel úgyszintén minden TInputLine rendelkezik.
Milyen validator-ok léteznek:
Hogyan hozzuk ezeket létre:
RangeValidator:=New(PRangeValidator, Init(1000, 9999));
Ugye nem felejtettük el a uses-ban a Validate Unit-ot beírni ?
Jelen esetben a RangeValidator, mint nevébôl is következik két határ közötti értékeket enged beírni, egyébként kiírja egy msgbox-ban, hogy: "Value is not in the range: 1000-9999".
A másik leggyakrabban használt Validator a TPXPictureValidator, melynek elôre egy maszkot adunk meg, hogy milyen karaktereket fogadjon el egymás után. A speciális ekkor használandó karakterek:
Speciális:
# Csak számot fogad el
? Csak betût fogad el, kis- és nagy-betûk megkülönböztetésével
& Csak NAGY betût fogad el
@ Bármilyen karaktert elfogad
! Bármilyen karaktert elfogad, ha betû akkor csak NAGY betût
Ha egyezik:
; A következô betû szerint
* Az ismétlés szorzója
[] Választási lehetôség - nem kötelezô beírni
{} Csoport operátor
, Alternatívák elválasztása
Minden más: betû szerint
Nézzünk egy példát a fentiekkel:
Kérjük be a telefonszámot a vidéki hívókoddal, a körzetszámmal, s a kapcsolási számmal, pl.: 06-72-123-456, erre a picturevalidator: Tel^.SetValidator(New(PPXPictureValidator, Init('##-##-###-###[#]', True))); Láthatjuk, hogy az utolsó [ ]-be van téve, mert nem kötelezô, ez azért van mert BP-n 7 jegyûek a számok míg a vidékiek 6 jegyûek.
Az elôzô példában már láthattuk a konkrét beállítását is a validator-nak de részletesebben:
Megbeszéptük, hogy a TInputLine-nak van egy Validate: PValidator mezôje melyet be kell állítanunk úgy, hogy létrehozzuk a validator-t és egyenlôvé tesszük ezzel a mezôvel. A legegyszerûbb létrehozás látható:
Tel^.SetValidator(New(PPXPictureValidator, Init('##-##-###-###[#]', True)));
Az inputline-nak a SetValidator() metódusa egy PValidator pointer-t vár. A New() függvénnyel újonan létrehozott objektum címét a New() simán a függvény értékeként adja vissza. Az Init-ben a maszkot adjuk meg a fentiek szerint, s az AutoFill Boolean változót, melyet most True-ra állítottunk. Nézzünk egy teljes példát a rangevalidator-ra:
R.Assign(50, 4, 59, 5);
New(Iranytszam, Init(R, 4));
if CommandEnabled(cmUjAdat)
then Iranytszam^.SetValidator(New(PRangeValidator, Init(1000, 9999)));
Insert(Iranytszam);
R.Assign(33, 4, 47, 5);
Insert(New(PLabel, Init(R, '~I~rányítószám:', Iranytszam)));
Látjuk, hogy a dialog-ra insert-áljuk az inputline-t s a hozzá tartozó labelt, míg a validator-t símán a SetValidator() metódus paramétereként hoztuk létre.
A Valid metódusok meghívása
Két fô kérdést lehet feltenni:
Az elsô kérdésre a válasz egyszerû: valid vagyis érvényes az inputline-ban a szöveg ha megfelel a beállított validator elvárásainak, pl.: a fenti range (határ) validator elvárása, hogy 1000-9999 között legyen a beírt érték.
A második kérdésre a válasz több úton folytatódik: Az inpútline-t leellenôrizhetem:
Iranytszam^.Options:=Iranytszam^.Options or OfValidate;
Ugyanígy kell beállítani más esetekben is az OfValidate Options flag ezen bit-jét.
if Valid(cmClose) then SaveRecordsToLastPos;
A Valid() metódus elé nyilván nem írjuk ki a TProbaDialog objektum nevet hisz saját magán belûl használjuk, a SaveRacordsToLastPos;-ról pedig fent beszéltünk.
Szerintem e cikk bôségesen (sôt túl) sok információt tartalmazott egyszere a Kedves Olvasónak. DE szerintem egy hasznos programmal szolgáltam, amit a kezdôk, -e témában érdeklôdôk remélem nagy örömmel fogadnak. A validate-ról nem mondtam el mindent mert ebben a részben ennyire van szükség, ill. ami kevéske hátramaradt az pedig az objektumok belsô szerkezetének - mûködésének leírásakor tárgyaljuk. Akinek kérdése van az az alsó címre eMail-ezzen, s válaszolok.
A cikk Winword DOC formátumban
Az adatnyilvántartó program EXE-je !
Bérczi László
eMail:PC-XUser@IDG.HU, Subject: "OOP rovat"
BELA@MI.STUD.PMMFK.JPTE.HU