Programozás | C / C++ » Visual C++ alapok

Alapadatok

Év, oldalszám:2002, 103 oldal

Nyelv:magyar

Letöltések száma:5050

Feltöltve:2004. június 06.

Méret:582 KB

Intézmény:
-

Megjegyzés:

Csatolmány:-

Letöltés PDF-ben:Kérlek jelentkezz be!



Értékelések

11110 istoth 2012. február 16.
  Köszi nagyon jó.
11111 ragacs21 2011. április 06.
  Köszi OK
11111 villmer 2010. május 17.
  Kiváló anyagok letöltésére ad lehetőséget a Doksi. Sokat tanulhatunk belőle.

Tartalmi kivonat

C++ ALAPOK Bevezetőmet nem kívánom túlságosan elnyújtani, így összegzem: mindenképen szükségünk van az objektum-orientált programozásra – s annak C++ ill. Java beli megismerésére -, mert fejlett versenyképes termékeket csak korszerű fejlesztő eszközökkel hozhatunk létre. Ugyanakkor ezek objektum-orientált nyelvek nagy segítséget nyújtanak egy rugalmasan, sokkal racionálisabban kezelhető, áttekinthetőbb program elkészítéséhez. Mindenek előtt a DOS-os Borland C++-szal ismerkedünk meg - csak a legszükségesebbeket – annak ellenére, hogy a DOS már abszolút elavult platform. Tekintettel vagyunk mindenkire és pár cikk lesz a Borland C++-ról, majd jöhet a Java (cikkeink java, a Java –nyelv-). Előkészületek a C++ nyelv objektum-orientáltságának bevezetésére Avagy ami az abC rovatból eddig kimaradt:     Default argumentumok – változó hosszú paraméterlista Overloading Generikus függvények S ezek után tejles

gőzzel az objekumok P Default argumentumok Mikor, mire és, hogyan használjuk ?     Jellegét tekintve inicializált függvényparamétereknek tekinthetőek Ekkor a függvény formális paraméter-éhez/eihez default / alapértelmezés szerinti értékeket rendelünk A függvény meghívásakor jobbról – balra elhagyhatók az aktuális paraméterek közül azok a paraméterek melyekhez rendeltünk alapértelmezési értéket Így az elhagyott paraméter értéke a default / alapértelmezés szerinti érték helyettesítődik be Példa: void open window(int ax=1, int ay=1, int bx=80, int by=25) Az így deklarált függvényt az alábbiak szerint hívhatjuk meg: 1. open window(2, 2, 6, 6); //Így a (2,2)-(6,6) pontokon elhelyezkedő ablakot nyitunk 2. open window(2, 2, 6); //Így a (2,2)-(6,25) pontok lesznek a sarkok, hisz az utolsó koordiátát elhagytuk 3. open window(2, 2); //A (2,2)-(80,25) koordinátájú pontokon lesz az ablak sarka, hisz az utolsó két

paramétert elhagyva a képernyő bal alsó sarkába lesz illesztett az ablak. 4. open window(2); //Az előzőek alapján nyílván (2,1)-(80,25) a méretezés 5. open window(); //S végül egy teljes képernyőnyi ablak: (1,1)-(80,25) 6. open window(, 2, 6, 6); //FIGYELEM: ez az ELRETTENTŐ példa hisz ROSSZ CSAK balról – jobbra hagyhatók el a paraméterek ! 1 Végül pár adalék a default argumentumú függvények felépítéséhez:   Adott default argumentumot csak akkor tudunk felülírni, ha az összes előtte lévő paramétert már megadtuk. Így vagy az összes paramétert vagy annak csak egy részét inicializáljuk. Ezek alapján a paraméterlistát úgy kell összeállítani:    Hogy nem inicializált paraméterekkel kezdődjön, s Praktikus a leggyakrabban felülírandó paramétert a default paraméterek elejére tenni. (Közvetlenül a nem inicializált paraméterek után.) Ezek által egyértelmű paraméterlistát kell létrehozni (hogy a

fordító eldönthesse mely paraméter hova kerül.) Példa:   void open window(int ax, int ay, int bx=80, int by=25) EZ JÓ ! void open window(int ax=1, int ay, int bx, int by=25) EZ ROSSZ ! Változó hosszúságú paraméterlista Sokszor előfordul, hogy nem tudjuk a függvényünk paramétereinek pontos számát. Vegyünk egy példát. (az egyszerűség kedvéért a főiskolán bevált példát taglalom) Nos: Van egy függvényünk mely a paraméterként átadott számokat adja össze. De mennyi paraméterünk lesz ? 3 vagy 10 vagy 100 ? Ez teljesen lényegtelen a továbbiakban. Lássuk a C++ által biztosított és igényelt feltételeket:    Ha nem tudjuk vagy akarjuk megadni a függvény paramétereinek pontos számát vagy típusát (lásd még generikus függvények) akkor a típus ellenőrzés felfüggeszthető a direktívával. Ezen direktíva jelzi a fordítónak, hogy ezen helyre előre nem deklarált mennyiségű és típusú paraméter kerül ill.

kerülhet A fordító feltétele, hogy a előtt legalább egy fix formális paraméternek kell lennie, melynek megadtuk nevét, s típusát. Gondoljuk végig ! Vajon olyan ördöngős dolog ez ? A csudákat, hisz a paramétereket simán lenyomja a fordító a stack-be, onnan meg bármelyiket ki lehet a függvényen belül szedni, csak a típusát, az indexét – hogy hányadik – kell ismerni. Az index megadása az egyszerűség kedvéért az utolsó ismert fix paraméter után kezdődik. Lássuk a példát:     Deklaráció: int plusz(int szam, ) Hívása: x = plusz(1, 2, 3, 4) x = plusz(0) x = plusz(3 ,2 ,1) A használata:  A függvények melyekkel a változó hosszúságú paraméterlistát kezelni tudjuk a STDARG.Hban vannak Ezek: 2      va list: Ez egy típus mellyel az adott indexű paraméterre mutathatunk, az-az argumentum pointer. Deklarálása: va list paremeter ponter neve; va start(va list ap, ua): Ez állítja be a fent

deklarált argumentum pointer (ap) értékét az első nem definiált paraméterre.Első paramétére az ap: a fent deklarált argumentum pointer Második paramétere az ua: a előtt utoljára deklarált fix paraméter neve. A fenti példában a szam az azonosító név. va arg(va list ap, típus): A 2. paraméterben megadott típusban adja vissza a következő paramétert. Vigyázat ha egy paraméternek rossz típust adunk meg, akkor hibás lesz az összes többi paraméter, hisz a stack-ből történő visszaolvasás elveszíti byte-sorrendjét.Pl: va arg(ap, int asd); va end(va list ap): a változó hosszúságú paraméterlistával rendelkező függvényből kilépés előtt ’rendrakás’ céljából kell meghívni. (BP-SP pontos visszaállítása) #include <stdio.h> #include <conio.h> #include <stdarg.h> int plusz(int, .); void main() { int x=5,y=3,osszeg; clrscr(); osszeg = plusz(3, 10, x, y); printf(" 10+%d+%d=%d", x, y, osszeg); getch(); }

//main int plusz(int db, .) { int i, szam, s=0; va list ap; //argumentum pointer va start(ap,db); //ap amit inicializálunk, ill. a db az utolsó ismert fix paraméter for (i=0;i<db;i++) { szam=va arg(ap, int); // a következő paramétert adja vissza int-ben s+=szam; } //for va end(ap); //SP, BP beállítás return s; } // plussz Overloading amire vártunk Nos ezzel belefogtunk mai legérdekesebb témáinkba. Mit jelent ? ? Többértelműség Lássunk egy frappáns definíciót: Egy típusokkal rendelkező nyelvben egy szerkezetet akkor mondunk többértelműnek, ha a szerkezet különböző, egymástól eltérő jelentéssekkel rendelkezik, és a megfelelő jelentés az argumentumainak típusinfomációja alapján van kiválasztva. Lássunk egy kézenfekvő példát: a + jel. Hogy miért ? Mert azonos hatáskörben többértelmű, mert jelenti egészek összeadását, valósok összeadását, ill. az automatikus konverziónak köszönhetően ezek keverékét.    Nos,

exakt megfogalmazásban: C++-ban lehetséges olyan eljárások definiálása: Melyek mind ugyanazzal a névvel rendelkeznek, S pontosan ugyanabban a hatáskörben vannak. 3 Azt, hogy melyik függvényt kell meghívni a sok közül azt a fordító dönti el a:   a szövegkörnyezet, ill. aszerint, hogy milyen típusú változókat tartalmaz az aktuális paraméterlista. Vezessünk be egy új fogalmat, a paraméter-szignatúra megnevezést, mely a későbbiekben fogunk használni: a függvény paramétereinek száma, típusa és, a paramétereinek sorrendje. A C++ fordító egyező függvénynév értelmezése: Ha egy függvénynév deklarációja (prototípus megadás) valamely programban egynél többször szerepel, akkor a fordító a második ill. további deklarációkat a következőképp értelmezi: 1. Eset: Ha a két függvény visszatérési típusa és a megfelelő paraméterek típusa teljesen megegyezik, akkor a második deklarációt az első

újradeklarálásának tekinti, és hibát generál. Pl.: void f(int x, char s); void f(int z, char t); A paraméterek neve jelentéktelen az összehasonlítást tekintve. 2. Eset: Ha a két paraméter lista megegyezik de a visszatérési típus nem, akkor hibás újradeklarálásról küld hibaüzenetet a fordító. (Type mismatch in redeclaration of f(int, char);) Pl.: void f(int x, char s); int f(int z, char t); 3. Eset: Ha a két paraméterlista akár paramétereinek számában akár azok típusában eltér akkor overloading-ról beszélünk. Az overloading fordító által kezelt esetei: o o o Egy paraméter esetén: A fordítóprogram összehasonlítja az aktuális paramétert a függvény minden egyes deklarált másának szignatúrájával. Egyezés: A fordító által a behelyettesítés során akkor történik egyezés, ha az aktuális paraméterlista teljesen megfelel egy adott paraméter-szignatúrának, vagy ez elérhető automatikus konverzióval. Kétértelmű

egyezés: Az aktuális paraméterlista egynél több paraméterszignatúrának felel meg. Pl.: void f(long, double=3.14); void f(long, int=0); void f(long); 4 long x; f(x); Itt default argumentumok miatt van kétértelmű egyezés, a fordító ezt az üzenetet írja ki: Ambiguity between f(long,int) and f(long) in function main(). o o o Nincs egyezés:Az aktuális paraméterlista egyik paraméter-szignatúrával sem egyeztethető össze még konverzió után sem. Több paraméter esetén: Minden paraméterhez a megfelelő egyeztető szabályt alkalmazza, és azt a függvényt válassza ki, amelyre a paraméterlista jobb eredményt ad, mint a többi. A hívás lehet többértelmű, ha egyik függvény sem ad a többinél jobb eredményt. Példa: int max(int, int); // (1) long max(long, long); // (2) double max(double, double); // (3) int x1=5,y1=3; long x2=3,y2=5; double x3=2.0,y3=70; float x4=4.0,y4=60; int *x6=&x1, y6=&y1; max(x1, y1); // (1) max(x2, y2); // (2)

max(x3, y3); // (3)-at hívja meg. max(x4, y4); // (3)-at hívja meg. Lásd a magyarázatot max(x1, x2); // x1: int; x2: long; -> Kétértelmű egyezés. Lásd lent. max(x6, y6); // nincs egyeztetés, lásd lent. o o o o Az első három függvény hívás egyszerű, hisz típusukban abszolút megegyeznek a paraméterek. A negyedik már cikisebb, mert nincs pont megfelelő. Így megpróbálkozik a konverzióval, ha sikeres és egyértelmű, akkor van megfelelő szignatúra, ha nem vagy többértelmű akkor hiba. Lássuk: lehet –e float ? double konverziót végezni Hát persze ! Így van megfelelő szignatúra. Az ötödiknél is az az eset áll fent, hogy nincs pont megfelelő szignatúra, de megpróbálunk konverzióval jót keresni. Az összes szignatúrát megpróbáljuk a mi 5 paraméterlistánkra konverzióval ráhúzni.Aktuális paraméterlista a típusokat tekintve: max(int, long); Ezt konverzióval az (1)-ból és (2)-ből is előállíthatjuk, hisz int ? long, ill.

long ? int konverzió azonos prioritással rendelkezik, így kétértelmű egyeztetés: Ambiguity between max(int, int) and max(long, long) in function main(). Utolsó hatodik esetben: nem talált a fordító konverzió után sem megfelelő szignatúrát, így: Could not find match for max(int*, int). Overloading és érvényességi kör:   Az adott névhez tartozó függvényhalmaz deklarálását ugyanazon hatáskörrel kell elvégezni. A lokálisnak deklarált függvény eltakarja és nem átdefiniálja a globális függvényváltozatot. Példa: extern void print(char*) extern void print(double); void main() { 5 extern void print(int); print(3); print("=X"); //? Could not find match for print(char *) } Amint láthatjuk a lokális print(int) eltakarja a modulra-globális print(char*)-t így nem tudjuk az utolsó sorban kiírni a string-et. A függvények királya: Generikus függvények Mint megszoktuk, a magas szintű programozási nyelvekben az eljárások

paramétereinek meg kell adni a típusát. Ha ugyanazt az eljárást különböző típusú paraméterekre akarjuk meghívni, de maga az eljárás algoritmusa ugyanaz akkor eddig el kellet készítenünk minden típusra ugyanazt az eljárást. Így minden eljárás ugyanaz volt, csak a paraméterek, ill. esetleg a belső változók típusában különbözik Pl. a matematikai műveletű eljárásoknál, vagy maximum / minimum meghatározásnál, rendezésnél lehet ilyen problémánk. Ezt a sok kellemetlenséget a C++-ban el lehet kerülni, s nem kell minden típusra az egyszer megírt rutint átirogatni. A paraméterek típusát is paraméterként kell átadni, így a rutin majd az átadott típussal fog dolgozni. A típussal – is – paraméterezett függvényeket generikus függvényeknek nevezzük. Az előzőek alapján nem overloading-gal fogjuk megoldani (az-az nem definiálunk x különböző típussal x különböző függvényt), hanem csak egy függvényt deklarálunk. Ezen

függvényt sablonnak nevezzük, mert a paraméterének nincs típusa, mert a típust is paraméterként adjuk át. Sablon definiálása:  Sablont csak globálisan lehet definiálni: template <argumentumlista> deklaráció  Az argumentumlista egy elemét a class kulcsszóval azonosítjuk: class azonosító Példa: template <class T> T max(T a, T b) { return( a > b ) ? a : b; } A példában egy paraméterként átadandó típust definiálunk melyet T azonosítóval látunk el, s ezt adjuk át paraméterként. Ezután ezt a T típust bárhol használhatjuk: bármely paraméter típusaként, vagy a függvény visszatérési típusaként, vagy a függvényben lokális változó deklarációjakor annak típusaként. De hogyan jön létre a tárgykód ? A fordító a meghívásnak, a függvény paraméterének típusának megfelelően generálja a megfelelő függvényt. Mi az összefüggés a generikus és overloading függvények között ? 6    Ha

van olyan – overloading – függvény mely szignatúrájának pontosan megfelel az aktuális paraméterlista akkor azt hívja meg, Ha nincs, akkor alkalmazza a sablont. De elképzelhető, hogy így sem lesz megfelelő függvény, gondoljunk az egyik overloading példára: max(int, long); //Hibaüzenet: nincs egezés a max(int, long) paraméterlistára. Could not find a match for max(int, long). Nézzünk egy utolsó példát melyben több paraméter van, így a paraméterek egymástól is különböző típusúak lehetnek. Egy egyszerű szorzás: template <class S, class T> T szoroz(S x, T y) { return y*x; } Hogy a félreértést elkerüljük, egy tényleg utolsó példa melyben felhasználjuk a paraméterként átadott típust lokális változó deklarációjára: template <class S> void csere(S *x, S y) { S temp; temp=*x; *x=y; *y=temp; } És végre: Objektum orientált programozás C++-ban Nos végre elérkeztünk az objektumok tárgyalásához C++-ban. Nem

kívánom az elméleti alapokat újra áttárgyalni, ugyanis az OOP rovat első számaiban ezt megtettem. Most csak a C++ specifikus témákat tárgyaljuk át. Ezen cikkeimhez fősikolai jegyzeteimet is felhasználom, melyhez köszönetet mondok Bálintné Farkas Judit tanárnőnek (JPTE-PMMK-Műinfó). Az objektum orientált programozás (OOP) a 90-es évek uralkodó stílusirányzata, s egyre inkább felváltja a - lassan már elavulttá váló, de ugyanakkor még klasszikusnak is számító – strukturált programozást. Az objektum-orientált programozás jobban megközelíti, utánozza a valóságot, és jobban igazodik a tárgyakhoz. Minden valóságos tárgyat nemcsak alakja, elhelyezkedése jellemez, (Tehát nem csak a rá jellemző adatok – méretek -.) hanem az is, hogyan viselkednek bizonyos körülmények között. Így a tárgyakat minden jellemzőivel együtt komplex egészként tekintjük (Az-az, az objektum az adatok és jellemzőjük komplexe, elválaszthatatlan

egésze.) Amikor egy objektumot deklarálunk, akkor írjuk le szerkezetét a mezőkkel, másrészt azokat a szubrutinokat, amelyek leírják az objektum viselkedését. Így az OOP-s nyelv:    Sokkal modulárisabb, absztraktabb, strukturáltabb, az általános strukturált nyelveknél. 7 Ahogy az OOP rovat elején is elmondtam, egy objektum orientált nyelvnek három fő jellemzője van:       Egységbezárás: az adat és kód egysége (encapsulation) Öröklés: a meglévő osztályokból újabbak származtathatóak (inheritance) Többrétűség – polimorfizmus (polymorphism) (ezt az OOP rovat elején két külön témakörbe soroltuk) Egységbezárás: Az adatokat és az adatokat kezelő függvényeket (vagy Pascal terminológia szerint metódusokat) egységként kezeljük méghozzá úgy, hogy a külvilág számára közömbös adat – kód részeket elzárjuk. Így megvalósul az adatrejtés elve is Az így kapott egységeket objektumnak

nevezzük. C++-ban az objektumoknak megfelelő tárolási egység típusa a class (osztály). (Megjegyezném, hogy a struct tárolási egység is elláthat hasonló funkciókat, mert lehetőség van a mezők mellett, metódusok elhelyezésére is. A class ill record típusok között csak az adatok – kód elérésében van különbség.) Öröklés: Meglévő osztályokból új osztályok származtathatóak. A származtatott új osztály örökli ősei (a származtatott ős, ill. annak ősei) összes adat ill metóduskészletét. (Ez persze egyáltalán nem azt jelenti, hogy el is érheti) Az örökölt mezőket az új osztály át is definiálhatja, vagy teljesen újakat is definiálhat. Ezáltal alakul ki az objektumok hierarchiája. Polimorfizmus: Egy metódus azonosítója többször is előfordulhat az osztályhierarchián belül ilyenkor, ha már létezik valamelyik ősben, akkor az örökölt metódust pontatlanul mondva felülbíráljuk, az-az az azonos nevű metódus

specifikus lesz ebben, az új objektumban s annak utódaiban is. Virtuális metódusok alkalmazásával lehetőségünk nyílik arra, hogy egy metódus konkrét végrehajtási módja csak futás közben dőljön el. (Az-az a virtuálisnak deklarált metódus az adott objektum minden metódusában elsődleges lesz annak ellenére, hogy nem írtuk felül minden az előd metódust használó helyen. Ez a futás időben történő metódus kiválasztás) Lássuk a C++-ban az objektum definiálását: Amint fent említettem a struct tárolási osztály is elláthat hasonló funkciót, a különbség a összetevők (mezők és metódusok) alapértelmezés szerinti elérésében van:Az alábbi elérési szintek vannak:      public – publikus – elérhető ezen objektumból s őseiből is private – privát – kívülről soha nem érhető el, s az utód objektumból se protected – védett – hasonló a priváthoz, de az utód objektumból elérhető

struct:alapértelmezés: minden mező és metódus publikus class:alapértelmezés: minden mező és metódus privát Ezek után nekünk kell az objektum definiálásakor az adott mezők és metódusok elérési szintjét. Az OOP elmélete szerint minden – adat – mezőnek privátnak kell lennie, hogy megvalósuljon az adatrejtés elve, továbbá kell lenni publikus metódusoknak melyek az előző adat mezőkön műveletet végeznek. Az objektum belső metódusai melyeket csak az objektum használ privátnak kell lenniük, hisz nem kívánatos a kívülről történő meghívás. Következő számunkban az objektumok öröklésével, konstruktorokkal és egyéb szépségekkel foglalkozunk. Ha valakinek bárminemű kérdése lenne az elhangzottakkal kapcsolatban bátran írjon az alábbi eMail címre: OBJEKTUMOK Öröklés – Konstruktorok C++-ban Öröklés 8 A Pascal-ban megismertek szerint a régi osztályokból újak származtathatók. (Sőt C++-ban egy osztálynak két

szülője is lehet.) A származtatott új (utód) osztály örökli az őse minden létező adat és metódusmezőit. (angol terminológia szerint data-, és method- member; adat-, és metódus- tagok) Az új osztály az örökölt metódusokat felül is bírálhatja a polimorfizmus eredménye képen. Nyílván az új osztály elődeihez képest ill. hasonlóan új mezőket is definiálhat C++-ban az objektum orientáltság által kialakíthatunk egy egyértelmű objektum hierarchiát, de nem vagyunk erre rászorítva. Ez csak a jó belátásunkon múlik Az objektumok minden képen valamilyen hierarchikus felépítést fognak követni de lehet, hogy több egymástól független hierarchia is létezhet, ami nyílván ellentmond az OOP elméletének. Vegyünk egy példát: mivel nem vagyunk rászorítva, hogy egy minden objektumnak egy végső, a hierarchia tetején álló őse legyen (mint a Java-ban) így azt a hatást érjük el, amit már említettem: C++-ban: fastruktúrák erdejét

kaphatjuk, Java-ban pedig egy, jólszervezett fastruktúrát. Egy objektum struktúra felépítése annyiból áll, hogy egy rendszertan (~logika) alapján építünk a valóságot megközelítő objektumokat, alias a valóság modelljét. A kezdet az egy közös őstől (~a hierarchia gyökere) illik, hogy származzék. Ezen alacsony szinten alig van meghatározott tulajdonság (Annyi fix, hogy ő objektum, és létre lehet hozni ill. felszabadítani Java-ban még ennél is kevesebb) Nos egy szinttel lejjebb, már kezd specializálódni a meghatározás, ugyanis kezd valami konkrét feladat rész megoldása lenni, és ugye ehhez új ismeretek szükségesek. Ezen a szinten szokás az absztrakt objektumokat elkészíteni, melyek nem működő objektumok csak a kinézetet, a tulajdonságokat definiálják. Ezeket Pascal-ben absztrakt objektumoknak nevezzük, C++-ban elvileg protokollnak, és Java-ban interface-nek. Természetesen a Java-ban valósul meg az eredeti koncepció. A

következő szint – az OOP elméletet követve – akár már konkrét feladatát ellátó objektum is lehet. Ez már számos speciális tulajdonsággal, s persze a megvalósítással is rendelkezik. Nézzünk egy biológiai példát: És lehetne a végtelenségig folytatni . Hogy miért nem neveztem meg az élőlények ősét ? Részint nem akarok teológiai vagy filozófiai vitába szállni senkivel de az biztos, hogy mint minden objektumnak az élőlényeknek is van valami (valaki) őse akitől ill. ahonnan származtatni lehet, hisz: világegyetemünkben minden ok, okozati összefüggésben van. Ki van zárva, hogy valami csak úgy magától legyen. (Ennyit már a bölcsesség szeretetéből, ugyanis a filozófia, mint szó ennyit jelent) Ha egy tulajdonságot definiáltunk az objektum hierarchia egy tagjában akkor az, az összes alatta lévőben jelen lesz. A tulajdonság, a jellemző, mint olyan nem tüntethető el, de értelmezése a tulajdonságot megtestesítő definíciót

megváltoztathatjuk. Pl az élőlényeknek van kültakarójuk A 9 majomnak hámszövete és szőre van. (Mivel állítólag az ember valahogy a majomtól származtatható Legalább is a tudomány mai állása szerint:) Ha az ember a majomtól származik akkor neki is szőre van. ? Hát igen elég cikis lenne, ha nem változtathatnánk meg ezt a tulajdonságot, hisz hibás lenne az OOP elmélete. Tehát ember ill leszármazottai esetén a kültakaró legyen csak hámszövet (az szőrt a majomhoz képest elhanyagolhatjuk) A tulajdonság nem tűnt el, csak módosult. (Elég furcsán néznénk ki kültakaró nélkül J ) Nézzük ezt az OOP nyelvén: Ha D osztály leszármazottja B-nek, B | D Akkor D-t (le)származtatott osztálynak (angol terminológi szerint derived class) nevezzük. Ugyanakkor B-t ősnek (B-t a D, ill. annak utódai bizonyos fokú ősének), alaposztálynak (angol terminológia szerint base class) nevezzük. Ha csak eggyel vissza ill. előre tekintünk az objektum

hierarchiában akkor az első rendű utódot közvetlen leszármazottnak (ang. term: immediate descendants) nevezzűk, míg ez a közvetlen ősének (immediate ancestor) a hierarchia pont eggyel feljebbi tagját nevezi. Amint már említettem C++-ban (és szinte sehol más nyelvben) a származtatott osztály nem csak egy őstől örökölhet. Amikor a származtatott típus több őstípusból kerül levezetésre akkor többszörös öröklésről (ang. term: multiple inheritance) beszélünk Objektum orientált programtervezés (hasonlóan a moduláris – strukturált tervezéshez) a hierarchia megfelelő kialakításából ill. a hierarchia egyes osztályainak tulajdonságainak definiálásából áll A tervezés első lépése, hogy tudjuk mit is akarunk csinálni. Ekkor kiderül milyen osztályok is kellenek Ezeket a megfelelő szempontok alapján kell hierarchiába tömöríteni. A hierarchiát úgy kell felépíteni, hogy az egymástól csak kicsit eltérő tagokat ne

külön-külön definiáljuk, hanem ezeknek egy közös őst hozzunk létre. A kis különbségeket származtatás után korrigáljuk Így áttekinthetőbb, megbízhatóbb kódunk van. (És az a közös ős is, jól jöhet még később) Példa a hierarchia felépítésére A példa ötlete nem az enyém, már ezer helyen láttam, én most főiskolai jegyzetemből veszem. [1]      Grafikus rendszert tervezünk A grafikus elemeket (grafikus objektumokat) ki kell tudnunk rajzolni, Mozgatni kell, Törölni, Nagyítani, kicsinyíteni. Lássuk a hierarchiát: PONT | KÖR | NÉGYZET A hierarchia megtervezése után az egyes osztályok tulajdonságait kell megadnunk: PONT: KÖR: NÉGYZET: Új tagok: Örökölt tagok: Örökölt tagok:  x, y  x, y 10  x, y, r    rajz törlés mozgatás    rajz törlés mozgatás Új tagok:   r nagyítás  kicsinyítés      rajz törlés mozgatás nagyítás

kicsinyítés Új tag:  a A fent említett módon lehet egy absztrakt osztályt definiálni, pl. csak a koordinátáknak (Ez persze nem klasszikus értelemben vett absztrakt definíció. Csak egy koordináta objektum Mint Pascal-ban a TPoint objektum (mert nem csak TPoint rekord, hanem objektum is van.) Ezt az alábbi módon definiáljuk: KOORDINÁTA | PONT | . (a többi ugyanaz) Bevezethetünk a PONT osztályba egy olyan boolean mezőt mely megadja, vajon látszik –e a pont a képernyőn. Lássuk az objektum definiálását, de a PONT osztályt vesszük közös ősül: enum BOOLEAN {FALSE, TRUE}; struct pont { private: int x, y; BOOLEAN lathato; }; Nos íme egy osztály konkrét deklarációja. Ahhoz, hogy ilyen típusú objektumot hozzunk létre az alábbi parancs szükségeltetik: struct pont p, *ptr; //C-ben a megadás pont p, *ptr; //C++-ban a megadás Ezek után csak a C++-t vesszük alapul. Láthattuk a private kulcsszót amit már előző számunkban kiveséztünk. Ez

annyit tesz, hogy megváltoztattuk az objektum (struktúra) minden tagjának (itt mezőjének) hozzáférési körés, s publikusról (ami az alap struct esetén) privátra, hogy teljesüljön az adatrejtés elve. Ahhoz, hogy az már deklarált adatokat el is érhessük metódusokat kell deklaráli. (metódus – függvénymező) Metódusnak nevezzük az osztály definíción belül deklarált függvényt (függvénymezőt). E definícióra két mód kínálkozik:   A definíció az osztálydefinícióban van implicite. Az osztálydefiníción belül csak a deklaráció van, s e törzsön kívül definiáljuk, az-az kívül valósítjuk meg. Az x koordinátát pl. a getx() metódussal kérdezzük le 11  Példa az implicit inline metódusra: struct pont { private: int x, y; BOOLEAN lathato; public: int getx() { return x;} };  A második megoldás: struct pont { private: int x, y; BOOLEAN lathato; public: int getx(); }; int pont::getx() { return x; } Láthatjuk

jelentősen eltér a két definíció mód. A másodiknál ha osztálydefiníción kívül kívánjuk definiálni az adott metódus törzsét akkor az alábbi formula szerint kell a metódust kifejtenünk: visszaadott érték típusa osztály típusnév::metódusnév(paraméter lista) { //parancsok }     A visszaadott érték típusát már az osztálydefiníciónál is megadtuk, ugyanannak kell lennie. Az osztály típus név, melynek metódusát kívánom definiálni A metódus név az adott metódus neve, s a paraméter lista pedig az adott metódus paraméter listája Aha: hogy mi az a dupla kettőspont ? :: ez a hatáskör definiáló operátor (ang. term: scope resolution operator.) Olyan, mint a struktúra pontja, vagy Pascal-ban a pont Ha az osztályok kívülről akarjuk elérni akkor viszont a pont (.) vagy ? a megfelelő operátor Pl.: objektumnév.metódusnév(aktuális paraméterek) objektumnév? metódusnév(aktuális paraméterek) Konstruktorok -

destruktorok Eme rejtelmes szavak elegye nem egyebet tesz, mint az objektumok problémamentes inicializálását, ugyanis általános probléma az objektumok könnyed létrehozása ill. kezdőértékekkel való feltöltése Inicializáljunk egy PONT objektumot. Ezt két féle képen tehetjük meg 12  Vagy ezzel a favágó módszerrel (ami a fenti példaprogramra nem is működik, hisz privátnak deklaráltuk az adat mezőket): egypont.x = 3; egypont.y = 4; egypont.lathato = FALSE;  Nos a már kicsit haladóbb programozó már használja az eszét, és egy rutinnal egyszerre az összes adatot inicializálja. (persze ez csak a sima strukturált programra igaz ahol még nincs objektum) void init(pont *p, int ax, int ay) { p? x=ax; p? y=ay; p? lathato=FALSE; } Ennek a rutinnak a meghívása: pont kp; init(&kp, 12, 40);     S végül a nyerő: a használatos, szabványos inicializálás objektumok esetén a konstruktornak nevezett metódussal történik. Ez

nem csak a változók kezdőértékét állítja be, hanem mást is: Az objektum felépítését (VMT) Memória allokációját Inicializációt A konstruktort vagy a felhasználó definiálja, vagy ennek hiányában a fordító generál egy alapértelmezés szerintit automatikusan.   Felettébb fontos, hogy a konstruktor neve mindig az osztály nevével egyezik meg. Ebből következően csak egy fajta konstruktora lehet egy objektumnak, de az overloading itt is megengedett. A megszokott módon az aktuális paraméterlista dönti el melyik paraméterszignatúrát alkalmazza a fordító. A konstruktornak nincs visszaadott értéke. (nem kell kiírni semmit, még a void-ot se) A konstruktort két féle képen definiálhatjuk:   Osztályon belül – inline Ill. osztályon kívüli definíció, amikor az osztályon belül csak a prototípust definiáljuk Lássuk: struct pont { int x, y; BOOLEAN lathato; Pont(int, int); }; pont::pont(int ax, int ay) { x=ax; y=ay;

lathato=FALSE; } Hívása: 13 Mivel paraméterek is vannak a konstruktorban (ill. csak olyan konstruktorunk van ami paraméterrel rendelkezik), ezért a deklaráció során az aktuális paramétereket is meg kell adni: pont p(3, 4); Hibás ebben az esetben ez a definíció: pont q; Hibaüzenet: Couldn’t find match for pont::pont() Ez persze kiküszöbölhető az előző számban említett default argumentumok használatával: pont::pont(int ax=0, int ay=0) { x=ax; y=ay; lathato=FALSE; } Így már használhatjuk ez alábbi definíciók közül bármelyiket: pont p(3, 4), q; Destruktor Mire jó ?     Az objektum felépítését határozza meg. Akkor hívódik meg, ha meg akarjuk szüntetni az objektumot. Hasonlóan definiáljuk a konstrukturhoz, ill. ha nem definiálunk akkor automatikusan generál egyet a C++. A destruktor neve előtt prefixumként a tilde jel: ~ áll, hogy megkülönböztesse a konstruktortól. Pl.: pont::~pont( argumentumok ) { // utasítások }

Nos ennyi eme metódusokról, lássunk egy olyan témát amiről már beszéltünk de érdemes összefoglalni: Mezőhozzáférés Szabályok: 1. Minden adatmezőt el kell rejteni, az-az privátnak vagy protected-nek kell definiálni 2. Létre kell hozni az adatmezők (csak annak amelyiknek szükséges) külső elérését biztosító metódusokat, melyeket lévén kívülről el akarunk érni publikusnak definiálunk. 3. Az osztály tulajdonságát alkotó metódusokat melyeket kívülről kell elérni azokat public-nak definiáljuk 4. Továbbá léteznek olyan metódusok is melyek csak az osztály belső működését segítik elő, ezeket specifikusságuktól függően: privát, ill. protecteed-nek kell definiálni Emlékeztetőül, hogy melyik kulcsszó mit is jelent:  Pritate – privát: Ezen mezőket – metódusokat (tagokat) csak az osztályon belüli metódusok érhetik el. 14  Protected - védett: Az így deklarált tagokat nem csak ezen osztályon, hanem

leszármazott osztályain belül is el lehet érni. (Ez egy szép újítás a Pascal-lal szemben)  Public – publikus: A tagok bárhonnan, korlátozás nélkül elérhetőek. Amit még, még egyszer elmondok, hogy a struc-tként definiált objektumok tagjai alapértelmezés szerint mind publikusak, míg a class-ként definiáltak pedig mind privátak. Így mind a két deklarációnál kell hozzáférést változtatni. A hozzáférés hatáskörét úgy kell felfogni, hogy azok a kulcsszavaktól, kulcsszavakig tartanak. Tehát a struct elején elképzelhetjük a public-ot, ami addig tart míg felül nem bíráljuk. A mezőhozzáférés és öröklés Lévén, hogy egyik osztályból újat származtathatunk érdemes elmondani az öröklés menetét: class D: hozzáférés-módosító B {.}; struct D: hozzáférés-módosító B {.}; ahol, D a származtatott osztály – az utód, és a B az ős. Hozzáférés-módosító: Lehet public vagy private. Nem szükséges megadni, de

érdemes a gubanc elkerülése végett, ugyanis a struct-ban publikus, míg a class-ban privát az alapértelmezés szerinti hozzáférés-módosító. (Fontos megjegyezni, hogy csak szigorítani, - s nem lazítani - lehet egy tag hozzáférését. Így:  Private hozzáférés-módosító: Az ős összes protected, és public mezője privát lesz.  Public hozzáférés-módosító: Az ős összes mezője változathal marad, a hozzáférés nem változik. Javasolt mezőhozzáférés:     Adatmező – protected (hisz az utódnak igen, de a külvilágnak nem szabad elérnie) Metódus – public (már amelyiket kívülről el kell érni) Metódus belső használatra – a specifikusság mértékében vagy protected esetleg privát. Származtatás public-ként. Nos ezzel hála’ Istennek ki is végeztük az OOP rovatot. Ezek után javaslom a Java rovatot, hiszen csak azért kontárkodtunk a C++-os OOP-be, hogy könnyebben menjen Java-ban. Persze ha bárkinek

kérdése van bátran írjon. R4s [1]: Köszönetet mondok főiskolai tanáromnak (JPTE-PMMFK-Műinfó), Bálintné Farkas Judit tanárnőnek a felhasznált irodalomért – jegyzetért – példákért, és segítségért. Köszönöm !!! 15 VISUAL C ALAPOK Akkor kezdjük egy kis elrettentéssel: A Visual C windows-os programozásra való, ezen tulajdonságánál fogva pedig erőssen támaszkodik az objektumorientált programozásra. Aki nem járatos az objektumokban C-ben annak ajánlom az obkektumorientált programozás C-ben (vagy valami ilyesmi) című rovatunkat. Nézzük, eszik-e, vagy isszák. A C++ gépfüggetlen volta, és windowsos programozásban való jó használhatósága miatt gyorsan elterjedt. A Visual C++ -t a Microsoft találta ki a windows-os programozás megkönnyítésére A Visual Basic után dobta ki. A célja ugyanaz volt vele, a programozót megkímélni az automatizálható felesleges rabszolgamunkától, kevert nyelven is programozható grafikus

fejlesztői felületet létrehozni, meggyorsítani a programfejlesztést. (És mellékesen, hogy minél több program íródjon a windows alá) A Visual C++a programfejlesztés során felmerülő egyes feladatok megoldásához különálló eszközcsoportokat ajánl fel, amik általában igen hasznosak. Szóval itt már nem arról van szó, hogy az ember leül, megírja a programot, aztán lefordítja. Hanem, az ember leül, a gép megírja a program vázát (nem vicc!) , aztán miután a programozó megírta a program lényegét, le lehet fordítani. A program váza alatt az alapvető header file-ok, Cpp file-ok, és az objektumrendszer alapja értendő. Ennyit az elméletről. Ugorjunk egyet a mélyvízbe. Kezdjünk el egy programot Egy projectet (mert itt már rég nem egy-egy programfájlról van szó) a file menu new parancsával tudunk kezdeni. Lehet választani külön file, project, workspace, vagy más dokumentum Különálló fileokat ebben a programnyelvben nem nagyon van

értelme írogatni, kezdjük a project-el Hát van egy pár fajta. Attól függően, milyen részeket istalláltunk, és milyen verziójú a Viusual -C a legfontosabbak:      Win32 Application application Csak Windows alatt futó, a teljes grafikus felületet az API funkciókkal kihasználó projectek hozhatók létre. Win32 Dynamic-Link Library Windows alatt futó programok által futás során meghívható DLL állományok létrehozására való projectek Win32 Static Library Saját könyvtárállomány (LIB file) létrehozására alkalmas állományok létrehozására alkalmas projectek. A projektlistán szereplő állományokból linkelt OBJ file-ok egy LIB-be gyűjtése MFC AppWizard (exe) A Microsoft Foundation Classes használatával egy project létrehozásának első lépéseként a program felépítéséhez szükséges minden állományt tartalmazó Visual Workbench kompatibilis állománykészletet hoz létre, és azokból egy azonnal futtatható projektet

készít. MFC AppWizard (dll) A Microsoft Foundation Classes használatával létrehoz egy projectet minden állományával együtt, DLL-re fordítható. 16  ATL COM AppWizard ATL projectet hoz létre objektumok nélkül, azokat nekünk kell betenni a Classwiev-ból a New ATL Object paranccsal. Az MFC-t is használhatjuk Custom AppWizard Egyénileg állítható minta szerint készít projectet. Választhatjuk alapul a MFC AppWizard-ot, egy már meglévő projectet, vagy magunk beállítgathatunk mindent Azán még sorolhatnám. Ezekkel profibbnál profibb dolgokat lehet összehozni, de válasszuk a legegyszerűbbet. Azaz, MFC AppWizard (exe). Ezzel a cuccal 4 lépésből lehet projectet csinálni Kezdjük Először is válasszuk ki a listáról, és írjuk be a megfelelő helyre a project nevét. Aztán jön a 4 lépés     Választhatunk, Single document, Multiple Document, és Dialog Based. A legegyszerűbb a Dialog Based. Legyen Ez Itt már több minden van.

About box Legyen-e about Context-sesitive Help Minden menüponthoz lehet Helpet rendelni 3D controls Valami ilyesmi, ha be van kapcsolva: Automation Lehetővé teszi más programok objaktumainak elérését ActiveX ActiveX vezérlés Windows Sockets A TCP/IP-hez kell Itt már minden érthető. Megkérdezi, akarunk-e commenteket, az előre elkészített file-okban. Akarjunk És még egy fontos dolog. shared DLL futásidőben rendeli hozzá a könyvtári függvényeket. (takarékosabb) staticaly linked library Fordításidőben teszi meg. (hosszú lesz az exe) Befejező lépésként megmutatja, milyen class-okat hozott létre, milyen néven, milyen file-ban. Miután megnyomtuk a finish-t, kapunk még egy utolsó áttekintést a beállításokról, majd az OK megnyomásával hozza létre az Application Wizard a projectet, mindenestül. Az alábbi file-ok mindig létrehozásra kerülnek: projetnév.clw Ezt az állományt a ClassWizard használja. Meglévő osztályok szerkesztéséhez,

újak beillesztéséhez. projectnév.rc Ez a project erőforrásállománya, az alapbeállítás menümeghatározásait, az alapbeállítás about mezőjét és tartalmazza az ikonállomány nevét.(ez általában res/projectnév.ico) resource.h A projecthez tartozó fejlécállomány projectnév.dsp Maga a project file projectnév.dsw Workspace file Az így létrehozott project azonnal lefordítható (build/build projectnév.exe), és elindítható (build/execute projectnév.exe) Azért így kapásból még ne indítsuk el, nézzünk kicsit szét a képernyőn. Nagyon fontos a workspace 17 window. Könnyen meg lehet találni, ha létrehozunk egy projectet, ez lesz az egyetlen ablak a képernyőn. Így néz ki: Négy féle beállítás leehtséges. ClassView (a képen), ekkor az objektumok szerkezetét, és egymáshoz való kapcsolatukat lehet nagyon jól áttekinteni. Bármelyik sorra rákattintva Megnyitja az állományt, amiben az objektum / member megtalálható

(általában minden objektum saját file-ban van) , és kapásból rápozícionál. Most még ne nagyon szerkezgessünk bele, nem biztos, hogy jól sül el. Mindenesetre mély megdöbbenéssel vegyük tudomásul , hogy ugyan még egy betüt nem programoztunk, mégis megvan a programunk, minden alapvető file-al és objektummal. ResourceView, ekkor az erőforrások szerkezetét nézhetjük meg. Ez a projectnévrc file Amint láthatjuk, Itt van eltárolva az összes ablak adata (hol van mekkora, holvan rajta gomb, stb.), az iconfile neve, karaktertábla, és a Version Info. Bármelyikre rákkattintunk, előjön egy megfelő szerkesztőprogram, és máris átrajzolhatjuk az icon-t/ átrakhatjuk a gombokat, és ami szem-szájnak ingere. FileView, itt a programunk file-jait különböző csoportokra osztva látjuk. Természetesen bármelyikre kattintva, rögtön szerkeszthetjük is. InfoView, itt mindenről információt kaphatunk, elég hatékony help. Ezen kívül még sokminden van a

képernyőn, de ezeket ráérünk később. A menü, meg a kis ikonok úgyis magától értetődőek. Na, miután ezzel megvagyunk, akár csinálhatnánk is valamit. Menjünk a ResourceView-ra, és szerkezgessük meg a Dialog-okat. Dupla kattintás a szerkeszteni óhajtott adatra, és máris itt van egy szerkesztő. 18 Itt aztán bárki kedvére eljátszhat. Minden objektum külön-külön kijelölhető, mozgatható, széthúzható, átírható, törölhető, beszúrható. Szóval minden ami kell Ha jobb gombot nyomunk egy objektumra, előjön egy menü, ahol nagyon sok fontos dolgot lehet csinálni. Másolni, kivágni, beilleszteni, kezelőfüggvényt rendelni hozzá. (Event handler) Egyenlőre ezekbe nem folyunk bele, ráérünk arra még. Ajánlom a properties menüt, ahol minden elképzelhetőt át lehet állítani A General-ban az ID-azonosítót adhatjuk meg, Látható lengyen-e, Megálljon -e rajta a tab, Lehet-e választani. A Legjobb a Captipon Ide kell beírni, mi legyen

a szöveg A Style és Extented style ablakokban az objektum stílusát adhatjuk meg. Van itt minden Lehet próbálkozni. Ha meguntuk a játszást, akkor Menjünk a BUILD/BUILD projrctnév.exe (F7) menüpontra, aztán meg a BUUILD/EXECUTE projectnév.exe (CTRL-F5) menüpontra A program működni fog Megnézhetjük mit csináltunk. MFC ALAPOK Miután van már működő programunk , igaz nem valami fenomenális, megnézhetnénk, hogy tulajdonképpen miért is működik. Mivel a programunk MFC-vel készült (Nem tudom miért éppen azzal, de ha már elkezdtem. ), tisztázni kéne, hogy mi is az Az MFC a Microsoft Foundation Classes rövidítése. Nem más, mint egy objektumrendszer, ami a windows-os programozást hívatott megkönnyíteni. Eltakarja előlünk a valódi API-t (Application Program Interface) és használja helyettünk. Ezen kívül a dokumentunkezelésekre, OLE-re, DAO-ra, és még sokmindenre vannak saját cuccai. Ennek fejében nekünk az ő szabályai szerint kell

játszanunk. Mit jelent ez? Meg vannak előre adva az objektumok típusai Pl: a dilalógusablak, a szerkesztőablak, gombok stb. Ezek tulajdonképpen megadják a programunk csontvázát. A mi dolgunk csak annyi, hogy hozzáírjuk azt a részt, amit csak mi tudunk, hogy minek kell lennie, ami már nem általánosítható, a program specifikus jegyei. Ennyi elég is lesz az elméletből, nézzük a gyakorlatot Csináljunk egy programot. 19 Ugyanúgy, mint az előző számban, MFC Appwizard, Dialog Based, stb stb. Azzal a kis különbséggel, hogy most ne kérjünk About Box-ot. Így egyszerűbben át fogjuk látni a dolgot. Na eddig jutottunk az előző számban is. A program ugyan már most is működik, de nekünk nem sok közünk van hozzá. Nézzük milyen forrás -és fejlécállományok vannak, anélkül, hogy belefolynánk a részletekbe: Ezeket alapvetően 2 részre oszthatjuk. Van, amibe nem nagyon kell belenyúlni: second.rc : ebben van eltárolva az összes ablak minden

adata, miden bitmap, az iconfile neve stb resource.h : itt van konkrét érték adva a különböző ID-knek stdafx.h : az MFC cuccokat include-olja be stdafx.cpp : MFC-t include-ol Amibe bele lehet, és ha programozunk, bele is kell nyúlni: Second.h Ebben van definiálva az application objektum típusa A CWinApp-ból van származtatva seconddlg.h Ebben van definiálva maga a kezdő dialógusablak A CDialog class-ból van származtatva. Fontos, hogy itt van megadva az IDD-je Ez annyit tesz, hogy a resources-nél ezen cím alatt lehet szerkeszteni az ablakot. enum { IDD = IDD SECOND DIALOG }; Tehát, a ResourceView-nél az IDD SECOND DIALOG-ra ráclickelve a kezdő dialógusablak szerkeszthető. 20 *. cpp File-okban a hozzájuk tartozó header-ekben definiált függvények megvalósítása található Na, azt hiszem sikerült elég felületes lenni ahoz, hogy még kérdések se tudjanak felmerülni. Nehogymár ne lássuk a fától az erdőt, akkor inkább már a fát se!

Csináljunk már valami újat is. Mondjuk egy új dialógusablakot Két dolgot kell csinálnunk 1. Először a Resource View-nál be kell insertelni egy új dialog-ot A jobb egérgombra előjön, hogy asszongya: Erre aztán nem is kérdez semmit, ott fog teremni egy újabb dialógusablak szerkesztőstül. Miután az ablakot mindenki kénye kedve szerint összeszerkesztette, felmerül a kérdés, hogy legyen más a neve. A ResourceView nál jobb gombbal rákatt, és properties. Itt más ID-t is beírhatunk Ez után jön a második lépés. 2. Az új ablakhoz létre kell hozni egy új Class-t is Főmenü Insert/New Class 21 Ami nagyon fontos, hogy a Dialog ID-hez az imént szerkesztett Dialog ID-jét írjuk be. A base class pedig legyen CDialog. A filename lehet bármi, a class type legyen MFC Megvan a class, saját kis header, meg cpp file-al. Megvan maga a dialog már csak azt kéne elérni, hogy a program futása során előjöjjön. Kicsit bele kell nyúlni a másik

dialógusablakba is. Nézzünk ki egy szimpatikus gombot rajta, és nyomjunk rá egy jobb clicket, és válasszuk az Events. menüpontot. (nálam ez egy kilép nevű gomb volt) 22 Szóval a bal oldalon azok közül az események közül lehet választani, amik bekövetkezhetnek. Ez egy gombnál nem túl bonyolult, lehet egy click, meg kettő. A jobboldalon felül a már létező handler-ek vannak. A jobb oldalon alul pedig azt kell kiválasztani, mivel akarjuk lekezelni Az Add Handler csak Hozzáad egy handlert, az Add and edit -nál pedig rögtön szerkeszthetjük is. Ezek az Event Handlerek nagyon fontosak, ugyanis ez az egyik ritka módja annak, hogy az általunk írt programrészekre tudjuk adni a vezérlést, az idő többi részében a WIN32s vagy az MFC végzi a dolgokat. Akkor Add and Edit. Rákérdez a létrehozandó függvény nevére, majd rögtön megjelenik a szerkesztőablakban. Most jön azon ritka pillanatok egyike, hogy beleírhatunk a programunka. Kezdjük

azzal, hogy az include-ok közé beírjuk a meghívandó dialógusablak header-jét. Ugyanis itt van definiálva a típusa pl.: #include "newdlgh" A handler pedig csak ennyi lesz: void CSecondDlg::OnKilep() { newdlg dlg; //dlg egy newdlg típusú változó dlg.DoModal(); //meghívjuk azt a függvényét, ami elindítja } Tehát mivel a class-ok csak típusok, nem adhatjuk rájuk a vezérlést (nem hívhatjuk függvényeiket). Kell csinálni egy változót abból a típusból. És a változó DoModal() függvényével tudjuk odaadni a vezérlést. 23 És már be is iktattunk egy újabb ablakot. Nem olyan bonyolult ADAT BEVITEL Na, nézzük. Miután ablakot már tudunk kirakni, próbáljunk meg adatot bekérni. Na persze ez nem nemgy csak úgy ripsz-ropsz. Mivel adatokról van szó, azokat ugye stílszerűen változókban illik tárolni. Ezért most először nem csak ide-oda kattintgatunk a menükben, hanem egy kis elméletet kell nézni. Mégpedig azt, hogy milyen

változókat enged meg a Visual C A dolog ott kezdődik, hogy itt már nem csak a klasszikus értelemben vett változók léteznek, hanem már objektumok is. Az adaton kívül az adott típusra jellemző konverziós és kezelőfüggvényeket is tartalmazzák. Persze minden adattárolásnak azért az alaptípusok, potosabban az úgynevezett egyszerű típusok (Simple types) az alapjai. Tehát jöjjenek az egyszerű típusok. Természetesen a C-ben és a C++-ban használt összes típus használható Ezeken kívül csak azt írom le, ami új. Na persze nem mind a 3 oldalt, csak a gyakrabban használatosakat. Ezeket a típusokat tulajdonképpen csak a paraméterátadásnál, visszatérési értékeknél, message handlereknél használjuk. Jó részüket csak a szabvány könyvtári híivásoknál kell elővennünk. Egy sima szám tárolása itt is egy egyszerű int –el történik A string-ek tárolásánál nagyobb változások történtek. Itt van az az objektumos dolog Na de nézzük

akkor a gyakrabban használt egyszerű típusokat.                  BOOL Boolean value BSTR 32-bit character pointer BYTE 8-bit integer that is not signed. COLORREF 32-bit value used as a color value. DWORD 32-bit unsigned integer or the address of a segment and its associated offset. LONG 32-bit signed integer. LPARAM 32-bit value passed as a parameter to a window procedure or callback function. LPCSTR 32-bit pointer to a constant character string. LPSTR 32-bit pointer to a character string. LPCTSTR 32-bit pointer to a constant character string that is portable for Unicode and DBCS. LPTSTR 32-bit pointer to a character string that is portable for Unicode and DBCS. LPVOID 32-bit pointer to an unspecified type. LRESULT 32-bit value returned from a window procedure or callback function. UINT 16-bit unsigned integer on Windows versions 3.0 and 31; a 32-bit unsigned integer on Win32. WNDPROC 32-bit pointer to a window procedure. WORD

16-bit unsigned integer. WPARAM value passed as a parameter to a window procedure or callback function: 16 bits on Windows versions 3.0 and 31; 32 bits on Win32 A P-vel, vagy LP-vel kezdődő változók nem valódi pointerek, hanem csak úgynevezett handle-ek. Ez a handle egy címtáblázat egy elemére mutat, ami a cucc valódi címét tartalmazza. Szóval Visual C-ben, ahogy mellesleg JAVA-ban is, már nincsenek klasszsikus pointerek, amik meghatározott memóriaterületre mutatnakak. Ezek lennének a lényegesebbek. A többi típust azért nem írom le, mert vagy ezekből, vagy az alaptípusokból származtathatóak, vagy nem nagyon van rá szükségünk. 24 Objektum típusúból (Simple value type) már kevesebb van. Ezekre az Objektumokra az a jellemző, hogy nincsenek sehonnan származtatva. Nem úgy, mint az a másik ötezer, aminek mind a CObject az alapja. Mint említettem, itt már nem csak az adat van tárolva, hanem egypár rá jellemző függvény is. Egyszerű

érték típusok:       CPoint Egy abszolút koordináta tárolására való. (x,y) CRect Egy téglalap alakú terület bal felső és jobb alsó koordinátáit tartalmazza CSize Egy relatív koordináta tárolása. X és Y irányú méret CString Egy String tárolására való. CTime Abszolút idő és dátum CTimeSpan Relatív idő és dátum Logikailag némileg nem stimmel, viszont kényelmi okokból maga az adat nem az objektum egy tagja (változója), hanem egyszerűen az objektum nevének leírásával hivatkozunk rá. Ennyi elmélet után csináljunk valami programot. Ugyanúgy kezdjük, mint eddig, Egy kis Application Wizard. Aztán rajzoljuk meg a kezdő ablakot. Mivel adatbekérést szeretnénk, nem árt, ha egy pár edit line-t is rakunk Ezekután eresszünk rá egy Classwizard-ot a Dialog ablak Class-ára. Ráállunk a Classview-ban, és View/Classwizard. Itt az Add Variable-re kattintva bármelyik Control (Gomb, edit line stb) -hoz rendelhetünk

változókat. Aminek az értékét később fel akarjuk használni, ahhoz rendeljünk Csak a váltlozó nevét, kell beírni, a felkínált típus midig megfelelő. A Classwizard elintézi a változók definícióját a class-hoz tartozó header-ben, a változó előkészítését a Class első függvényében, az adatcserét pedig a DoDataExchange-ben. Ha meg akarjuk tudni a változóhoz rendelt elemek értékeit, az Update nevű függvényt kell hívni TRUE-val. Update(TRUE); Ez a függvény a Cwind objektum tagja, a Cdialog a Cwind-ből származik, tehát simán hívhatjuk. Ez a függvény végzi az adatcserét a változók és a hozzájuk rendelt Control-ok között. Ha TRUE-val hívjuk, a változókat feltölti az adatokkal, Ha FALSE-al, a változókban tárolt adatokat rakja be az ablakba. Mégpedig úgy, hogy közvetve meghívja a DoDataExchange-t. Mi ezt soha ne tegyük, mert elszáll a program Ha ezzel megvagyunk, készítsünk egy új Dialógusablakot. Egy pár Static Text-el

Mindegyiknek legyen saját ID-je. Ide fogjuk beírni az átvett adatokat A hozzá tartozó Class-ot ClassWizard-dal elkészítjük úgy, hogy a static text-ekhez változókat rendelünk. Ha ez megvan, márcsak egy olyan függvényt kell hozzáadni a Class-hoz, ami ezeknek a változóknak értéket ad. Balgomb a ClassView-nál és add member function 25 Itt meg kell adni a Függvény típusát, a deklarációját, az elérését. Ha csak egy sima függvény, akkor az alapbeállításokat el lehet fogadni. Ha ez megvan, csak meg kell írni a függvényt, és a másik Class-ból meghívni. A függvény: void CInfOutDlg::SetMyData(LPCSTR nev,LPCTSTR szam,LPCTSTR cim) { //Adatok atvetele m cim = cim; m nev = nev; m szam = szam; } Ezt egy Handler-ből meghívva: void CMythirdDlg::OnNext() { // TODO: Add your control notification handler code here UpdateData(TRUE); //Adatok behuzasa a valtozokba InfWin.SetMyData(nev,szam,cim); //Adatok atadasa InfWin.DoModal(); //Ablak Inditasa cim =

T(""); szam = T(""); nev = T(""); //Kezdo adatok UpdateData(FALSE); //toroljuk a beirt adatokat. } A program működik. . 26 LISTBOX HASZNÁLATA Feketelistakészítő program készítése Miután modális(modal) dialógusablakok gyártásában már otthonosan mozgunk, készítsünk nem modálisat (modeless). Mert eddig ugyan nem fordítottunk rá különösebb figyelmet, de bizony a dialógusablakok modálisak voltak. Ez egyszerűen annyit jelent, hogy addig nem tudunk visszalépni az előzőre, amíg az éppen aktívat be nem zártuk. Ez a bezárás praktikusan az OK, vagy CANCEL gomb lenyomásával szokott történni. A nem modális dialógusablak onnan ismerszik meg, hogy nem kötelező bezárni, az előzőre mindig visszaléphetünk. Ez eddig elég egyszerűnek hangzik, de azért ennek messzemenő következményei vannak. A legszembetűnőbb probléma az, hogy míg a modális ablak automatikusan eltűnik, mivelhogy tudja, hogy el kell

tűnnie, nekünk nem sokat kell az ablak megsemmisítésével (destroy) foglalkozni. Most viszont lesz dolgunk ez ügyben Aztán az adatcserénél is változások lesznek Eddig nagyon jó volt a DDX, meg a DDV, ezekkel ki tudtuk szedni az adatot változókba, és feldolgozni. Most viszont mivel mind a két dialógusablak egyszerre létezik, az is, ahonnan indítottunk, meg az is, ami indult, az adatcsere egymás control-jainak közvetlen elérésével sokkal egyszerűbb. Akkor a tőlem már megszokott bőre eresztett elmélet után a gyakorlat. Az egész projektet ugyanúgy kell kezdeni, mint eddig (AppWizard,MFC,.), megrajzoljuk a dialógusablakokat. Eddig még nem használtuk a List Box-ot, de gondolom senkinek nem jelent különösebb akadályt kiválasztani az eszköztárból és berajzolni a helyére. Az újonnan rajzolt ablakhoz létre kell hozni egy új Class-t. Persze ClassWizard-dal a legegyszerűbb (dupla kattintás a vadi zsír dialógusablakunkon) Mindent ugyanúgy kell

beállítani, mint eddig, csak most a member függvényeknél lesz egy kis eltérés. Pontosan körülbelül így fog kinézni: 27 Eddig nem így készítettük a kezelő (handler) függvényeket, de ez sem bonyolultabb. Ki kell választani az Object Ids ablakban annak az azonosítóját, amihez akarjuk rendelni a függvényt, a Messages ablakban, hogy milyen eseményhez, és rá kell bökni az Add Function Gombra. Amint a mellékelt ábra is mutatja 4 kezelőfüggvényünk lesz. Ebből egy már eddig is létezett a DoDataExchange. Ezzel most nem is kel törődni A két gombhoz kellett rendelni új függvényeket, meg a PostNcDestroy már létező függvényből egy újat csinálni, ami persze nem írja felül a régit, mert virtuális. A két gombon nem sokat kell magyarázni, de ez a PostNcDestroy elég homályos. Nos ez a függvény a CWnd objektum tagja (member). Mellékesen ebből az objektumból származik mindenféle ablak. Akkor hívódik meg, ha az ablak eltüntette

magát, és az automatikusan létrehozott cuccokat megsemmisítette. Tipikusan arra való, hogy az általunk foglalt memóriát felszabadítsuk, és eltüntessük a maradék dolgokat. Eddig nem nagy kaland. A húzósabb rész akkor kezdődik, mikor a létrehozott függvényeket meg kell írni. Rajta Lássuk a Hozzáad gomb lenyomásakor lefutó OnHOZZAAD nevű függvényt. void CAdderDialog::OnHOZZAAD() { CEdit* pEdit = (CEdit) GetDlgItem(IDC NEWNAME); CListBox* pList = (CListBox) (m pParent->GetDlgItem(IDC LIST)); ASSERT(pList != NULL); ASSERT(pEdit != NULL); //ha nulla, akkor hibajelzés if (pList != NULL && pEdit != NULL) { 28 CString str; pEdit->GetWindowText(str); pList->AddString(str); } } Némi magyarázatra szorul. Kell két változó, egy Cedit objektumra mutató pointer, Ez fog mutatni az editline-ra meg egy ClistBox objektumra mutató pointer, ez fog mutatni a szülő (ahonnan indítottuk, m pParent mutat rá) ablak ListBox-ára. Az adatcserét a

két objektum között fogjuk elvégezni egy Cstring típusú változó segítségével. A CEdit GetWindowTExt függvényével bekérjük az editline-ban szereplő stringet az str-be, a ClistBox AddString függvényével pedig berakjuk a ListBox-ba. Tehát az adatcsere nem DDX-el történt. A Kész gombra aktivizálódó OnKESZ függvény: void CAdderDialog::OnKESZ() { ((CMainDlg*)m pParent)->BoxDone(); DestroyWindow(); } Ez csak annyit csinál, hogy meghívja a kezdő dialógusablak BoxDone függvényét, ami újra engedélyezi a nem modális ablak aktivizálódásánál letiltott Hozzáad gombot. Erre a játékra azért van szükség, mert erre a gombra indul a nem modális ablak, és ahányszor rányomnánk mindig újból megjelenne. Ezekután meghívja a DestroyWindow() függvényt, ami a Cwind tagja. Amint a neve is mutatja az ablak megsemmisítésére, azaz bezárására való A PostNcDestroy függvényünk: void CAdderDialog::PostNcDestroy() { delete this; } Nos a sor nem

igazán emlékeztet egy C++-ben írt sorra, pedig működik. A this egy konstans pointer, amit nem kell definiálnunk, mindig hivatkozatunk rá. Mindig az őt tartalmazó függvény objektumára mutat. A delete egy operátor, ami felszabadítja az utána szereplő pointer segítségével címzett, és természetesen lefoglalt memóriát. Tehát az objektumnak minden apró porcikáját száműztük a memóriából. Ezzel megvolnánk, de van még egy apró probléma amit eddig elhalgattam. Nem modális dialógusablakot a Create függvénnyel kell létrehozni. Ez a Cdialog Class tagja Meg kell neki adni az ablak ID-jét, meg a Parent Class címét. Tehát: BOOL CAdderDlg::Create() { 29 return CDialog::Create(m nID, m pParent); } Természetesen az m nID, és m pParent változókat létre kell hozni, és értéküket a Class konstruktorában be kell állítani. (A Class konstruktora a Classal azonos nevű függvény) Ezzel megvolnánk,, már csak a kezdő dialógusablakkal kell

tudatni, hogy itt van rajta kívül még valaki. Kell egy változó, pontosabban egy pointer, ami a nem modális dilaógusablakra fog mutatni.: CAdderDlg* m pModeless; Ezt persze egy add member variable-val el lehet intézni. Aztán már csak el kell indítani amit eddig műveltünk. void CMainDlg::OnOK() { if (m pModeless == NULL) { m pModeless = new CAdderDlg(this); if (m pModeless->Create() == TRUE) GetDlgItem(IDOK)->EnableWindow(FALSE); } else m pModeless->SetActiveWindow(); } Tehát a new operátorral hoztuk létre az objektumot, ezért kell majd a delete operátorral megsemmisíteni. A this pointer már ismerős A konstruktornak átadjuk a parent , tehát a saját cíimünk. Miután megvan a dialógusablak, csak le kell tiltani azt az gy gombot SDI ABLAKOK Miután dialógusablakokban már profik vagyunk, most a normál ablakok következnek. Az az egyszerű egy menü, egy kis keret, meg a menüből nyíló dialógusablakok. Az ilyen stílusú alkalmazást SDI-nek (Single

Document Interface) is szokás nevezni. 30 Szóval a helyzet úgy néz ki, hogy van egy Frame Window, ami maga az alkalmazás ablaka. Része a rendszermenü, a menüsor, az esetleges ikonsor és a fejléc. Magába foglalja az un Client Area –t. Ez az üres terület az, ahol a dokumentum megjelenik, és a felhasználó dolgozhat vele. A Client-Area-hoz hozzá van rendelve, egy View, ami a megjelenítést végzi Maga a dokumentum pedig a View-val tart kapcsolatot. A Frame-hez, pontosabban a menüpontjaihoz pedig Dialógusablakok rendelhetők. Az ilyen menüpontok általában -ra végződnek. Az SDI lényege tehát az, hogy egy Frame Window-hoz egy Dokumentum van rendelve. Ami a példaprogramot illeti, aClient Area-t nem kötelező View-hoz rendelni, a Frame -ből is írhatunk rá. Így természetesen Dokumentum sem lesz, de így a kályhától indulunk el A mostani program összehozásához most nem nyújt olyan rugalmas segítséget az AppWizard, de azért még mindig

egyszerűbb, mint mindent újból kezdeni. Tehát MFC AppWizard, Single Document. Az egyszerűség kedvéért különböző extrákkal nem kell agyontuningolni. Ha ez megvan, tisztogatni kell egy kicsit. Az AppWizard által létrehozott View és Doc Classokat egy az egyben le lehet szedni Mivel a ClassView-ból nem lehet törölni, a file-jaikat kell eltávolítani. Ezek után a program összesen 3 Class-t tartamaz.    CSDIApp CMainFrame CAboutDlg az alkalmazás Class-a a Frame Window Class-a az about ablak. Az alkalmazás objektumában csak az InitInstance függvényre van szükségünk, és esetleg az AboutBox Message Handler-ére. Az InitInstance tartalmazza a FrameWindow meghívását: m pMainWnd=new CMainFrame; m pMainWnd->ShowWindow(m nCmdShow); m pMainWnd->UpdateWindow(); 31 A new paranccsal létrehozunk egy FrameWindow-t, amit kirajzoltatunk, és átadjuk rá a vezérlést. A lényeges dolgok innentől a FrameWindow-ban zajlanak.A Konstruktorban van egy

dolog, amit mindenképp meg kell tenni.: LoadFrame(IDR MAINFRAME); //Frame betoltese A Frame-hez tartozó összes resources (erőforrás) itt kerül betöltésre. Ilyenek a menüsor, az Ikonok, a Gyorsbillentyűk, és a string tábla. A leggyakrabban használt ezek közül a menüsor. A szerkesztése nagyon egyszerű, a Resource View-ban csak rá kell kattintani. Minden menün belül létrehozhatunk menüpontokat, és továbbnyíló Pop-Up menüket. A Pop-Up menükkel semmi dolgunk nincs, azok kezelését a rendszer teljesen magától végzi. A menüpontok az érdekesek Mindegyikhez tartozik egy ID Kezelőfüggvényeket rendelhetünk az általuk kiváltott üzenetekhez. A kétféle üzenet lehetséges: COMMAND UPDATE COMMAND UI A COMMAND üzenethez Handler-t, azaz kezelőfüggvényt lehet rendelni. Például az Open menüponthoz egy olyan függvény tartozna, ami elindít egy dialógusablakot, amiben bekéri a file nevét, majd megnyitja azt. Szóval ez a függvény a parancs

végrehajtására való Az UPDATE COMMAND UI üzenethez callback függvény rendelhető. A Callback függvény olyan fajta függvény, amit csak az alkalmazáson kívülről lehet hívni. A menüponthoz tartozó CallBack függvény akkor fog meghívódni (szép magyar szóval), ha a menüpont állapota változik. Ami azt jelenti, hogy rányomtunk Tipikusan arra való, hogy a kiválasztott menüpontot megjelöljük egy pipával, vagy más menüpontokat inaktívvá tegyünk. Röviden mindkét üzenet a menüpont kiválasztásnál generálódik, de teljesen más céllal. Ezért a hozzájuk rendelt függvényeket sem jó összekeverni. Ezeknek a hozzárendeléseknek a megvalósítása legkönnyebben a ClassWizard-dal érhető el. Kiválasztjuk a menüpont ID-jét, az üzenetet (COMMAND/UPDATE COMMAND UI), és egy Add Function-nal már meg is jelent. Ennek megfelelően egy példa a Handler függvényre: void CMainFrame::OnSettext() { CNewTextDlg dialog; dialog.m NewText=Text; if

(dialog.DoModal()==IDOK) Text=dialogm NewText; MyUpDate(); } Minden dolog ismert benne, meghív egy dialógusablakot, ami bekér egy stringet, és a saját változójában eltárolja. Onnan kiszedhetjük, hiszen attól, hogy az ablak már eltűnt a memóriából, a változói még megvannak. Egy példa a CallBack függvényre: void CMainFrame::OnUpdatePos(CCmdUI* pCmdUI) { pCmdUI->SetCheck(pCmdUI->m nID == Pos); 32 } Amint látható a függvény kap paraméterként egy CCmdUI Class-ra mutató pointert. Ennek segítségével tudjuk a menüt változtatni. Változás esetén a régi menüponttól leszedi a pipát, az újra meg felteszi. Csak azért lehetséges, mert az összes menüpont CallBack függvénye egy személyben, akit változtatni kell az adott Pop-Up menün belül. A program egy stringet pozícionál a képernyőn a menüben kiválasztott helyre. Ehhez írni kell a Client Area-ra. Ilyenkor leggyakrabban az WM PAINT üzenethez rendelt függvénnyel lehet célba

érni. Az WM PAINT üzenet akkor generálódik, ha az ablak újrarajzolódik. Ebben az estben pedig nyugodtan írhatunk. void CMainFrame::OnPaint() { CPaintDC dc(this); // device context for painting dc.SetTextAlign(TA BASELINE | TA CENTER); dc.SetTextColor(::GetSysColor(COLOR WINDOWTEXT)); dc.SetBkMode(TRANSPARENT); dc.TextOut(TextX, TextY, Text); } A CPaintDC Class segítségével írhatunk ki nem az ablakhoz (most a Client Area-hoz) tartozó szöveget. Tetszőleges koordinátákra, tetszőleges színnel A koordinátatengelyek: Természetesen a helyes koordináták kiszámolásához ismernünk kell az ablak méretét, a szöveg méreteit képpontokban. Ezen kívül minden alkalommal, amikor újraméretezzük az ablakot, újra kell számolni a koordinátákat. Egy példa a Client Area méreteinek lekérdezésére: CRect rect; //negyzet koord CSize WindSize; GetClientRect(rect);//Ablak koord. lekerd WindSize.cx=rectright; WindSize.cy=rectbottom; A Crect Class egy négyzet jellemző

két koordinátáját tárolja. A bal felsőt és a jobb alsót Hasonló Class a CSize is, ez x és y méretet tárol. A GetClientRect függvény egy CRect Classba adja vissza a ClientArea méreteit, innen pedig kényelmesen ki lehet pakolni. Egy példa a szöveg méreteinek lekérdezésére: CPaintDC dc(this); // device context for painting 33 CSize TextSize; TextSize=dc.GetTextExtent(Text); Szintén a CPaint segítségével. A GetTextExtent függvény képernyőpontokban adja vissza a string méretét. Említettem, hogy az újraméretezésnél is mindent előröl kell kezdeni. Kell egy Handler-t írni a WM SIZE üzenetre. Akkor hívódik meg, mikor az ablak új méretet kapott. Példa: void CMainFrame::OnSize(UINT nType, int cx, int cy) { CFrameWnd::OnSize(nType, cx, cy); WindSize.cx=cx; WindSize.cy=cy; MyUpDate(); } A függvény kapásból megkapja az új méretet, csak el kell tárolni, és újraszámolni a szöveg koordinátáit. Úgy érzem minden részletre kitértem. Azt

inkább nem írom le, hogy kell kiszámolni egy négyzet közepét, meg a széleit. Azt hiszem eddig ez a leghosszabb VisualC-s cikkem. Nem baj úgyis jubilálunk, mert ez a 20 (vagy a 21.) PCX-USER [Ez a 24 Szerk] :-) MDI ABLAKOK MDI Multi Document Interface. Segítségével egyszerre több dokumentumablak szerkeszthető Ezket az egy alaklmazáshoz tartozó ablakokat child window-nak nevezzük. Evileg végtelen számú nyitható meg egy –egy típusból. Tehát ugyanabból a Class-ból egyszerre bármennyi változó lehet. Így létrehozásuk dinamikusan futás időben a (new operátorral) történik, és természetesen kapcsolatot kell tartaniuk az őket létrehozó szülő (Parent) ablakkal. Így elöljáróban csak ennyit. Nézzük kicsit képletesebben 34 Ahogy az SDI-nél, a főablak itt is egy frame window. Ennek egy speciális child ablaka az MDICLIENT window. Ez kezeli a már SDI-nél is létező CLIENT Area-t, beleértve az összes child window-al. A child window

szerkezete tulajdonképpen megegyezik a már megismert Frame window-al. A Frame (Most Child) window szerkezete: A figyelmesebbek észrevehetik, hogy az ábra pontosan ugyanaz, mint az SDI-s frame window. Tehát, mint látszik, a Child window szinte ugyanolyam mint az eddig már megismert Frame window, csak ugye egyszerre több lehet belőle, és van szülő ablaka. Mely szülő a Main Frame. Normális esetben a megjelnítést egy View Class végzi, miközben a Dokumentummal kapcsolatot tart. Az egyszerűség kedvéért most View és Dokumentumkezelést kihagyom, ezekről majd később bővebben. Tehát a példaprogram a legegyszerűbb esetet próbálja feldolgozni, így talán még meg is lehet érteni. Hogyan is kell elkezdeni egy ilyen programot. Első nekifutásra az AppWizard a legegyszerűbb megoldás. Ugyanúgy, ahogy eddig, a típusnál pedig értelemszerűen MDI-t kell választani Nem kell mindenféle extrát beixelni, nem biztos, hogy átláthatóbbá teszi a problémát. Ha

ez megvan, tisztogatnunk kell egy kicsit. Törölni kell a View és Doc Class-okat, mivel nem lesz rájuk szükség. Ha minden megvan, a következő Classok fognak látszani: 35 Class Feladata Az alkalmazás A Főablak A Child Window Származtatjuk: CwinApp CMDIFrameWnd CMDIChildWnd A programban: CMDIApp CMainFrame CChildFrame Ezeken kívül természetesen még sok-sok más lehetne, de ez a váz. Mint látható minden egyes Class-nak jól definiált feladata van. Nézzük először a legegyszerűbbet, és legismerősebbet, az alkalmazás objektumát.: class CMDIApp : public CWinApp { public: CMDIApp(); //{{AFX VIRTUAL(CMDIApp) public: virtual BOOL InitInstance(); //}}AFX VIRTUAL // Implementation //{{AFX MSG(CMDIApp) afx msg void OnAppAbout(); //}}AFX MSG DECLARE MESSAGE MAP() }; Van itt egy konstruktor, egy kezelőfüggvény, ami az About-ot nyitja, meg az InitInstance. Ezek közül egyedül az InitInstance() érdekes igazán. Mint nevéből is kiderül, itt kell megteremteni

a kezdőfeltételeket. BOOL CMDIApp::InitInstance() { #ifdef AFXDLL Enable3dControls(); // Call this when using MFC in a shared DLL #else Enable3dControlsStatic(); // Call this when linking to MFC statically #endif Eddig csak a 3d Control betöltése került sorra, de ezután jönnek a fontos dolgok. CMainFrame* pMainFrame = new CMainFrame; if (!pMainFrame->LoadFrame(IDR MAINFRAME)) return FALSE; m pMainWnd = pMainFrame; Létrehozzuk a MainFrame-et (Ld. Az ábra), azaz a főablakot Ez dinamikusan, new operátorral történik. A főablak címét a CWinApp m pMainWnd változójába kell tenni, tulajdonképpen itt kapcsolódik az ablak az alkalmazással. Az ablakhoz természetesen be kell töletni a hozzá tartozó erőforrást, ez a LoadFrame függvénnyel történik, és egyszerűen az azonosítót kell megadni. Az erőforrás itt, és általában a Frame ablakoknál egy egyszerű menüsor, vagy ikonsor. pMainFrame->ShowWindow(m nCmdShow); pMainFrame->UpdateWindow(); 36

return TRUE; Miután megvan az ablak erőforrássával együtt, meg kell jeleníteni. A ShowWindow függvényt meghívva kirajzolódik az ablak. Egyedül a főablaknál kell meghívni ezt a függvényt, tehát egy alklamazás során csak egyszer. Ezekután az Update meghívásával kiváltunk egy WM PAINT üzenetet, melynek hatására a Client Area újrarajzolódik. Esetünkben megjelenik A MainFrame objektuma: CMainFrame: class CMainFrame : public CMDIFrameWnd { public: CMainFrame(); //konstruktor public: virtual ~CMainFrame(); //destruktor protected: //{{AFX MSG(CMainFrame) afx msg void OnNew(); //}}AFX MSG DECLARE MESSAGE MAP() }; Egy konstruktor, egy destruktor, meg egy üzenetkezelőfüggvény. A konstruktor és a destruktor jelen esetben üres. Az üzenetkezelő függvény végzi a Child ablakok létrehozását, szempontunkból ez az érdekes. Először az ablak objektumánk létrehozása a new paranccsal. Az objektumra mutató pointert kapunk. CChildFrame *pChildWnd = new

CChildFrame; //Ez lesz a Child Window Ahoz, hogy egy általunk létrehozott Class-ból ablak legyen, regisztráltatni kell az ablakot. Ez annyit tesz, hogy a framework bejegyzi az ablakot a megadott tulajdonságokkal. Ez az AfxRegisterWndClass függvénnyel történik. LPCTSTR AFXAPI AfxRegisterWndClass( UINT nClassStyle, HCURSOR hCursor = 0, HBRUSH hbrBackground = 0, HICON hIcon = 0 ); Visszatérési értéke egy C típusú string, amely az azonosítót tartalmazza. Un class name Erre majd szükség lesz az ablak létrehozásánál. A Paraméterek: Az ablak stílusa. A stílusok kombinációit kell megadni a bitenkénti vagy (|) operátorral. Ezek a stílusok előre meghatározott konstansok Van belőlük egy halom, a help-ben bárki megtalálja. A jelnleg használtak a CS HREDRAW, és a CS VREDRAW. Azt biztosítják, hogy mind horizontális, mind vertikális méretváltozáskor az ablak újrarajzolódjon. A kurzor azonosítója. Lehet általunk gyártott is, de általában a

LoadCursor hCursor függvény által betöltött szabvány kurzorokat használjuk. Erről majd a továbbiakban. hbrBackground A háttér. Meghatározza a kitöltési színt és mintát Típusa szerint HBRUSH, ami egy CBrush objektum handlere. Az ikon handlere. Hogy ezt megkapjuk, a kurzorhoz hasonlóan úgy kell hIcon betölteni, a LoadIcon függvény segítségével. nClassStyle 37 A megemlített további függvények: A kurzor betöltéséhez: HCURSOR LoadCursor( HINSTANCE hInstance, LPCTSTR lpCursorName ) Visszatérési értéke a kurzor handlere Paraméterek: Annak a DLL-nek, vagy EXE-nek,vagy erőforrásállománynak az azonosítója, amely a kurzort tartalmazza. Szabványos kurzorokk betöltésénél ez NULL lpCursorName kurzor azonosítója. Előre meghatározott konstansok: IDC APPSTARTING nyíl+homokóra IDC ARROW sima nyíl IDC CROSS Crosshair IDC IBEAM I-beam IDC ICON megrajzolt ikon IDC NO nincs kurzor IDC SIZE méretezési kurzor IDC SIZEALL méretezési kurzor

négyfelé nyíllal IDC SIZENESW jobbra fel,balra le nyíllal IDC SIZENS fel-le nyíllal IDC SIZENWSE balra fel, jobbra le IDC SIZEWE jobbra-balra IDC UPARROW felfele mutató nyíl IDC WAIT Mindenki kedvence, a homokóra hInstance Az Ikon betöltése: HICON LoadIcon( HINSTANCE hInstance, LPCTSTR lpIconName) Vissztérési értéke az Icon handlere Paraméterek: hInstance Ahogy a kurzorbetöltésnél, annak az erőforrásnak az azonosítója, ahol az Ikon van. Ez általában a saját programunk A saját programunk handle-ét a AfxGetInstanceHandle() függvénnyel tudjuk meg. lpIconName Az ikon azonosítója (name –je). A MAKEINRESOURCE makróval tudunk az ID ből name-t csinálni. Tehát egy példa az ablak bejegyzésére, és name-jének eltárolására: LPCTSTR lpszChildClass = AfxRegisterWndClass(CS HREDRAW | CS VREDRAW, LoadCursor(NULL, IDC NO), (HBRUSH)(COLOR WINDOW+1), LoadIcon(AfxGetInstanceHandle(), Ez nem egy egyszerű dolog, de még mielőtt az ablak megjelenne, a menu-t

is be kell tölteni. 38 A CWnd menu.LoadMenu függvényével történik Menu betöltése: pChildWnd->menu.LoadMenu(IDR LINEFRAME); Ezekután a Child window m hMenuShared tagváltozójának is meg kell adni a menut. Mivel ez a változó protected, csak az objektumon belülről lehet átírni. m hMenuShared = menu.m hMenu; Ha ez megvan, hozzuk létre az ablakot. A CWnd Create függvényével virtual BOOL Create( LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext = NULL); Visszatérési érték nem nulla, ha sikeres, nulla ha sikertelen. Paraméterek: lpszClassName Az AfxRegisterWndClass által adott Class Name. lpszWindowName C típusú string, amely a címet tartalmazza. dwStyle Az ablak stílusának további attributumai. Van belőle egy pár Alapértelmezésben: WS CHILD | WS VISIBLE | WS OVERLAPPEDWINDOW rect Az ablak pozíciója. Legkényelmesebb a rectDefault, ekkor az elhelyezés

automatikus lesz. pParentWnd A szülő ablak címe. Simán this pContext Az ablak Context-je. Ezt általában kell megadni, alapértelmezés NULL Egy példa: pChildWnd->Create(lpszChildClass, T("Im a Child"), WS CHILD | WS VISIBLE | WS OVERLAPPEDWINDOW, rectDefault, this); } És már meg is van a Child Window. A VIEW ÉS A DOKUMENTUM KAPCSOLATA Ha valaki egyszer elmagyarázná, hogyan kell bevezetőt írni. :-) [ Szerk: én tudom :-) ] A View és a Dokumentum kapcsolata Induljunk el a kályhától: A CDocument Class biztosítja az alapvető dokumentumkezelési funkciókat. Az általunk ebből 39 származtatott Class-ok fogják kezelni a dokumentumot. A dokumentum azt az adategységet jelenti, amit a felhasználó áltálában egy Open File-al nyit meg. Tehát fizikailag valójában egy file Azonban a dokumentum kezelésére szolgáló Class-ban egy Archive-ot kezelünk. Ez egy kicsit hasonló, mint Cben a Stream(De persze azért egész más  ) A CView Class

szolgál alapjául a View kezelésére létrehozott Class-oknak. Tehát egy ilyen Class-ból kell származtatni a saját View Class-unkat. Tulajdonképpen egy közvetítő szerepet játszik a Dokumentum és a felhasználó között. A View jeleníti meg a dokumentum egy részét, és a felhasználó által eszközölt módosításokat közvetíti a dokumentum felé. Szintén a View feladata a nyomtatás, és a nyomtatási nézet (Print PreView). Aki eddig értette, azt az alábbi ábra remélem végleg összezavarja: A Document-View párosításnak az az előnye, hogy elválasztjuk a dokumentumot kezelő cuccokat, a dokumentumot megjelítőktől. Így átláthatóbb programot kapunk Ebből a szétvágásból aztán rögtön az a hátrány következik, hogy a View-nak, és a Dokumentum-nak állandóan kapcsolatot kell tartaniunk egymással. Azt lássuk a képernyőn, ami a tényleges adat, és az legyen a tényleges adat, amit a képernyőre beírtunk. Ez az oda-vissza játék néha

elég bonyolult tud lenni (De mint tudjuk, olyat még senki nem talált ki, hogy mindenkinek jó legyen  ) Akkor az adatkezelésről kicsit bővebben. Ahogy említettem a Dokumentumot kezelő Classból nem közvetlenül érjük el a file-t, hanem egy Archive-on keresztül. Ez a CArchive Class, amely lehetőséget ad Objektumok, Objektumhálózatok tárolására (mondjuk egy file-ban) bináris formában. Az Objektumok megszűnésük után innen visszaállíthatóak. Ezt az Objektum elmentést, visszatöltést Serialization-nek hívjuk (Próbáltam magyarra lefordítani, de a megsorosítás volt a legjobb verzió). Tehát mikor a dokumentumot kezeljük, nem lesz filekezeléssel dolgunk, csak serialization-nel. Ez jóval kényelmesebb a szokásos filekezelésnél, meg Recordok olvasgatásánál. Mikor a felhasználó megnyit egy file-t, az automatikusan deserializálódik(szépen magyarul), azaz visszaállítódik minden elmentett Objektum. Ebben van a dolog hátránya, hiszen így

jóval több memóriát használunk Természetesen a régi bevált Rekord-os kezelés is megoldható, csak ki kell trükközni a Serializationt. Az is lényeges, hogy csak azok az objektumok Serializálhatóak, amik erre fel vannak készítve. Azaz létezik Serialize() függvényük. Ezenkívül használniuk kell az IMPLEMENT SERIAL, és DECLARE SERIAL makrókat. 40 A View: A View tulajdonképpen Child-ja egy Frame Window-nak. Egy Frame ablakon belül több View is létezhet, például felosztott ablakoknál. Egy View csak egy Documenthez rendelhető, viszont egy Document-hez több View is. A View a hozzá rendelt Document-et a GetDocument() függvény segítségével éri el. Ha a Document-ben változás történt, tehát változott az adat, egy kis segítséggel a View OnUpDate függvénye hívódik meg. Itt lehet elintézni, hogy a módosított adatok kiíródjanak A View-Class-unkat a Cview-ból kell származtatni, vagy egy abból származtatott Class-ból. Ebből többféle

van, ami szemnek szájnak ingere.          CCtrlView, Control-okat tartalmazó view (tree, list, rich edit controls). CDaoRecordView, adatbázisrekordokat tartalmazó view CEditView, Egy egyszerű text editor. CFormView, dialog-box control-okat tartalmazó View, egy dialog template resource-ből szedi a control-okat. CListView, list controls. CRecordView, adatbázisrekordokat kezelő view CRichEditView, rich edit controlt tartalmazó View CScrollView, egyszerű, görgethető ablak CTreeView, Tree View A programban a CFormView-t fogjuk használni. A Document: A CDocument alapban tartalmazza a File Open, File Save, stb. parancsokat Sőt kilépéskor magától megkérdezi, hogy mentsen-e. Egy alkalmazáson belül többféle Document is lehet Minden Document Class-nak van egy Document Template-je. Ez tartalmazza a Document hozzárendeléseit, milyen menüket használ, milyen ikont, stb Az UpDateAllViews függvény hatására hívódnak meg a View-okban az

OnUpdate függvények. Így elöljáróban ennyit. Nézzünk egy programot. Egy igen buta telefonkönyv. Nem eredeti ötlet, de elsőnek megteszi A program vázát megkaphatjuk az AppWizard-tól (Single Document, stb) Az első fontos dolog az Application InitInstance-ében: CSingleDocTemplate* pDocTemplate; pDocTemplate = new CSingleDocTemplate( IDR MAINFRAME, RUNTIME CLASS(CTkDoc), RUNTIME CLASS(CMainFrame), // main SDI frame window RUNTIME CLASS(CTkView)); AddDocTemplate(pDocTemplate); Itt történik a Document Template elkészítése. Össze kell rendelni a dokumentumot a View-al, és a Frame window-al. A CsingleDocTemplate objektum csak SDI alkalmazásoknál használható Csinálunk egy ilyen típusú pointert, a new operátorral ténylegesen is létrehozzuk a Class-t, majd az AddDocTemplate függvénnyel bejegyeztetjük. Kell egy Objektum, ami a menteni kívánt adatokat tartalmazza. class CTkRecord : public CObject 41 { // Construction public: CTkRecord(LPCTSTR

NewName,LPCTSTR NewAddress,LPCTSTR NewNumber); protected: CTkRecord(); DECLARE SERIAL(CTkRecord) // Attributes public: CString Name; CString Address; CString Number; // Operations public: virtual void Serialize(CArchive& ar); // Implementation public: virtual ~CTkRecord(); }; Az Objektum a CObject-ból van származtatva. A 3 adatmező hivatott a kezelendő adatok tárolásában. A paraméterek nélküli konstruktor és a DECLARE SERIAL makró a későbbi lementéshez kell. Az igazi konstrukornak meg tudjuk adni az adatmezők értékét. A Serialize függvény lehet érdekes: void CTkRecord::Serialize(CArchive& ar) { if (ar.IsStoring()) { ar << Name; ar << Address; ar << Number; } else { ar >> Name; ar >> Address; ar >> Number; } } Ez a függvény végzi az objektum állapotának elmentését. Paraméterként megkapja az Archive-ot Az első szembeötlő dolog a kettős kacsacsőr. Ezt a speciális operátort csak a CArchive class-nál

használhatjuk. A << jelenti az Archive-ba való írást, a >> jelenti az Archive-ból való olvasást. Akár lementjük az Objektum állapotát, akár visszatöltjük, ez a függvény hívódik meg. Hogy éppen írni kell-e, vagy olvasni, azt az Archive IsStoring() és IsLoading() függvényeivel tudjuk meg. Értelemszerűen az első az írást, a második az olvasást jelzi. A Document Class-ban ilyen objektumokat fogunk kezelni. Tulajdonképpen egy ilyen objektum rekordként is felfogható, hiszen az adatmezői hordozzák a fontos információt. A Document Class- 42 ban ezekből az objektumokból tömböt képezve kényelmesen kezelhetőek az adatok. CTypedPtrArray<CObArray,CTkRecord*> m RecordList; A CTypedPtrArray csak egy segéd Class. CObArray tömböt csinál, melynek CTkRecord típusú elemei lesznek. Abban különbözik, a sima megadástól, CObArray CTkRecord hogy így jobb hibakezelést kapunk, biztonságosabb. A Serialize függvény: void

CTkDoc::Serialize(CArchive& ar) { m RecordList.Serialize(ar); } Mivel már a recordot tartalmazó objektumban megoldottuk a mentést, visszatöltést, csak ezt kell meghívnunk. Ezzel a mentés, visszatöltés már le is van tudva De azért akadnak egyéb problémák. A View és a Doc közötti adatcsere A Document Class oldaláról mikor módosul a kiírandó adat (pl. rányomunk hogy következő), mindig meg kell hívni az UpdateAllViews függvényt. De csak azután, ha már lekérdeztük a View-ban módosított adatokat void UpdateAllViews( CView* pSender, LPARAM lHint = 0L, CObject pHint = NULL ); Paraméterként meg kell adni a View-ot. NULL, ha az összeset Ezenkívül a módosítás típusát is meg lehet adni, és egy objektumot, ami leírja, de ez már nem kötelező. void CTkDoc::OnNext() { WriteBack(); ActualIndex++; ActualRecord=m RecordList[ActualIndex]; CDocument::UpdateAllViews(NULL); } void CTkDoc::WriteBack() { CView* pView; POSITION pos = GetFirstViewPosition();

while (pos != NULL) { pView = GetNextView(pos); CTkView* pTkView = DYNAMIC DOWNCAST(CTkView, pView); if (pTkView != NULL) pTkView->WriteBack(); } } A WriteBack hívja meg a View azonos nevű függvényét, amely visszaírja az adatokat a Document ActulRecord-jába. A dolog nehézsége, hogy a View-ot nem lehet csak úgy meghívni, meg kell találni Esetünkben egy egyszerűbb függvény is megtette volna, mivel csak egy View van ,de sosem lehet tudni Először le kell kérdezni az első View pozícióját, a GetFirstViewPosition függvényel. Majd ezt követően megkapjuk a View-ra mutató pointert, a GetNextView függvénnyel. Ha ez megvan, csak át kell Cast-olni a CView-ra mutató pointert CTkView-ra mutatóra. Ez a DYNAMIC DOWNCAST 43 makróval történik. Ezután már meg is tudjuk hívni az ominózus függvényt Ahol void CTkView::WriteBack() { UpdateData(TRUE); CTkDoc* doc=GetDocument(); doc->ActualRecord->Address=m address; doc->ActualRecord->Number=m

number; doc->ActualRecord->Name=m name; } Az UpateData függvény behúzza a beírt értékeket a változókba. (Meghívódik a DoDataExchange) Ha TRUE-t adunk paraméterként bekéri az adatokat, ha FALSE, akkor írja. A GetDocument függvénnyel megkapjuk a Doc címét, és beírhatjuk a változóiba a beírt adatokat. Visszafele az adatcsere az UpDateAllViews meghívása után: void CTkView::OnUpdate(CView* pSender, LPARAM lHint, CObject pHint) { CTkDoc* doc=GetDocument(); m address=doc->ActualRecord->Address; m name=doc->ActualRecord->Name; m number=doc->ActualRecord->Number; UpdateData(FALSE); Invalidate(); } Lekérjük a GetDocument()-el a Doc címét, amivel elérjük a Document változóit. Kivesszük ami kell, majd az UpdateData függvénnyel beíródik a Controllok-ba. Az Invalidate hatására újrarajzolódik a frame a már új adatokkal. Ennyi Lenne. INI file kezelés és MDI Document-View kapcsolat Kezdjük az egyszerűbbel Az INI file-ok

programunk beállításainak tárolására szolgálnak. Az állomány közönséges ASCII szövegfájl, ennek megfelelően elérése egyszerű. A Windows 31-ig közvetlenül is elérhettük programunkból a saját INI állományunkat, azonban különböző adatvédelmi okokból ezt már a Windows 95, 98 és NT nem engedi. A hozzáférést a win32s API függvényei biztosítják számunkra Egy INI file felépítése: [IniFileSection1] IniFileEntry1=valami IniFileEntry2=akarmi . . [IniFileSection2] IniFileEntry1=valami IniFileEntry2=akarmi 44 . . És így tovább. Tehát az INI file Section-okból (fejezetekből) áll, a section neve van szögletes zárójellel bezárva. Minden fejezet Entry-k sorozatát tartalmazza Az Entry nevét egyenlőségjel követi, és a hozzá eltárolt érték (Profile String). Az INI file-t nem kell létrehozni, az INI file-ba való íráskor, ha nem létezik a file, létrehozza helyettünk a framework (programkeret). A kezelést végző MFC

függvények: CWinApp::LoadStdProfileSettings void LoadStdProfileSettings( UINT nMaxMRU = AFX MRU COUNT ); Az InitInstance-ból kell hívni. A framework az általa automatikusan az INI file-ba elmentett értékeket engedélyezi, és tölti be. Ilyen tipikusan a Recent File List (előzőleg használt file-ok listája). Az nMaxMRU paraméter a betöltésre és tárolásra engedélyezett filenevek száma. Ha nullát adunk meg paraméterként, le tudjuk tiltani az egészet. CWinApp::GetProfileString CString GetProfileString( LPCTSTR lpszSection, LPCTSTR lpszEntry, LPCTSTR lpszDefault = NULL ); A függvény az elmentett string-et olvassa be, és visszaadja visszatérési értékként. Paraméterként a Section és az Entry nevét kell megadni. Ha a függvény nem találja az értéket, lpszDefault -ot ad vissza. CWinApp::GetProfileInt UINT GetProfileInt( LPCTSTR lpszSection, LPCTSTR lpszEntry, int nDefault ); Ugyanaz, mint az előző függvény, csak most egy stringként beírt számot

integer-ként olvasunk be. CWinApp::WriteProfileString BOOL WriteProfileString( LPCTSTR lpszSection, LPCTSTR lpszEntry, LPCTSTR lpszValue ); A paraméterként megadott Section és Entry által meghatározott helyre beírja az lpszValue-t. Ha az Entry, vagy a Section nem létezik, akkor létrehozza. Ha nem sikerült beírni a visszatérési érték nulla, egyébként nem nulla. Egyszerű példa: CString INIFileSection="Last used file"; CString INIFileEntry="File"; void CTkApp::PutDocFileToINI(LPCTSTR DocFile) { WriteProfileString(INIFileSection,INIFileEntry,DocFile); } CString CTkApp::GetDocFileFromINI() { return GetProfileString(INIFileSection,INIFileEntry); } 45 MDI Document-View kapcsolat Az SDI Document-View kapcsolathoz hasonlóan itt is Document Template-el rendeljük össze a dokumentumot a View-al. Azzal a különbséggel, hogy itt egyszerre több template-ünk lesz A template-eket az application class kezeli és tartja nyilván. A saját kis listáján

mindegyik template-re mutat egy pointer. Az alkalmazás annyi template-et kezel, ahány féle dokumentumot kezel Egy template egy dokumentumtípust határoz meg. Minden Template-ből tetszőleges számú dokumentum lehet nyitva. Egy példa template létrehozására: formTemplate = new CMultiDocTemplate( IDR FORMFRAME, RUNTIME CLASS(CTkDoc), RUNTIME CLASS(CTkFrame), RUNTIME CLASS(CTkView)); AddDocTemplate(formTemplate); A CmultiDocTemplate objektumot hozzuk dinamikusan létre. A konstruktornak a Frame Id-jét, a Document, Frame, és View classokat kell megadni. Ha nincs szükségünk rá, hogy külön Frame window-t származtassunk, CMDIChildFrame-et is megadhatunk a frame class-hoz. A Frame window ID-jére azért van szükség, mert a string táblában hozzá rendelt érték adja meg a megnyitandó dokumentum típusát, az ablak címét, a file kiterjesztését, stb. A template bejegyzése az AddDocTemplate függvénnyel történik. Természetesen az egészet az application

InitInstance függvényében kell megtennünk. Íme egy ábra, hogy összezavarjon mindenkit: Ennyi félrevezetés után foglaljuk össze, ki kivel van:   A document Class listát vezet a saját View-okról, és egy pointere van ami az őt létrehozó template-re mutat. A View a frame window child-ja, és van egy pointere a hozzá rendelt document-hez. És nyílván a frame window is tudja, hogy melyik View van benne. Ezen kívül a Document template számontartja az általa létrehozott dokumentumokat, ahogy az alkalmazás is a template-ket. Igazán egyszerű. Ahogy az lenni szokott, hiába ismerik egymást a dokumentumkezelésben résztvevő Class-ok, akárki akárkinek mégsem küldözgethet akárhogy. Ahogy azt az Sdi-dokumentumkezelésnél is tapasztaltuk 46 Egy kis összefoglalás: Adatcsere lehetősége a CDocument Class-ból:   A GetFirstViewPosition és GetNextView függvényekkel elérhetjük a View-okat, és meghívhatjuk, amit szabad. A GetDocTemplate

függvénnyel megtudjuk a létrehozó template-et. A View- ból: A GetDocument függvénnyel megkapjuk a Document-et. Ezzel elérjük az összes adatát, amiből könnyen frissíteni tudjuk a képet. Ha a Frame window-ra is szükségünk lenne, azt a GetParentFrameel kapjuk meg A frame window-ból A View-ot a GetActiveView-al kapjuk meg, az aktív dokumentumot, meg a GetActiveDocument –el. MDI frame window-ból: Az éppen aktív child window-t a MDIGetActive függvénnyel kapjuk. Ha ennek fényében megírjuk a View és Document Class-okat, még nem végeztünk mindennel. Ha a template már be van jegyezve, a dokumentumokat és a hozzá tartozó View-okat már csak létre kell hozni. Ha a dokumentumhoz csak egy View van rendelve, a View-ot a framework magától létrehozza. Azonban, ha több View használja ugyanazt az dokumentumot, csak az elsőt fogja létrehozni, a többiről magunknak kell gondoskodni. Legpraktikusabb az application OpenDocument függvényében. CDocument*

CTkApp::OpenDocumentFile(LPCTSTR lpszFileName) { CTkDoc* pDoc = (CTkDoc)CWinApp::OpenDocumentFile(lpszFileName); if (pDoc == NULL) return NULL; CFrameWnd* pNewFrame = listTemplate->CreateNewFrame(pDoc, NULL); if (pNewFrame == NULL) return pDoc; listTemplate->InitialUpdateFrame(pNewFrame, pDoc); ASSERT KINDOF(CMDIChildWnd, pNewFrame); CMDIFrameWnd* pMDIFrameWnd = ((CMDIChildWnd)pNewFrame)->GetMDIFrame(); ASSERT(pMDIFrameWnd != NULL); pMDIFrameWnd->MDITile(MDITILE HORIZONTAL); return pDoc; } Az eredeti függvényt persze meg kell hívni, ami visszaadja a Document-re mutató pointert. Ezek után a template segítségével meg kell nyitni a Frame windowt. A CreatNewFrame hatására a template hozza létre a frame window-t. Az InitialUpdateFrame kivált egy InitialUpdate üzenetet a frame számára, és az OnInitialUpdate függvény fog lefutni, ami a mi initalizációs kódunkat tartalmazza. Az ablak tulajdonképpen már kint van, de ha méretezni akarjuk, meg kell tudnunk az

MDI Chlid window címét. Ez a GetMDIFrame függvénnyel lehetséges Az MDI ablak MDITitle függvénye végzi a képernyőn való rendezést. A Document Class szempontjából szinte teljesen mindegy, hány View használja, ezért ott nem is kell változtatni. 47 A Beillesztett új View is nagyon egyszerű. Ha adatbekérésre nem használjuk, a Dokumentum-na nem kell állandó kapcsolatot tartania a View-al, elég, ha a View látja a Dokumentum adatait. Ennek megfelelően csak a Dokumentum által küldött Update üzenetre kell új handlert írni. void CTkListView::OnUpdate(CView* pSender, LPARAM lHint, CObject pHint) { Invalidate(); } Nagyon egyszerű. Az Invalidate hatására Draw üzenet generálódik, és meghívódik az OnDraw, ahol a kiírást végezzük. void CTkListView::OnDraw(CDC* pDC) { CTkDoc* pDoc = (CTkDoc)GetDocument(); TEXTMETRIC tm; pDC->GetTextMetrics(&tm); int CharWidth = tm.tmAveCharWidth; int CharHeight = tm.tmHeight; CRect Area; GetClientRect(Area);

pDC->TextOut(Area.Width()/2-10*CharWidth, Area.Height()/2-CharHeight, T("Név : ")+pDoc->ActualRecord->Name); pDC->TextOut(Area.Width()/2-10*CharWidth, Area.Height()/2, T("Cím : ")+pDoc->ActualRecord->Address); pDC->TextOut(Area.Width()/2-10*CharWidth, Area.Height()/2+CharHeight, T("Szám : ")+pDoc->ActualRecord->Number); } Be kell kérni a karakterek magasságát, és szélességét, hiszen a kiíratásnál egységkoordinátákat kell megadni, nem sort és oszlopot. Ezután tulajdonképpen készen is vagyunk. Statusbar, Tool Bar, Docking Tool Bar, ToolTip Egy pár olyan dologról lesz szó, ami szinte mindegyik windows programnak része, mégis szinte észre sem vesszük őket, csak akkor, ha mégsincsenek. Az Eddigi példaprogramok is mind használták ezeket, de mégsem ejtettem szót róluk. Talán azért, mert az AppWizard egy részüket magától létrehozza, kezelésükkel nem sok gond van. A Statusbar A Statusbar

az ablak legalsó sávjában helyezkedik el. Egy sor, amely un Output panelekből áll Itt jelezzük a Num Lock, Scroll Lock, és egyéb billentyűk állapotát, de ebben a sorban jelenik meg 48 például a menüpontokhoz rendelt rövid help is. MFC-ben a CStatusBar class kezeli. Az alkalmazásunkban a MainFrame Class-ban jelenik meg Ha használunk StatusBar-t, először egy CStatusBar típusú változót kell deklarálnunk a class-ban. Ha ez megvan, a CMainFrame OnCreate függvényéből kell létrehozni a Statusbart, és beállítani a panelek szövegét, számát, stb. Ezek után a dolog magától működik, ha nem akarunk extrákat, nem is kell törődni vele. A framework a paneleket, és azok szövegét (Indicators) egy tömbben tárolja. Az elemek sorrendjük szerint kerülnek ki a Statusbar-ra balról jobbra haladva Alapértelmezés szerint a legelső elem mérete nincs meghatározva, elasztikus. Ez a többi indicator által nem használt szabad területet foglalja el

Tehát a panelek jobbra lesznek igazítva, mivel baloldalon ez lefoglalja a szabad helyet. Ha átraknánk az utolsó helyre, akkor természetesen minden balra lenne igazítva. Létrehozáskor egy ilyen tömböt adunk meg paraméterként, az elemek String ID-k. Az MFC a következő ID-ket tudja, és kezeli is magától.     ID INDICATOR CAPS : CAP lock indicator ID INDICATOR NUM : NUM lock indicator ID INDICATOR SCRL : SCRL lock indicator ID INDICATOR KANA : KANA lock indicator (Csak Japán rendszereken. Hogy mi az pontosan, nem tudom, de izgatja a fantáziám) Ezeket az Indicatorokat a rendszer magától kivillantja, ha kell. A megvalósításuk a CFrameWnd::OnUpdateKeyIndicator függvényben van. Ha ettől különböző Id-ket használunk, azoknak elrejtését és kivillantását magunknak kell csinálni. A CSstatusBar függvényei: A StatusBar létrehozása: CStatusBar::Create BOOL Create( CWnd* pParentWnd, DWORD dwStyle = WS CHILD | WS VISIBLE | CBRS BOTTOM, UINT nID

= AFX IDW STATUS BAR ); Paraméterként elég csupán a Mainframe címét megadni, hiszen a többi paraméternek van alapértelemzése, és csak ezeknek az értékeknek van értelmük. Nem hiszem hogy valaki például láthatatlan statusbar-t szeretne, vagy olyat, ami a frame tetejére van igazítva. A visszatérési értéke nem nulla, ha sikeres volt Az Inicator-ok szövegének beállítása: CStatusBar::SetIndicators BOOL SetIndicators( const UINT* lpIDArray, int nIDCount ); Paraméterként az ID-ket tartalmazó tömb címét, és az elemek számát kell megadni. A függvény kiszedi a String táblából az ID-hez tartozó értéket, és arra állítja be az inidicator szövegét. A nulladik elem átírása: 49 CWnd::SetWindowText void SetWindowText( LPCTSTR lpszString ); A paraméterként megadott pointer egy C típusú string-re mutat, erre írja át a 0. Inidcator értékét. Ez általában az üres bal oldali sáv. Tetszőleges elem átírása: CStatusBar::SetPaneText BOOL

SetPaneText( int nIndex, LPCTSTR lpszNewText, BOOL bUpdate = TRUE); A paraméterként megadott számú indexet átírja a megadott névre. A harmadik paraméter alapértelmezés szerint TRUE, tehát a panel újraíródik. Egy egyszerű példa a StatusBar létrehozására: static UINT indicators[] = { ID SEPARATOR, // status line indicator ID INDICATOR CAPS, ID INDICATOR NUM, ID INDICATOR SCRL, }; int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { m wndStatusBar.Create(this); m wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT)); . . . A ToolBar Az ablak felső sávjában helyezkedik el, apró ikonok (Bitmap-ek) sora, melyek lenyomása egy-egy parancsot helyettesít. Az ikonok működhetnek egyszerű pushbutton-ként, radio button-ként, vagy checkbox-ként. Az MFC ToolBar átmozgatható, a Parent window bármelyik oldalához “odaragasztható” (docking ToolBar), vagy bemozgatható az ablak belsejébe, ekkor “lebegő” (floating) toolbarrról

beszélünk. Ilyenkor egy saját kis Frame Window-t nyit magának, amley átméretezhető és bezárható. A ToolBar ikonjaihoz ugyanúgy lehet ID-t és kezelőfüggvényeket rendelni, mint a menüpontokhoz. Sokszor ugyanazt az ID-t adjuk meg, mint egy menüpontnál, így ugyanaz a handler fut le, mint a menü hatására. A ToolBar ikonaihaoz rendelhetük úgynevezett ToolTip-eket Ezek azok a kis feliratok, amik megjelennek, ha sokat ácsorgunk egy ikon felett. A ToolBar létrehozása, és szerkesztése nagyon egyszerűen a ToolBar editorral történik. Ha ToolTipet is szeretnénk, a Prompthoz a szöveg után /n-el elválasztva írhatjuk a tooltip szöveget A ToolBart a CToolBar Class kezeli. A Függvényei: 50 Egy ToolBart kétféleképpen is elkészíthetünk:     Egy ToolBar Resource-t készítünk a Toolar Editorral Egy CToolBar Class típusú objektum létrehozása A ToolBar létrehozása a Create függvénnyel A LoadToolBar függvénnyel betöltjük a ToolBar

resurce-t. Pl.: int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { m wndToolBar.Create(this); m wndToolBar.LoadToolBar(IDR MAINFRAME) . . Vagy:     Egy Bitmap Resource elkészítése, amely a toolbar képét tárolja. Egy CToolBar típusú objektum létrehozása A Bitmap betöltése a LoadBitmap függvénnyel A tulajdonságok beállítása a SetButtons függvénnyel. Pl.: Az ID-ket a String táblában definiálni kell. A hozzájuk rendelt érték a Status Bar-on megjelenő Prompt, és -el elválasztva a ToolTip szöveg. static UINT BASED CODE MainButtons[] = { ID FILE NEW, ID FILE OPEN, ID FILE SAVE, ID SEPARATOR, ID EDIT CUT, ID EDIT COPY, ID EDIT PASTE, }; int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { m wndMainBar.Create(this, WS CHILD | WS VISIBLE; IDW MAIN BAR); m wndMainBar.LoadBitmap(IDR MAINBAR); m wndMainBar.SetButtons(MainButtons, sizeof(MainButtons)/sizeof(UINT)); . . . Ez ilyen rövidre sikerült NÉZETEK A view (nézet) megvalósítása

51 A view az MFC programnak az a része, amely kezeli a nézetablakot, feldolgozza a felhasználói inputot, és megjeleníti a dokumentum adatait a nézetablakban, és minden más eszközön (nyomtató). Az összes nézetosztály a CView osztályból van származtatva. Nézeteket már eddig is használtunk, az AppWizard is készít nekünk egyet, igaz ez kevés kivételtől eltekintve nem sok mindent csinál, azért a vázat megadja. A legegyszerűbb módja az, amikor egyszerűen leszármaztatjuk egy már működő nézet osztályból a sajátunkat. Ezt már az AppWizard-ból is megtehetjük Ilyen például a CEditView osztály A belőle származtatott nézet már egy egyszerű szövegszerkesztő. Általában ilyen kész osztályt nem találunk a megoldandó feladat megoldására, nekünk kell a nézetet megvalósítani. Ilyenkor a nézetosztályunkat valamelyik általánosabb célú MFC nézetosztályból származtatjuk, és hozzáírjuk amit kell. Egy ilyen általánosan

használt nézetosztály a CScrollView, aminek segítségével gördíthető nézeteket készíthetünk. (A példaprogramban a CTkListView egy ilyen nézet) A nézetek fontosabb függvényei, melyeket általában felül kell definiálnunk:        A Konstruktor: Itt lehet beállítani a változók kezdeti értékét, egérkurzort betölteni stb. PreCreateWindow: az ablak létrehozása előtt fut le, az ablak stílusát lehet beállítani. Alapértelmezett egérkurzor beállítása, háttér. OnInitialUpdate: A nézet első megrajzolásánál fut le. Itt tudjuk lekérdezni a kirajzoláshoz majd szükséges adatokat. Pl karaktermagasság, sorok száma OnDraw: Ez a függvény fut le, akárhányszor újra kell rajzolni a nézetet. Tehát itt kell megvalósítani a nézet megrajzolását. Paraméterként megkapunk egy CDC (Device Context) osztály pointerét, ezzel tudunk rajzolni. OnUpdate: CDocument::UpdateAllViews függvény hatására minden nézet OnUpdate

függvénye meghívódik. Itt kell gondoskodni a nézet újrarajzolásáról Azonban nem úgy, hogy csak nekiállunk és megrajzoljuk, hanem az invalidate függvényt meghívjuk, minek hatására majd lefut az OnDraw függvény. Ezen kívül lekérdezhetjük a dokumentum méretét, mert ha változott, újra kell állítani a ScrollBar-t. OnSize: A nézetablak méretváltoztatásakor hívódik meg, ha használunk gördítősávokat, azokat újra kell állítani. Innen nem kell az újrarajzolással törődni, hiszen az automatikusan lefut. A Destruktor: A lefoglalt memóriaterületeket fel kell szabadítani. Egyszerűbb nézeteket már eddig is használtunk, most a gördíthető nézetekkel foglalkozunk. Gördíthető nézetek megvalósítása: A Gördítés a függőleges és vízszintes gördítősávok segítségével történik. A gördítés azon alapszik, hogy a nézetablakban nem az egész nézet jelenik meg, hanem csak az a része, ami belefér az ablakba. Ez azt jelenti,

nekünk a nézet kirajzolásánál nem kell törődnünk azzal, hogy amit rajzolunk az éppen kint van-e a képernyőn, vagy sem, ez már a gördítés dolga. A gördítés lényege, hogy megváltoztatjuk a nézőpont koordinátákat. Gördítés nélkül a nézőpont mindig a rajz bal felső sarka Az eszközkoordináták: a Client Area beli koordináták, tehát az ablakon belüli koordináták. A logikai koordináták: az egész nézeten belüli koordináták. Egy pár összezavaró ábra: A gördítés előtt: 52 A gördítés után: Tehát gördítés közben az eszközkoordinátákat mindig át kell konvertálni logikaivá. Ha például egy egéreseményt akarunk kezelni, csak az esemény eszközkoordinátáit (ablakon belüli) fogjuk megkapni. A ClassWizard-dal könnyen tudunk kezelőfüggvényt rendelni pl a bal egérgomb lenyomásához. A WM LBUTTONDOWN üzenethez rendelt kezelőfüggvény: 53 void CTkListView::OnLButtonDown(UINT nFlags, CPoint point) { CClientDC

ClientDC(this); OnPrepareDC(&ClientDC); //elokeszites ClientDC.DPtoLP(&point); //Device point -> Logical point int Line=point.y/m LineHeight; if (Line>=0 && Line<m LineNum) { ChangeSelection(Line); //Erre a sorra nyomtunk CTkDoc* pDoc = (CTkDoc)GetDocument(); pDoc->SetActualRecord(Line); pDoc->UpdateAllViews(this); //Mindenki ujrarajzolodik, kiveve a kuldo (en) } CScrollView::OnLButtonDown(nFlags, point); } A konvertálás egy CDC (Eszközkörnyezet objektum) segítségével történik. Mint tudjuk ez az objektum kapcsolódik ahhoz az eszközhöz, ahova rajzolni/írni akarunk. Esetünkben az ablak Client Area-jához. Ennek létrehozása után a CView::OnPrepare függvényét kell meghívni, ami az eszközkörnyezetet előkészíti. Beállítja a nézőpontot Ha ez megvan, az eszközkörnyezet DptoLP függvénye segítségével át tudjuk konvertálni a megkapott eszközkoordinátát logikai koordinátává. Ezután már a koordináták nem az ablakra,

hanem a nézetre fognak vonatkozni. A Függvény nem csinál mást, mint kiszámolja melyik sorra nyomtunk a listában, majd ezt a rekordot teszi aktuálissá a dokumentumban. Az UpdateAllViews hatására a másik nézetben is az aktuális rekord lesz látható. Ha a nézeten belül bárhol máshol eszközkörnyezetet használunk, azt is az OnPrepare függvénnyel kell előkészíteni, különben nem tudja, hogy mennyit gördítettünk. Ahhoz, hogy egy ablak gördíthető legyen, először is be kell állítani a paramétereket. Ezt a CScrollView::SetScrollSizes függvény végzi el. void SetScrollSizes( int nMapMode, SIZE sizeTotal, const SIZE& sizePage = sizeDefault, const SIZE& sizeLine = sizeDefault ); A függvény első paramétere a leképezési mód. Az a módszer, ahogy a logikai és eszközkoordináták közti konvertálás megtörténik. Elég sok módszer közül lehet választani A repertoár: Leképzési mód Logikai egység Y merre nő. MM TEXT 1 pixel

Lefelé MM HIMETRIC 0.01 mm Felfelé MM TWIPS 1/1440 in Felfelé MM HIENGLISH 0.001 in Felfelé MM LOMETRIC Felfelé 0.1 mm MM LOENGLISH 0.01 in Felfelé A legsűrűbben az MM TEXT leképezést (Mapping mode) használjuk, hiszen ez a legegyszerűbb. Az egység 1 pixel, és a koordinátarendszer Balról jobbra, és felülről lefelé nő. A második paraméter a nézet teljes mérete logikai egységekben. Ez természetesen a dokumentum méretétől függ. 54 A többi paraméter megadása nem kötelező, de úgyis megadjuk. Az oldal és sorméret. Gördítésnél a le, vagy felfelé nyilak megnyomásánál egy sorral lejjebb ill feljebb megy a nézet. A gomb és a nyíl közé a gördítősávon belül nyomva egy oldalt gördítünk Alapértelmezés szerint az oldalméret az egész méret tizede, a sorméret pedig az oldalméret tizede. Ezeket a méreteket CSize class-ként, vagy SIZE struktúraként kell megadni. Egy példa: void CTkListView::SetScroll() { CSize

DocSize(m ChrWidth*LINELENGTH, m LineNum*m LineHeight); CSize RowSize(m ChrWidth,m LineHeight); m LinePerPage=m WindSize.cy/m LineHeight-1; CSize PgSize(m ChrWidth*20,m LinePerPagem LineHeight); SetScrollSizes(MM TEXT,DocSize,PgSize,RowSize); } Ezt persze ahányszor változik a dokumentum, vagy az ablak mérete, mindig meg kell hívni, hogy újra beállítsa az értékeket a dokumentum változásakor az OnUpdate függvény fut le, az ablak újraméretezésénél pedig az OnSize. Ezekből a függvényekből ezért meg kell hívni ezt a SetScroll függvényt. Gördítés billentyűzetről: Alapértelmezés szerint a gördítősáv csak az egérrel vezérelhető. Ha a kurzormozgató nyilakat, és a PgUP, PgDown stb. billentyűkhöz saját kezelőt kell írnunk ClassWizard-ból rendeljünk kezelőfüggvényt a WM KEYDOWN üzenethez. Akárhányszor lenyomunk egy nem rendszerbillentyűt, ide adódik a vezérlés. Rendszerbillentyűnek számít az ALT, PintScreen, és bármely ALT-al együtt

lenyomott billentyű. A rendszerbillentyűk WM SYSKEYDOWN üzenetet váltanak ki. Ha olyan billentyűt nyomunk le, aminek van ASCII kódja, egyszerűbb a WM CHAR üzenetet kezelni, mert itt saját kódjukkal hivatkozhatunk a billentyűkre. A WM KEYDOWN üzenet kezelésekor minden gombnak saját kódja van. Azoknak a billentyűknek a kódja, amik nem váltanak ki WM CHAR üzenetet: Érték konstans Billentyű 12 VK CLEAR Numerikus 5 (Ha nincs NumLock) 16 VK SHIFT Shift 17 VK CONTROL Ctrl 19 VK PAUSE Pause 20 VK CAPITAL CapsLock 33 VK PRIOR PgUp 34 VK NEXT PgDown 35 VK END End 36 VK HOME Home 55 37 VK LEFT ¬ 38 VK UP 39 VK RIGHT ® 40 VK DOWN Ż 45 VK INSERT Insert 46 VK DELETE Delete 112 VK F1 F1 . . . . . . . . . 123 VK F12 F12 144 VK NUMLOCK NumLock 145 VK SCROLL Scrollock Az üzenetkezelő paraméterként megkapja a leütött billentyű kódját, az ismétlésszámot, és egy Flaget, ami az jelzi, lenyomtuk,

vagy felengedtük, mi a scan kódja stb. Az üzenetkezelő: void CTkListView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { // TODO: Add your message handler code here and/or call default CTkDoc* pDoc = (CTkDoc)GetDocument(); switch (nChar) { case VK HOME: SendMessage(WM VSCROLL,SB TOP); ChangeSelection(0); pDoc->SetActualRecord(0); pDoc->UpdateAllViews(this); //Mindenki ujrarajzolodik, kiveve a (en) break; case VK END: SendMessage(WM VSCROLL,SB BOTTOM); ChangeSelection(m LineNum-1); pDoc->SetActualRecord(m LineNum-1); pDoc->UpdateAllViews(this); //Mindenki ujrarajzolodik, kiveve a (en) break; case VK NEXT: SendMessage(WM VSCROLL,SB PAGEDOWN); ChangeSelection(min(m LineNum-1,m SelectedLine+m LinePerPage)); pDoc->SetActualRecord(m SelectedLine); pDoc->UpdateAllViews(this); //Mindenki ujrarajzolodik, kiveve a (en) break; case VK PRIOR: SendMessage(WM VSCROLL,SB PAGEUP); ChangeSelection(max(0,m SelectedLine-m LinePerPage)); pDoc->SetActualRecord(m SelectedLine);

pDoc->UpdateAllViews(this); //Mindenki ujrarajzolodik, kiveve a (en) break; 56 kuldo kuldo kuldo kuldo case VK DOWN: ChangeSelection(min(m LineNum-1,m SelectedLine+1)); pDoc->SetActualRecord(m SelectedLine); pDoc->UpdateAllViews(this); //Mindenki ujrarajzolodik, kiveve a kuldo (en) break; case VK UP: ChangeSelection(max(0,m SelectedLine-1)); pDoc->SetActualRecord(m SelectedLine); pDoc->UpdateAllViews(this); //Mindenki ujrarajzolodik, kiveve a kuldo (en) break; } CScrollView::OnKeyDown(nChar, nRepCnt, nFlags); } A dolog kulcsa a CWnd::SendMessage függvény. Ezzel a függvénnyel tudunk küldeni egy üzenetet a hozzá tartozó kóddal. Gördítősávok lévén kétféle üzenet lehetséges, a WM VSCROLL, a függőleges sáv, és WM HSCROLL, a vízszintes sáv. A Hozzá tartozó kódok: A kezelőfüggvény a SendMessage függvény segítségével tulajdonképpen ugyanazt az üzenetet küldi el, amit az ábrán jelölt helyeken végzett egérgomb lenyomás okozna.

A függvény a gördítés mellett természetesen a kiválasztott sort is módosítja, és ezzel az aktuális rekordot. Ehhez a dokumentum általunk megírt függvényét használja Az UpdateAllViews után pedig a másik nézetben is ugyanazt az adatot fogjuk látni, amit kiválasztottunk. A kiválasztott sor megváltoztatását a ChangeSelection függvény végzi. Abból a szempontból talán érdekes lehet, hogy bemutatja, hogyan kell lekérdezni az aktuális gördítési pozíciót. void CTkListView::ChangeSelection(int Line) { CClientDC ClientDC(this); OnPrepareDC(&ClientDC); //elokeszites 57 if (m SelectedLine!=Line && Line>=0 && Line<m LineNum) { DrawLine(&ClientDC,m SelectedLine,0); //regi torlese CPoint pos(GetDeviceScrollPosition()); //Lekerdezzuk a Scroll koordinatakat if (Line*m LineHeight<pos.y) //Fent kilog { pos.y=Line*m LineHeight; ScrollToPosition(pos); } else if (Line*m LineHeight+m LineHeight>pos.y+m WindSizecy) //Lent Kilog {

pos.y=(Line-m LinePerPage)*m LineHeight; ScrollToPosition(pos); } OnPrepareDC(&ClientDC); //elokeszites DrawLine(&ClientDC,Line,1); m SelectedLine=Line; } } A függvény az éppen kiválasztott sort újrarajzolja (így nem lesz rajta a “csík”), az újonnan kiválasztottat pedig újrarajzolja kiválasztva a DrawLine függvénnyel. Természetesen le kell kérdeznünk az aktuális gördítési pozíciót, ennek segítségével tudjuk megállapítani, hogy a kiválasztott sor nem lóg-e ki a képről. Ekkor Odagördítjük az ablakot, ahol már látszik Az aktuális pozíció lekérdezése a GetDeviceScrollPosition függvénnyel történik. Visszatérési értéke egy CPoint osztály. Már csak egy részletkérdés van, a nézet újrarajzolásának optimalizálása. Eddig az OnUpdate függvényünk simán meghívta az eredeti OnUpdate-et, ami pedig egy sima Invalidate paranccsal lerendezi az egészet. Minek hatására az egész nézet újrarajzolódik A megoldás az

UpdateAllViews-nak megadható paraméterekben rejlik. Eddig csak az első paramétert adtuk meg, de lehetőségünk van még további egy int, és egy CObject-ből származtatott osztály átadására, amiben leírhatjuk, milyen változás történt a dokumentumon. Ha ezt megtesszük, az OnUpdate is meg fogja kapni ezeket a paramétereket, így ki tudja számolni, a nézet melyik részét kell újrarajzolni. Egy kicsit bénára sikerült ilyen útmutató (Hint) Class: class CMyHint : public CObject { public: CMyHint(int ActualIndex,BOOL isModified) { Index=ActualIndex; Modified=isModified; } int Index; BOOL Modified; }; Nem nagy durranás, de kezdetnek jó. Akárhányszor meghívjuk az UpdateAllViews függvényt, egy ilyen osztály címét átadjuk paraméterül. Ezt a címet majd megkapja az OnUpdate, és tudni fogja, mi a teendő. Íme: 58 void CTkDoc::OnNew() { SetActualRecord(NewRecord()); SetModifiedFlag(); CDocument::UpdateAllViews(NULL,0,&CMyHint(ActualIndex,TRUE)); }

Megadtuk a Hint Class-ban, hogy melyik rekordon matattunk, és hogy egyáltalán változott-e. Ennek fényében az OnUpdate feladata az újrarajzolandó terület megállapítása: void CTkListView::OnUpdate(CView* pSender, LPARAM lHint, CObject pHint) { CTkDoc* pDoc = (CTkDoc)GetDocument(); CRect InvalidRect; if (pHint!=NULL) { CClientDC ClientDC(this); OnPrepareDC(&ClientDC); if (((CMyHint *)pHint)->Modified && ((CMyHint )pHint)>Index==m SelectedLine) { CRect InvalidRect(COLOUMN1*m ChrWidth,m SelectedLinem LineHeight, (COLOUMN1+LINELENGTH)*m ChrWidth,(m SelectedLine+1)m LineHeight); ClientDC.LPtoDP(&InvalidRect); InvalidateRect(&InvalidRect); } else CScrollView::OnUpdate(pSender,lHint,pHint); } else CScrollView::OnUpdate(pSender,lHint,pHint); } Egy erősen leegyszerűsített forma. Annyit csinál, hogy ha az aktuális rekordot módosítottuk, akkor csak azt rajzoltatja újra. Kiszámoljuk a területet, majd logikai koordinátáról eszközkoordinátára

váltunk, mert ezt kell majd megadni. Egy adott terület kijelölése újrarajzolásra az InvalidateRect függvénnyel történik. Ha a pHint paraméter nulla (például legelső kirajzolásnál) akkor az eredeti OnUpdate függvényt kell meghívni, ami az egész ablakot újrarajzolja. Az OnDraw függvényben pedig megvalósítjuk, hogy csak egy területet rajzolunk újra: void CTkListView::OnDraw(CDC* pDC) { CTkDoc* pDoc = (CTkDoc)GetDocument(); CRect ClipRect; CRect IntRect; CRect LRect; ChangeSelection(pDoc->ActualIndex); OnPrepareDC(pDC); pDC->GetClipBox (&ClipRect); for(int Line=0; Line<m LineNum; Line++) { LRect.SetRect(COLOUMN1*m ChrWidth,Linem LineHeight, (COLOUMN1+LINELENGTH)*m ChrWidth,(Line+1)m LineHeight); 59 if (IntRect.IntersectRect(LRect,ClipRect)) DrawLine(pDC,Line,(Line==m SelectedLine)); } } Az érvénytelenített területet az eszközkörnyezet GetClipBox függvénye szolgáltatja, már logikai koordinátákban. Minden egyes sor szövegre

kiszámoljuk, hogy hol, mekkora területet foglal, és csak azokat írjuk ki újból, amik belelógnak az érvénytelenített területbe. Hogy két terület metszi-e egymást, azt a CRect::IntersectRect függvénye megadja. Tehát csak azokat a sorokat kell kirajzolni, amire ez a függvény igazat ad vissza. Na erről azt hiszem ennyit Eszköz-környezet Objektum létrehozása, Bitműveletek és bittérképek használata. A példaprogram egy tetris lett. Tetrist már mindenki látott. Négy blokkból álló figurákat kell egymásra ejteni Ha betelik egy sor, azt töröljük, ha nem tud több beesni, vesztettük. Nyerni meg nemigen lehet A megvalósítás egyszerű (mármint egy quake-hez képest). Arra azért vigyázzunk, hogy senki se írjon Tetris nevű Tetrist, mert az jogilag le vagyon védve. A tetrisprogramozás rejtelmeibe nem nagyon fogok belemélyedni, aki kicsit is tud programozni, van róla fogalma, hogyan kell megoldani a figurák forgatását, mozgatását,

eltárolását, sorok törlését stb Inkább az MFC vel történő grafikus kezelés és a bittérképek témakörében szeretnék segíteni. Az Eszköz-környezet Objektum (Device Context) Ahogy az már itt-ott előjött, az ablakra való rajzoláshoz, szövegkiíráshoz egy eszköz-környezet objektumra van szükségünk, amely az adott ablakhoz, vagy eszközhöz van rendelve, amely fogadja az outputot. Ez lehet akár egy nyomtató, vagy a képernyő, vagy csak egy ablak, vagy az ablak egy része. Az eszköz-környezet tartalmazza a kiválasztott rajzolóeszközöket, az attribútumokat, és a különböző alakzatok rajzolásához, és bittérképműveletekhez szükséges tagfüggvényeket. Ha egy nézetablakban (View) akarunk grafikát megjeleníteni, akkor annak az OnDraw függvényének átadott eszköz-környezettel tudjuk megtenni. Ha egy Dialógusablakban, akkor pedig az OnPaint függvényben kell újrarajzolni. void CTetrisWnd::OnPaint() { CPaintDC dc(this); // device

context for painting Eszköz-környezet objektumból több típusú is van, de mindegyik a CDC osztályból származtatott. 60     CPaintDC Csak az OnPaint függvényből használható, egy dialógusablak, vagy egy Controlnak az újrarajzolásakor. CClientDC Segítségével a nézetablakba (Client Area) rajzolhatunk CWindowDC Az ablak által használt egész területre rajzolhatunk. CMetafileDC Windows metaFile-ba rajzolhatunk. Eszköz-környezetek létrehozása: A CpaintDC, CClientDC és CWindowDC számára meg kell adni a CWnd osztály címét, ahonnan létrehoztuk. Ebből következik, hogy őket csak egy olyan osztályból tudjuk létrehozni, amely a CWnd ből származik. Ez nem okoz gondot, hiszen minden ablak osztály őse Pl. CClient dc(this); Ha az ablak görgethető, akkor az OnPrepareDC függvényit is meg kell hívni. De erről már volt szó Bittérképek és bitműveletek. A bittérkép egy kép pontos reprezentációja a memóriában, vagy egy fájlban.

Minden egyes pixel színét, tárolja, ez elég a megjelenítéshez. Ha MFC-ben bittérképet akarunk használni, a CBitmap osztály egy példányát kell használnunk. Ez az osztály biztosítja a bittérképek létrehozását, tárolását, betöltését. Egy példány létrehozása: CBitmap m bitmap; Egy bittérképet létrehozhatunk üresen, betölthetünk file-ból, vagy erőforrásból. Bittérkép betöltése Erőforrásból. Egy bittérkép erőforrásként történő kezelése azzal az előnnyel jár, hogy a fordító beleszerkeszti az adatot az .exe állományba, így nem fog külön bmp-ként megjelenni Az Insert/Resource menüpontnál, vagy a New gombra nyomunk, ekkor a Visual C++ szerkesztőjében tudjuk megrajzolni. Ha az Import gombot választjuk, egy bmp állomány-t rakhatunk be az erőforrások közé. Ez csak akkor jelenik meg a szerkesztőben, ha kevesebb, mint 256 színű Minden bittérképnek, ahogy más erőforrásnak is van azonosítója (ID-je). Ezt az ID-t

dupla kattintással a Resource View-ban be tudjuk állítani, és megjegyezni sem árt. Ha sikerült a bittérképet valahogy betuszkolni az erőforrások közé, a programból valahogy be is kell tölteni. m bitmap.LoadBitmap(IDB BITMAP); A LoadBitmap függvénynek átadott paraméter az az azonosító, melyet hozzárendeltünk a szerkesztőben. A bittérkép automatikusan kompatíbilis lesz az outputtal Üres bittérkép létrehozása Szerkesztőben való létrehozás helyett hozhatunk létre bittérképet a memóriában is a program futása közben. Ekkor a képet az MFC rajzolófüggvényeivel, és bittérképműveleteivel tudjuk létrehozni, miután eszköz-környezet objektumot rendeltünk hozzá. A bittérkép létrehozását a CBitmap CreateCompatibleBitmap tagfüggvénye végzi. 61 Pl.: m pWinDC = new CWindowDC(this); m Bitmap.CreateCompatibleBitmap(m pWinDC,sizecx,sizecy); CreateCompatibleBitmap függvény első paramétere egy eszköz-környezet objektum címe, ezzel

lesz kompatíbilis a bittérkép. Ez azt jelenti, hogy az adatok ugyanúgy lesznek tárolva, ahogy az outputot végző eszköz tárolja azokat. Például ha a monitor true color módban van 3 byte határozza meg egy pixel színét a képernyő-memóriában. Egy ezzel kompatíbilis bittérképen szintén 3 byte határozza meg a pixelt. A függvény második paramétere a bittérkép szélessége, a harmadik a magassága pixelekben A függvény egy memóriablokkot rendel a bittérképhez, melyen a pixelek színei definiálatlanok. A bittérképet a CDC rajzolófüggvényeivel tudjuk megrajzolni. Ahhoz viszont, hogy rajzolni tudjunk a bittérképre, létre kell hozni, és hozzá kell rendelni egy eszköz-környezet objektumhoz. A Windows különleges típusú eszköz-környezet objektumokat biztosít a bittérképek használatára, ezek a memória eszköz-környezetek. Egy ilyen létrehozásához a CDC osztály egy példányát kell definiálnunk, majd a CreateCompatibleDC függvényt

meghívva létrehozzuk. Pl.: m pMemDC = new CDC; m pMemDC->CreateCompatibleDC(m pWinDC) ; A CreateCompatibleDC függvénynek a paramétere egy eszköz-környezet objektum címe, a kapott eszköz-környezetünk avval az eszközzel lesz kompatíbilis, amely a paraméternek megadotthoz volt hozzárendelve. Így aztán az átadott eszköz-környezetnek ugyanahhoz az eszközhöz kell kapcsolódnia, amihez a CreateCompatibleBitmap-nek megadottnak. Ezt praktikusan úgy érjük el, hogy ugyanazt az eszköz-környezet objektum címet adjuk át mindkét függvénynek. Tehát esetünkben lett egy a képernyővel kompatíbilis eszköz-környezetünk. Ami ugye nincsen hozzárendelve semmihez sem. Még hozzá kell rendelni a bittérképhez. A CDC SelectObject tagfüggvényével Pl.: m pMemDC->SelectObject( &m bitmap); A függvénynek a bittérkép címét kell megadni paraméterként. Visszatérési értéke az eddig hozzárendelt bittérkép. Ha ezzel megvagyunk, már rajzolhatunk is a

bittérképre a memória eszköz-környezet objektumot használva. Ha bittérképet megsemmisítjük előtte a memória eszköz-környezetet is meg kell semmisíteni, vagy egy másik bittérképet hozzárendelni. Például a régit, amit visszaadott a SelectObject függvény Nézzük egyben az egészet: void CTetrisWnd::CreateBuffer() { CRect rect; CSize size; GetClientRect(&rect); size = rect.Size(); if(m pWinDC!=NULL) { delete m pWinDC; m pWinDC=NULL; } m pWinDC = new CWindowDC(this); if (m Buffer.m hObject != NULL) m Buffer.DeleteObject(); 62 m Buffer.CreateCompatibleBitmap(m pWinDC,sizecx,sizecy); if (m pMemDC!=NULL) { m pMemDC->SelectObject(m pOldBitmap); delete m pMemDC; m pMemDC = NULL; m pOldBitmap = NULL; } m pMemDC = new CDC; VERIFY( m pMemDC->CreateCompatibleDC(m pWinDC) ); m pOldBitmap = m pMemDC->SelectObject( &m Buffer ); m Size=size; m width=size.cx/m BlockWidth; m height=size.cy/m BlockHeight; } A függvény készít egy m Buffer nevű

bittérképet, és készít egy memória eszköz-környezetet, melyhez hozzárendeli a bittérképet. Arra is figyel, ha már ezek el vannak készítve, először megsemmisíti őket A létrehozott memória-eszközkörnyezet a programban Frame Buffer-ként fog funkcionálni. Bittérképek megjelenítése (másolása) Egy bittérkép megjelenítése tulajdonképpen csak másolás, eszköz-környezetek közötti adatcsere. Az adatok továbbítása eszköz-környezetek között a CDC osztály BitBlt tagfüggvényével történik. BOOL BitBlt(int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc, int ySrc, DWORD dwRop); Az első két paramétere a másolás céljának bal felső sarkának logikai koordinátái, a második kettő a bittérkép szélessége és magassága a következő a forrás eszköz-környezet objektum címe, aztán a másolandó bittérkép bal felső koordinátái, majd a raszterműveleti kód. A függvény az ötödik paraméterként megadott

eszköz-környezethez rendelt eszközről másol át egy adatblokkot arra az eszközre, amely ahhoz az eszközkörnyezethez volt rendelve, amelyre a függvényt meghívtuk. A raszterműveleti kódokat nem írom le, megtalálhatóak a help-ben. Platform SDK, Graphics and Multimedia Services, GDI, Raster Operation Codes Ami nekünk most kell az az SRCCOPY, amely változtatás nélküli másolást végez. Egy példa: void CTetrisWnd::ShowFrame() { m pWinDC->BitBlt( 0, 0, m Size.cx, m Sizecy, m pMemDC, 0, 0, SRCCOPY ); } A függvény a m pWinDC eszköz-környezethez rendelt ablakra másolja az m pMemDC eszközkörnyezethez rendelt bittérképet mindenféle változtatás nélkül. Röviden kirakjuk a Frame Buffer-ünket Némileg bonyolultabb a dolog, ha nincs eszköz-környezet objektum rendelve a bittérképhez. 63 Egy példa, ahol egy mátrixból felépítjük a képet a tetris blokkok másolásával. A blokkok egy bittérképen vannak, amihez nincs eszköz-környezet rendelve,

tehát kell egyet készítetnünk. void CTetrisWnd::BuildFrame() { int x; int y; BYTE c; CDC dc; dc.CreateCompatibleDC(NULL); dc.SelectObject(&m Blocks); m pMemDC->PatBlt(0,0,m Size.cx,m Sizecy,BLACKNESS); for(x=0; x<m width; x++) for(y=0;y<m height;y++) { if ((c=*(m matrix+ym width+x))!=0) { m pMemDC->BitBlt( x*m BlockWidth, y*m BlockHeight, m BlockWidth, m BlockHeight, &dc, Colors[c-1]*m BlockWidth, 0, SRCCOPY ); } } } Először létrehozunk egy memória eszköz-környezetet, és meghívjuk a CreateCompatibleDC függvényét. Ha a függvényt NULL paraméterrel hívjuk, a képernyővel kompatíbilis eszköz-környezetet készít. A SelectObject függvénnyel hozzárendeljük a bittérképet, majd a BitBlt alkalmazásával a m pMemDC-re felépítjük a képet a dc eszközkörnyezeten található blokkokból. A CDC osztály PatBlt tagfüggvényéről még nem esett szó. Evvel tudjuk feltölteni a bittérképet Az első két paraméter a feltöltendő terület bal

felső sarkának logikai koordinátái, a harmadik és negyedik a feltöltendő terület szélessége és magassága, az utolsó pedig a raszterműveleti kód. Esetünkben a BLACKNESS azt jelenti, hogy feketével töltjük fel a bittérképet. Ennyit a részletekről. A program dialógusablak alapú, ezért le kellett mondani a szokásos kurzormozgató billentyűkkel történő irányításról. Dialógusablak lévén, ezek a billentyűk a gombok közti mozgásra valók, és az MFC nem töri magát agyon, hogy tudassa velünk mikor melyik nyilat nyomták meg. Például nem jön létre WM KEYDOWN üzenet, így az OnKeyDown-ban nem tudnánk kezelni őket. Ezért lett ez a balkezes kiosztás. Az ablak, ahol a játék fut, a CStatic osztályból származtatott. Ebből volt a legegyszerűbb A ClassWizard-al össze kell rendelni a control-t az objektummal, így a control-t a mi Classunk fogja kezelni. Ennyi. 64 KÉPERNYŐVÉDŐ Először nézzük meg mi is egy képernyővédő

program. Első ránézésre is kiderül, hogy a kiterjesztése .scr, ami azt sugallja, hogy nem egy szabvány Windows-os .exe Ez persze nem igaz A felépítése teljesen ugyanaz, át is írhatnánk exe-re, és el is indulna, ha olyan parancs-sor argumentummal hívnánk, amilyennel a Windows kezeli őket: A képernyővédőnek átadott parancs-sor argumentumok a következők lehetnek:    /p handle PreView. Child Window-ban való megjelenés Ekkor a második argumentum a Parent Window Handle-je. Ezt a Handle-t használva tudjuk lekérdezni a megjelenítésre kijelölt helyet egy egyszerű GetClientRect-el. Mikor a Display properties-nél képernyővédőt választunk, ezeket a paramétereket használva hívja meg a Windows programunkat, ezért látszik ott kicsiben. /c Konfigurálás. Mikor képernyővédőt választunk, a Settings gombra bökve ezzel a paraméterrel hívódik meg a programunk. Ekkor a képernyővédőnek biztosítania kell, hogy felhasználói

beállításokat eszközölhessen. Ezt egy egyszerű dialógusablakban szokták megoldani. A beállításokat pedig el kell tudni menteni, és persze beolvasni /s Save. Mehet a cuccos Képernyővédés egész képernyőn Bármely eseményre (billentyűlenyomás, egérmozgatás, clickelés) azonnal kilépni. Tulajdonképpen a képernyővédők is ugyanolyan programok, mint a többi, csak ezekkel az argumentumokkal kell őket hívni, különben nem történik semmi. A programunknak átadott parancs-sori argumentumok lekérdezése: Ezt természetesen többféleképpen megtehetjük. A legegyszerűbb megoldás a sima DOS-os C-s programozásnál is létező argc és a argv használata. Ezek globális változók, minden előkészítés nélkül használhatóak (sima C-nél include-olni kellett a dos.h-t) Az argc az argumentumok számát jelenti Az argv egy tömb, melynek minden ndik eleme az n-dik argumentumot tartalmazó stringre mutat Maga a programfájl is egy argumentumnak

számít, ezért az argv 0-dik eleme mindig a programfájl neve lesz, és az argc is egyel több, mint a ténylegesen átadott argumentumok száma. Ezekkel a változókkal egyszerűen megnézhetünk minden argumentumot, sőt a programfájl nevét is. A bonyolultabb, szebb, és MFC-s megoldás az, ha az objektumokat is belekeverjük. Az MFC-nek léteznek az argumentumokat kezelő dolgai. Ezeket az AppWizard minden alkalommal előszeretettel berakja a programunkba. Az Application osztály InitInstace-ébe: // Parse command line for standard shell commands, DDE, file open CCommandLineInfo cmdInfo; ParseCommandLine(cmdInfo); // Dispatch commands specified on the command line if (!ProcessShellCommand(cmdInfo)) return FALSE; Talán ismerős. Ezek csak a standard parancsokat kezelik (DDE, egy fájl megnyitása stb) Ha vannak speciális argumentumai a programnak, ezeket a dolgokat kell egy kicsit átírni. A parancs-sor 65 információt tartalmazó osztály a CCommandLineInfo. Ebből

leszármaztathatjuk a saját parancs-sor információnkat tartalmazó osztályt. Az osztálynak van egy pár adattagja, amik az információt tárolják, és egy ParseParam nevű függvénye (meg sok minden, ami most nem érdekes). virtual void CCommandLineInfo::ParseParam( LPCTSTR lpszParam, BOOL bFlag, BOOL bLast ); Ez a függvény végzi egy argumentum vizsgálatát, és az argumentumnak megfelelően állítja be az adattagokat. Az első paramétere az argumentum, a második azt jelenti, hogy volt-e előtte – vagy / jel A harmadik azt mondja meg, hogy ez-e az utolsó. A CWinApp::ParseCommandLine függvény hívja meg minden argumentumra a paraméterként átadott CCommandLineInfo osztály ParseParam függvényét. Egy képernyővédő argumentumait kezelő CCommandLineInfo-ból származtatott osztály így néz ki: class CMyCommandLine : public CCommandLineInfo { public: HWND m ParentHandle; enum Operations { NOP=0, CONFIG, DOSAVE, PREVIEW, } m Operation; public: CMyCommandLine()

{ m Operation=NOP; } virtual void ParseParam( LPCTSTR lpszParam, BOOL bFlag, BOOL bLast ) { //Preview-nal a masodik(utolso) argumentum a parent Handle-je if (bLast && m Operation==PREVIEW) m ParentHandle=(HWND) atol(lpszParam); switch(lpszParam[0]) { case c : case C : m Operation=CONFIG; break; case s : case S : m Operation=DOSAVE; break; case p : case P : m Operation=PREVIEW; break; } } }; A konstruktorban csak nullázni kell az adattagot, ami a teendőt fogja tárolni. A ParseParam függvény beállítja mi lesz a teendő, és a Parent window Handle-jét is eltárolja, ha van. 66 Az adatok tárolása Az adatok eltárolására nem INI file-t használunk, hanem a registry-t. A registry key beállításához a CWinApp::SetRegistryKey függvénye kell. void SetRegistryKey( LPCTSTR lpszRegistryKey ); Paraméterként a Registry Key-t kell megadni. Ekkor a GetProfileInt, WriteProfileInt, GetProfileString, és WrtieProfileString függvények a beállított registry key-ből

dolgoznak. (Ezekről a függvényekről egy előző részben már volt szó.) Az adataink a következő helyen lesznek a registryben: HKEY CURRENT USERSoftwarelpszRegistryKey><application name><section name><value name> Ezenkívül Ezt az InitInstace-ból kell meghívni. Nézzük is: BOOL CSSApp::InitInstance() { Enable3dControlsStatic(); // Call this when linking to MFC statically //itt fogjuk tarolni az adatainkat a registryben: SetRegistryKey( T("MFC ScreenSaver example.")); CMyCommandLine cmdInfo; ParseCommandLine(cmdInfo); //argumentumok be switch(cmdInfo.m Operation) { case cmdInfo.NOP : case cmdInfo.CONFIG : DoConfig(); break; case cmdInfo.DOSAVE : DoSave(); return TRUE; case cmdInfo.PREVIEW : DoPreView(cmdInfo.m ParentHandle); return TRUE; } return FALSE; } Elintéztük az argumentumok lekezelését is. Ha nem adtunk meg argumentumot, akkor is konfigurálunk. Ez elég egyszerű, csak be kell olvasni a konfigurációt, elindítani a dialog

window-t, majd lementeni a változásokat: 67 void CSSApp::DoConfig() { CConfigDlg ConfigDlg; ReadConfig(&ConfigDlg.m Config); m pMainWnd = &ConfigDlg; if (ConfigDlg.DoModal()==IDOK) WriteConfig(ConfigDlgm Config); } Közben az Application m pMainWnd-jét be kell állítanunk. A másik két esetben, mikor valóban el kell indítani a screen savert, nem ilyen egyszerű a helyzet. A rajzolást végző osztályt (CSSWnd) a CWnd class-ból származtattuk, így ott külön meg kell hívni az osztály Create (vagy CreateEx) függvényét, hogy létrehozza az ablakot. Ebből a rajzolgatást végző osztályból van származtatva az, (CSSsave) amelyik az egész képernyős saving-et végzi. A rajzolást végző osztály Create függvényét kicsit meg változtatni, hogy megfeleljen. Ki kell kapcsolni az egérkurzort, ezt úgy érjük el, hogy létrehozunk egy ablakosztályt üres kurzorral. Ezután a CreateEx függvényt hívjuk meg, nem a Create-t, mert az ablak lehet

kibővített stílusú. Az egész képernyős játéknál biztosítani kell, hogy minden ablakot takarjunk el, ez a WS EX TOPMOST stílus. A Create meghívása után meghívódik az OnCreate, ahol beállítjuk a Timert, és már minden megy. Az átírt Create függvény: BOOL CSSWnd::Create(DWORD dwExStyle, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd) { // Register a class with no cursor if (m lpszClassName == NULL) { m lpszClassName = AfxRegisterWndClass(CS HREDRAW|CS VREDRAW, ::LoadCursor(AfxGetResourceHandle(), MAKEINTRESOURCE(IDC NULLCURSOR))); } return CreateEx(dwExStyle, m lpszClassName, T(""), dwStyle, rect.left, recttop, rectright - rectleft, rectbottom recttop, pParentWnd->GetSafeHwnd(), NULL, NULL ); } Paraméterként a kibővített stílust, a stílust, az ablak elhelyezkedését, és a szülőt kell megadni. Szülő csak PreView-nál lesz, bővített stílus pedig csak egész képernyőn. A CreateEx függvénynek átadandó paraméterek sorrendben a

bővített stílus, az ablakosztály, az ablak címe, a stílus, bal felső sarok koordinátái, szélesség, magasság, a szülő Handle-je, Child window ID-je, majd egy pointer a createstruct struktúra lpCreateParams elemére. Látható, hogy a szülőablak Handle-jét a GetSafeWnd függvénnyel tudjuk lekérdezni. Egyszerűbb lett volna rögtön a Handle-t átadni, de most már mindegy. Tehát a preview elindítása: void CSSApp::DoPreView(HWND ParentHandle) { 68 CWnd* pParent = CWnd::FromHandle(ParentHandle); ASSERT(pParent != NULL); CSSWnd* pPreViewWnd = new CSSWnd(); CRect rect; pParent->GetClientRect(&rect); pPreViewWnd->Create(NULL, WS VISIBLE|WS CHILD, rect, pParent); m pMainWnd = pPreViewWnd; } Csak a szülő azonosítóját tudjuk (handle) a parancssorból, kell hozzá rendelni egy CWnd classt, mert onnan kell lekérdeznünk az ablakterületet. Ez a CWnd::FromHandle függvényével lehetséges Így egy GetClientRect-el le tudjuk kérdezni az ablakterületet.

A Create után megadjuk a főablakot az Application osztály számára. Teljes képernyőre: void CSSApp::DoSave() { CSSsave* pSaveWnd = new CSSsave; pSaveWnd->Create(); m pMainWnd = pSaveWnd; } Egy másik osztály végzi, mint a Child window-ban való futást, de abból van származtatva. Tartalmaz egy csomó üzenetkezelőt, hogy bármely eseményre ki tudjon lépni. Ilyenkor egyszerűen egy WM CLOSE üzenetet küld. Egy üzenetkezelő a sok közül: void CSSsave::OnLButtonDown(UINT nFlags, CPoint point) { PostMessage(WM CLOSE); CSSWnd::OnLButtonDown(nFlags, point); } Egyedül az OnMouseMove néz ki másképpen, figyelni kell rá, hogy rögtön az ablak létrehozásakor is meghívódik egyszer, ilyenkor pedig nem jó kilépni. Az osztály Create függvénye biztosítja, hogy fusson a képernyővédő: void CSSsave::Create() { CRect rect(0, 0, ::GetSystemMetrics(SM CXSCREEN), ::GetSystemMetrics(SM CYSCREEN)); CSSWnd::Create(WS EX TOPMOST, WS VISIBLE|WS POPUP, rect, NULL); }

Lekérdezzük a képernyő méreteit, ezt adjuk át ablakterületnek az ősosztályunk Create-jának, ami mindent elintéz. Ha a programot megírja az ember, azt sem árt elintézni, hogy ne exe-kiterjesztésűre, hanem scr-re fordítson a Visual C++. Egyszerűen beszabadulunk a ProjectSettings -be, és ahol csak exe-t látunk átírjuk scr-re. 69 Fontos: A Display properties a ScreenSaver listán megjelenő neveket a programok erőforrásából szedi. A String táblán az 1-es string lesz a listán. Ez a programban az IDS DESCRIPTION azonosítójú string Az azonosító mindegy, az a lényeg, hogy a hozzá rendelt érték 1 legyen. Ezt úgy érhetjük el, hogy a String Properties-nél az ID-hez hozzáírunk egy “=1”-et. Pl: ID: IDS DESCRIPTION=1 Egy másik megoldás, ha a resource.h-ban írjuk át az értéket 1-re Ha nincs ilyen értékű string a string táblán, a Windows csak egy üres sort tesz a lista elejére, és nem enged vele semmit csinálni. Hát ennyi. Ja és

még felhívnám a figyelmet, hogy ha lefordítod a forráskódot, akkor át kell nevezni az SS.scr-t valami másra, mert SS.scr néven nem látja a Windows 9x képernyővédő beállítója (Az SS egymásutáni karakterek valami spec. célra lehetnek fenntartva Windows NT alatt nincs ilyen probléma. Azért megjegyezném a teljesség kedvéért, hogy az MS féle "túldokumentáltságnak" köszönhetően egy hetem ment rá, hogy vajon miért megy WinNT alatt és miért nem megy Win9x alatt. Talán a resource filelal van a gond (gondoltam naívan) ? Ilyen triviális dologra, hogy nem lehet a képernyővédő program neve SS.scr - mint a ScreenSaver rövidítése -, nem gondoltam, csak végső elkeseredésemben. Lehet, hogy nem ártott volna a helpben erre utalni ?) Többszálú (MultiThread) Programok Úgy általában Az eddigi programjaink mind egy szálon futottak, egy úgynevezett elsődleges szálon. Azonban a Windows 95/98, és Windows NT alá írt programok már

elindíthatnak egy vagy több másodlagos szálat is, melyek egymástól függetlenül párhuzamosan futhatnak. A párhuzamosság gyakorlatilag persze azt jelenti, hogy az operációs rendszer hol az egyik szálat, hol másikat futtatja gyors egymásutánban. Ezt a Windows NT 3.5 óta bevezetett preemptív multitasking teszi lehetővé Tehát a többszálúság API szinten támogatott. Ennek a lehetőségnek olyan programoknál vehetjük hasznát, ahol egyszerre több feladatot kell végrehajtanunk. Szálak (Thread) alkalmazásával a program leegyszerűsödik és hatékonyabb lesz. Szálakat alkalmazhatunk az üzenetkezelés gyorsítására is Ha egy lassú, vagy nagy számításigényű folyamatot az elsődleges szálban végzünk, az üzenetkezelés lelassul, vagy esetleg egészen addig nem működik, amíg a folyamat le nem zárul. A lassú folyamat egy másodlagos szálba helyezése megoldja a problémát. Ilyen külön szálba rendezett feladatok lehetnek hálózatra való

connect-elés, vagy mondjuk a Word automata helyesírás-ellenőrzője, ami minden második szavamat piros hullámos vonallal aláhúzza, minden ötödiket pedig zölddel, én meg nem tudom, hogy hol kell lekapcsolni! Egy szál elindítása viszonylag gyors folyamat, és kevés memóriát igényel. A Program minden szála ugyanabban a memóriatartományban fut, így ugyanazokat az erőforrásokat és globális változókat használják. Az erőforrások és globális változók ilyen megosztása problémákat is okozhat, ezért van szükség a szálak szinkronizálására. A dolog MFC-ben Mivel a dolog már API szinten támogatott, az MFC-nek nincs nehéz dolga a szálakkal, és sok a többszálú programozást elősegítő osztálya, eljárása van. De még az elején említsük meg, hogy bizonyos MFC korlátozások is vannak. Ezek kellemetlenek tudnak lenni, de annak érdekében születtek, hogy ne szálljon el mindig a program. 70 Project beállítások: A Project/ Settings

dialógusablakban a C/C++ fület választva a Category listán válasszuk a Code Generation-t. A Use RunTime Library beállítást válasszuk MultiThread-re, vagy Multithread DLL-re Debug konfigurációnál a Debug MultiThread-et, vagy a Debug MultiThread Dll-t válasszuk. Ez egyébként a ClassWizard-al létrehozott programok esetében mindig helyesen van beállítva. Új szál létrehozása Egy új szál elindításához egy CWinThread osztályra van szükségünk, és az AfxBeginThread függvényt kell meghívni. CWinThread* AfxBeginThread ( AFX THREADPROC pfnThreadProc, LPVOID pParam, int nPriority = THREAD PRIORITY NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY ATTRIBUTES lpSecurityAttrs = NULL ); Vagy: CWinThread* AfxBeginThread ( CRuntimeClass* pThreadClass, int nPriority = THREAD PRIORITY NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY ATTRIBUTES lpSecurityAttrs = NULL ); Az első függvény egy úgynevezett dolgozó (working) szálat hoz

létre. Ezt használhatjuk folyamatok levezérlésére. A második egy Felhasználói Interface (User-Interface) szálat hoz létre Egy ilyen szál a felhasználói merényletek, és események kezelésére használatos, maga a program elsődleges szála is egy ilyen szál, ritkán van szükség még egy létrehozására. Sokkal gyakrabban van szükségünk dolgozó szál (working Thread) létrehozására, ezért a továbbiakban ezzel foglalkozunk. Tehát tekintsük a függvény első megadását. Az AfxBeginThread függvény elindítja az új szálat, és visszatérése után a két szál párhuzamosan fut. Az első paraméter (pfnThreadProc) a szál-függvényt (Thread Function) határozza meg. Ezt a függvényt kezdi az új szál végrehajtani. A függvényből természetesen hívhatunk újabb függvényeket, de ha a szál-függvény visszatér, a szál befejeződik. A pParam egy paraméter, amit a szál-függvény fog megkapni mikor meghívódik. A szál-függvényt a

következőképpen kell megadni: UINT MyThreadFunction (LPVOID pParam) A függvény által visszaadott UINT a kilépési kód. Normális esetben a szál függvény nullát ad vissza, ami a rendeltetés szerinti kilépést jelenti, de használhatunk bármi más értéket is egyet kivéve, a STILL ACTIVE –ot (0X00000103L), ami azt jelzi, hogy a szál még mindig fut. Az átadott pParam lehet egy egyszerű számra, vagy egy struktúrára mutató pointer, ami információkat tartalmaz a szál számára. Az AfxBeginThread utolsó négy paramétere előre definiált értékekkel is rendelkezik, amik általában meg is felelnek, de azért nézzük. Az nPriority a szál prioritását jelenti. Minél magasabb, annál több processzoridőt fog kapni az operációs rendszertől. Alapértelmezett értéke az átlagos prioritás THREAD PRIORITY NORMAL 71 A prioritások növekvő sorrendben: THREAD PRIORITY IDLE Csak akkor kap processzoridőt, ha többi szál nem fut. THREAD PRIORITY

LOWEST Átlag alatt 2 ponttal THREAD PRIORITY BELOW NORMAL Átlag alatt 1 ponttal THREAD PRIORITY NORMAL Átlag THREAD PRIORITY ABOVE NORMAL Átlag felett 1 ponttal THREAD PRIORITY HIGHEST Átlag felett 2 ponttal THREAD PRIORITY TIME CRITICAL Időkritikus szál. A többi szál nem nagyon kap időt. A dwCreateFlags paraméter alapértelmezett értéke nulla. Ez azt jelenti, hogy létrehozás után a szál azonnal elindul. Ha CREATE SUSPENDED értékkel hozzuk létre, akkor magunknak kell majd elindítani (CWinThread::ResumeThread). A többi paramétert nem kell piszkálni, a szálhoz tartozó verem méretét, és a biztonsági jellemzőket adhatnánk meg, ha nem lennének jók az alapértelmezett értékek. Egy szál elindítása egyszerűen: UINT Function(LPVOID pParam) { return(0); } Void proc() { Int Attr=13; CWinThread *PwinThread=AfxBeginThread (Function, &Attr) . . } Szálak megállítása: MFC-ben egy szálat, egy másik szálból nem tudunk megállítani. Egy szál

csak akkor fog leállni, ha saját maga is, meg a párt is úgy akarja. Ezért, ha egy másik szálban derül ki, hogy neki meg kell állni, annyit tehetünk, hogy jelezzünk ezt a kívánságot számára (a később tárgyalásra kerülő megoldások bármelyikével), majd reménykedünk a jóindulatában. Egy szál kétféleképpen állíthatja meg magát. Az egyszerűbb módja, ha egyszerűen a szálfüggvényből visszatér Ez egy szál befejezésének rendeltetésszerű módja Ekkor felszabadul a szálnak lefoglalt verem, meghívódik az általa készített minden objektumra a destruktor. A másik út, hogy erőszakosan megállunk. Ezt az AfxEndThread függvénnyel tehetjük meg Void AfxEndThreat(UINT ExitCode); Ezt a függvényt a szálon belülről bárhonnan meghívva azonnali leállást tudunk produkálni. Paraméterként a szál kilépési kódját kell csak megadni. Annyi szépséghibája van a dolognak, hogy a szálhoz tartozó verem felszabadítódik, de minden más

lefoglalt memória, vagy létrehozott objektum a helyén marad. Ezért mielőtt behúznánk a kéziféket, tegyük meg a szükséges destruktorhívásokat, meg minden egyebet. 72 Szálak kezelése A szál létrehozásakor egy CWinThread objektumra mutató poinert kaptunk vissza (a példában PWinThread). Ennek a pointernek a segítségével tudjuk a szálon kívülről is kezelni a szálat Egy szál felfüggesztéséhez a SuspendTread metódust hívhatjuk: PWinThread -> SuspendThread(); Egy felfüggesztett szál újraindításához, illetve CREATE SUSPENDED paraméterrel létrehozott szál elindításához a ResumeThread függvényt használhatjuk. PWinThread -> ResumeThread(); Egy szál prioritási szintjét megváltoztathatjuk a SetThreadPriority függvénnyel. PWinThread -> SetThreadPriority (THREAD PRIORITY HIGHEST); Prioritási szintjét pedig a GetThreadPriority függvénnyel tudjuk lekérdezni. Ezek mellett az MFC függvények mellett használhatunk a WinAPI-ban

már meglévő függvényeket is a szálak kezelésére. Tipikusan ilyen a ::GetExitCodeThread Le tudjuk vele kérdezni egy szál kilépési értékét, vagy ha még mindig fut, akkor STILL ACTIVE-ot fogunk kapni. DWORD ExitCode; :: GetExitCodeThread(PWinThread->m hThread, &ExitCode); A függvénynek a szál Windows handler-jét kell megadni (m hThread), és egy DWORD címét, ahova meg fogjuk kapni a kilépési értéket. Ha nem STILL ACTIVE-ot kapunk, azt jelenti, hogy a szál már nem fut. Azért van egy kis probléma ezzel a dologgal. Alapértelmezésben egy szál mikor befejezi a futását, a hozzá tartozó CWinThread objektum is megsemmisül. Így mikor feltesszük egy már lefutott szálnak a nagy kérdést, hogy futsz-e még (közben a már nem létező objektum m hThread tagjára hivatkozunk), a Windows egy nem túl informatív, de annál aggasztóbb általános védelmi hiba című üzenetet fog megereszteni. Úgy tudjuk megakadályozni, hogy a CWinThread objektum

automatikusan legyilkolja magát, ha az m bAutoDelete tagjának FALSE értéket adunk. Előtte a szálat felfüggesztve hozzuk létre, hogy biztos mi legyünk a gyorsabbak. Ugyanis, ha a szál lenne a gyorsabb, és már ki is lépett mire eljutunk az objektumra való hivatkozásig, szintén védelmi hibát generálnánk. CWinThread *PwinThread=AfxBeginThread (Function, &Attr, THREAD PRIORITY NORMAL, 0, CREATE SUSPEND); PwinThread->m bAutoDelete=FALSE; PwinThread->ResumeThread(); Ekkor viszont azzal a problémával találjuk szembe magunkat, hogy az objektum örökké élni akar. Miután biztos nincs már rá szükségünk, nekünk kell megszüntetni: delete PWinThread; Korlátozások MFC osztályokra. 73 Kétféle korlátozás van: 1. Két szál nem próbálhat egyszerre ugyanahhoz az MFC objektumhoz hozzáférni Természetesen megoszthatnak MFC objektumokat, de csak külön-külön férhetnek hozzá. 2. Az alábbi osztályú, vagy abból származtatott osztályú

objektumokat csak az őt létrehozó szál használhatja: -CWnd -CDC -Cmenu -CgdiObject Ezeknek az osztályoknak mind van olyan adattagjuk, amely a Windows handle-t tartalmazza. Ha egy másik szálból akarunk egy ilyen objektumot használni, át kell adni a szálnak ezt a handle-t, és a szál készít magának a handle-ből egy objektumot, (FromHandle metódus) amit szabadon csak ő használhat. Felmerül a kérdés, mi van, ha nem tartjuk be ezeket a korlátozásokat. A fordító nem fog szólni érte, minden nyavalygás nélkül lefordítja, csak a program futása során érhetnek minket kellemetlen meglepetések. Könnyen előfordulhat, hogy egyszerűen nem áll le a szál, így ha a szál leállására várunk egy másik szálból, ott is meg fogunk fagyni. Persze ez a probléma csak akkor fog jelentkezni, ha már jóval előbb nem kaptunk egy védelmi hibát. De azért olyan is elképzelhető, hogy véletlenül tökéletesen működik. Hiába, a Windows útjai

kifürkészhetetlenek Szálak szinkronizálása Már az 1. Korlátozásból is adódik, hogy a szálakat valahogy szinkronizálni kell, hiszen tudnunk kell, mikor szabad hozzáférni egy áhított megosztott MFC objektumhoz. Akkor szabad hozzáférni, mikor más nem akar. Az ilyen kérdések eldöntéséhez kell használnunk az MFC nyújtotta, illetve az API által nyújtott szinkronizálási lehetőségeket. A legegyszerűbb a kölcsönös kizárás (mutual exclusion) elve. Ezt, ahogy az összes többi szinkronizációs megoldást kétféleképpen használhatjuk. Használhatjuk az MFC-s megoldást (CMutex), vagy a Win32 API függvényeit. Nézzük meg mindkét esetet: A Win32 API–val egy példa: Először deklarálunk globálisan egy mutex handle-t, hogy később minden szálból elérjük: HANDLE Hmutex; Ezután valahol a programban létrehozzuk a mutex-et: Hmutex=::CreateMutex(NULL, FALSE, NULL); Az első paraméter NULL, ez azt jelenti, hogy az alapértelmezett biztonsági

jellemzőket elfogadjuk, és a mutex azonosító (handle) nem lesz örökölhető. A második paraméter FALSE, ez azt jelenti, hogy kezdetben a mutex jelezni fog. (Ha TRUE lenne, akkor kezdetben nem jelezne) A harmadik paraméter NULL, ez azt jelenti, hogy nem adunk meg a mutexnek nevet. Ha a mutexet nem fogjuk a process-en kívülről használni, akkor nem is lesz szükségünk névre. A függvény egy azonosítót ad vissza, amire a későbbiekben szükségünk lesz. Ezek után következhet a tényleges kizárás. Egy ::WaitForSingleObject hívást kell elvégeznünk a programrészlet elején , ahol a “védeni kívánt erőforrást használjuk. A használat után pedig egy ::ReleaseMutex hívással felszabadítjuk a védett erőforrást. 74 ::WaitForSingleObject(HMutex, INFINITE); strBuffer = T("CSA! "); ::ReleaseMutex(HMutex); A ::WaitForSingleObject függvénynek az első paramétere a mutex, a második, hogy mennyi időt vagyunk hajlandó várni, amíg a mutex

nem jelez. Egy mutex akkor jelez, ha szabad, tehát éppen senki sem használja az áhított erőforrást. Ha nem jelez, akkor a paraméterként megadott időt várunk, hogy szabad legyen. INFINITE paraméter esetén bármeddig A ReleaseMutex hívásával szabadítjuk fel a használt erőforrást. Ahányszor bármelyik szálból használni akarjuk ezt az erőforrást, annyiszor meg kell hívnunk a ::WaitForSingleObject függvényt paraméterként mindig ugyanazt a mutex azonosítót átadva, majd a ::ReleaseMutex függvényt szintén ezzel az azonosítóval. Annyi mutex-et kell csinálnunk, ahány erőforrást meg akarunk osztani. A ::WaitForSingleObject egyébként más várakozásoknál is jó. Segítségével például megvárhatjuk egy szál lefutását: bRun=FALSE; //megkerjuk allljon le ::WaitForSingleObject(m PThread->m hThread,INFINITE); //megvarjuk mig leall Ugyanez MFC-vel: Deklarálunk egy CMutex objektumot valahol, ahol mindegyik szál látja: CMutex m mutex; Majd

ezután valahol meghívjuk a mutex konstruktorát: m mutex(FALSE,NULL); Az eső paraméter azt jelzi, abból a szálból, ahol meghívtuk a konstruktort hozzá akarunk-e férni a mutex által vezérelt erőforráshoz. A második paraméter a mutex neve Ha nem adunk meg, más processzekből nem tudjuk majd elérni. Ezután következik a mutex vezérlése a szálból: CSingleLock sLock(&m mutex); sLock.Lock(); strBuffer = T("CSA! "); sLock.Unlock(); Egy CSingleLock objektumot kell létrehoznunk, ennek segítségével zárjuk le, illetve szabadítjuk fel az erőforrást. A további szinkronizációs objektumok részletes leírását mellőzném, inkább egy rövid táblázat: Szinkronizációs Célja lehetőség Win32 API függvények MFC osztály Mutex ::CreateMutex ::WaitForSingleObject ::WaitForMultipleObject ::ReleaseMutex ::CloseHandle Cmutex Megakadályozza, hogy több szál használjon egy erőforrást 75 Kritikus rész Ugyanaz, mint a mutex, csak nem

osztható meg processzek között ::InitalizeCriticalSection ::EnterCriticalSection ::LeaveCriticalSection ::DeleteCriticalSection CcriticalSection Szemafor Megszabja, hogy maximum hány szál használhat egy erőforrást ::CreateSemafor ::WaitForSingleObject ::WaitForMultipleObjects ::ReleaseSemaphore ::CloseHandle Csemaphore Esemény egy szál különféle jelzéseket küldhet más szálaknak ::CreateEvent ::SetEvent ::PulseEvent ::ResetEvent ::WaitForSingleObject ::WaitForMultipleObjects ::CloseHandle Cevent A példaprogram bemutatja egy szál létrehozását, megszüntetését, megállítását, szálból más szál ablakára való rajzolást. Folyamatok (process-ek) kezelése A folyamat tulajdonképpen egy programot jelent. Egy programot addig nevezünk programnak, amíg az a háttértárolón pihenget, ha betöltődik a memóriába, és elindul, akkor már egy folyamatnak nevezzük. Más szavakkal a folyamat (process) egy programnak a memóriában futó egy

példánya. Hála a preemptív multitasking-nek, a folyamatok (process-ek) egymással párhuzamosan futnak, akárcsak egy folyamaton belül a szálak (thread-ek). Folyamatokat általában manuálisan, a futtatható állomány betöltésével indítunk. Folyamatokat bármelyik másik folyamatból is indíthatunk, de ellentétben egy szál indításával ez egy lassú folyamat (mire bemászik a memóriába a HDD-ről), és sok memóriát igényel. Ekkor az indított folyamatot gyermek folyamatnak (child process) nevezzük, azt ahonnan indítottuk, pedig szülő folyamatnak (parent process). A szálak könnyen meg tudnak osztani egymás közt egy-egy globális változót, vagy objektumot, így könnyen kommunikálva egymással, hiszen azonos memóriatartományban futnak. Ezzel ellentétben a párhuzamos folyamatoknak nem sok közük van egymáshoz, elszigetelt memóriatartományban futnak (szerencsére). Az adatcseréket mindenféle trükkös úton kell végezniük. Pl: vágólap,

csatorna, megosztott memória, OLE Azért persze itt is léteznek a szálaknál már megismert szinkronizációs objektumok. Egy új folyamat elindítása Egy folyamathoz biztos, hogy egy és csakis egy futtatható állomány tartozik. Egy futtatható állomány elindításához, és az ezzel járó külön folyamat (és annak elsődleges szálának) elindításához a következő függvényt használjuk: BOOL CreateProcess( LPCTSTR lpApplicationName, LPTSTR lpCommandLine, LPSECURITY ATTRIBUTES lpProcessAttributes, LPSECURITY ATTRIBUTES lpThreadAttributes, // a futtatható állomány neve // a parancssor (parancssorban átadandó paraméterek) // a folyamat biztonsági jellemzői(NULL, ha jó az alapértelmezés) // az elsődleges szál biztonsági jellemzői(NULL, ha jó az alapértelmezés) 76 // örököljük-e a szülő folyamat megosztási handlejeit (mutex, semaphor, stb.) // folyamat létrehozási jellemzői(NULL, ha jó az alapértelmezés) // Új environmentre mutató

pointer (NULL, ha jó lesz a parent-é) // mi legyen az aktuális könyvtár az új folyamat számára (NULL, ha jó lesz a parent-é) // Az új folyamat ablakának paraméterei // itt kapjuk vissza az elindított folyamatra vonatkozó információkat. BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS INFORMATION lpProcessInformation ); Az állomány lehet win32, DOS, vagy bármely az operációs rendszerünk által támogatott futtatható forma. (Tehát általában csak ez a kettő) A parancssor csak az átadni kívánt argumentumokat tartalmazza, az állomány nevét nem. Kivéve Windows NT-nél, ahol DOS alkalmazás futtatásakor a futtatható állomány nevéhez NULL-t is írhatunk, ekkor az átadott parancssor tartalmazza az állomány nevét. A biztonsági jellemzőket nyugodtan fogadjuk el alapértelmezettnek, csak NULL-t írunk a helyükre. Ugyanígy az lpEnvironment és lpCurrentDirectory

paramétereknél Csak az állománynevet kötelező megadni, az lpProcessInformation-t és az lpStartupInfo-t. Egy új folyamat egyszerű létrehozása: STARTUPINFO StartupInfo; memset(&StartupInfo, 0, sizeof(STARTUPINFO)); //mindent standard-ra StartupInfo.cb=sizeof(STARTUPINFO); //ezt az egy mezot ki kell tolteni PROCESS INFORMATION ProcessInfo; ::CreateProcess („IZE.EXE”, //a futtatható állomány neve NULL, //nem adunk meg semmit a command line-ban NULL, //alapértelmezett folyamat biztonsági opciók NULL, //alapértelmezett szál biztonsági opciók FALSE, //nem örökölünk handle-eket 0, //alapértelmezett létrehozási opciók NULL, //nem lesz külön environment NULL, //ugyanaz a munkakönyvtár, mint a parent-é &StartupInfo, //az ablak jellemzői &ProcessInfo); //a folyamatra vonatkozó információk A példában a futtatható állománynak az aktuális könyvtárban (vagy a keresési útban) kell lennie. Semmilyen parancssori paramétert nem adunk át

neki, mindenhol elfogadjuk az alapértelmezett beállításokat. Az utolsó paraméterként megadott inicializálatlan PROCESS INFORMATION típusú rekord címét adjuk meg. Ide fogjuk megkapni a folyamatra vonatkozó adatokat A folyamat és annak elsődleges szálának azonosítóit. A ::CreateProcess Win32 API függvény visszatérése után a szülő és a gyermek folyamat egymással párhuzamosan fut. A szálakhoz hasonlóan a folyamatok kilépési kódját (Process exit Code) is le tudjuk kérdezni. Ehhez tudnunk kell a folyamat azonosítóját Ha a folyamat még mindig fut, akkor akárcsak a szálaknál, STILL ACTIVE értéket kapunk vissza. A kilépési kód lekérdezése: 77 BOOL GetExitCodeProcess( //A folyamat azonosítója HANDLE hProcess, LPDWORDlpExitCode //egy 32 bites változó címe, ahova megkapjuk a kilépési kódot ); Egy egyszerű példa a folyamat kilépési kódjának lekérdezésére: DWORD ExitCode; //ide fogjuk kapni a kilépési kódot

GetExitCodeProcess(ProcessInfo.hProcess, &ExitCode); If (ExitCode==STILL ACTIVE) { //még aktív } else { //már kilépett, az ExitCode a kilépési kódot tartalmazza } A példában feltételezzük, hogy a ProcessInfo a ::CreateProcess által feltöltött rekord, amelyet utolsó paraméterként adtunk meg. Természetesen itt is használhatóak a szálaknál is hasznos szinkronizációs objektumok, és függvények. Így a ::WaitForSingleObject API függvény is, melynek segítségével megvárhatjuk a szülő folyamatból, hogy a gyermek futása befejeződjön. A függvénynek csak a gyermek folyamat azonosítóját kell átadni. Ha csak addig akarunk várni, míg a gyermek folyamat készen nem áll az üzeneteink kezelésére (be nem töltődik és ki nem rajzolódik), akkor a ::WaitForInputIdle Win32 API függvényt kell használni, a függvény egyetlen paramétere a folyamat azonosítója (handle). Ezen kívül még számos beállítást elvégezhetünk a folyamaton, ha tudjuk

az azonosítóját. Ilyen a ::SetPriorityClass, melynek segítségével a folyamat prioritási szintjét tudjuk beállítani. Ez a prioritás, és a szálak prioritása együtt határozza meg egy szál valódi prioritását. Egy folyamatot meg is tudunk állítani a ::TerminateProcess függvénnyel. Egy folyamat azonosítója akkor is érvényes, ha a folyamat már nem fut. Ha ezt az azonosítót nem akarjuk többet használni, le kell zárni a ::CloseHandle függvénnyel. Ha egy folyamathoz tartozó összes azonosítót lezártuk, felszabadítódik a folyamat nyilvántartásához lefoglalt memória. Ha ezt a lezárást nem tesszük meg, a programunk kilépésekor az azonosítók automatikusan lezáródnak. A szülő folyamatnak, és gyermek folyamatot, egymástól elszigetelten, és függetlenül futnak. A szülő folyamat bezárása után is futni fog minden általa elindított folyamat. Nem érik el egymás adatait, ezért szükség van a szálaknál is megismert szinkronizációs

függvényekre. Külön folyamatok szálainak szinkronizálására három objektum alkalmas. A mutex, a szemafor, és az esemény. Mikor ezeket használjuk, az őket kezelő API függvényeknek meg kell adni az azonosítót Ezt az azonosítót mindegyik az objektumot használó szálnak tudnia kell, tehát mindegyik folyamatnak meg kell szereznie (ha nem örökölte). Egyszerű megoldás, ha nevet rendelünk az objektumhoz, és mindegyik szál erre a névre hivatkozva szerzi meg az azonosítót. Pl.: Az első folyamatban egy mutex elkészítése: //első folyamatból HMutex= ::CreateMutex (NULL, //alapértelmezett biztonsági opciók, nem örökölhető 78 FALSE, //elkészítés után jelez ”Mutex neve”); //A mutex neve, ezzel tudunk hivatkozni rá más //folyamatból is. Ezzel a függvényhívással létrehoztuk a szinkronizációs objektumot. Ha egy másik folyamatból is használni akarjuk, akkor ugyanezt a függvényhívást kell megtennünk ugyanezzel a névvel, és

megkapjuk az azonosítót. A második folyamatban az azonosító megszerzése: //második folyamatból HMutex= ::CreateMutex (NULL, //alapértelmezett biztonsági opciók, nem örökölhető FALSE, //elkészítés után jelez ”Mutex neve”); //A mutex neve, ezzel tudunk hivatkozni rá más //folyamatból is. A két függvényhívás pontosan ugyanaz, viszont a kiváltott hatásuk különböző. Az első függvényhívás elkészíti az objektumot, a második, pedig csak visszaadja az azonosítót, az objektummal semmit nem kell csinálnia (mivel már létezik). Akkor sem történik semmi katasztrófa, ha véletlenül a második folyamat függvényhívása fut le előbb, ekkor az objektum létrehozódik, az első folyamat függvényhívásakor pedig csak az azonosító jön vissza. Tehát a két folyamat futtatási sorrendje felcserélhető. Ha biztos vagyunk benne, hogy először az első folyamat fog elindulni, és a második elindulásakor már létezni fog a szinkronizációs

objektumunk, akkor a második folyamatból egy ::OpenMutex (vagy ::OpenSemaphore vagy ::OpenEvent) függvényhívással megkaphatjuk az azonosítót: //második folyamatból Hmutex=::OpenMutex (MUTEX ALL ACCESS, //hozzáférés módja (teljes hozzáférés) FALSE, //nem örökölhető ”Mutex neve”); //a név Ha egy nem létező objektum névre hivatkozunk, akkor a függvény NULL-t ad vissza. Ezeknek a módszereknek hátránya, hogy nem tudhatjuk, más programok vajon használnak-e ugyanolyan nevű objektumot, mint mi. Mert ebben az esetben egyik programnak sem fog tetszeni a dolog. Olyan nevet kell választani, amit biztos nem használ még semmi Amolyan próba-szerencse alapon. Örökölt és másolt azonosítók Egy gyernekfolyamat örökölhet azonosítókat a szülőtől, de ehhez az kell, hogy a szinkronizációs (vagy bármely más) objektum létrehozásakor beállítsuk az örökölhetőséget jelző flag-et. A SECURITY ATTRIBUTES rekord bInheritHandle mezőjét TRUE-ra

kell állítani. SECURITY ATTRIBUTES Security = {sizeof (SECURITY ATTRIBUTES), //A méret az első érték NULL, //alapértlemezett biztonság 79 TRUE} //örökölhető HMutex =::CreateMutex (&Security, //az előző rekord címe FALSE, //kezdetben jelez NULL); //nem adunk meg nevet Ez még nem minden, amikor a szülő létrehozza a gyermek folyamatot, a ::CreateProcess ötödik paraméterének igazat kell átadni. Ez jelzi, hogy a létrehozott folyamat örökölni fogja a szülő azonosítóit. Végezetül pedig el kell juttatni a létrehozott folyamatnak az azonosítók értékeit Mondjuk folyamatok közti adatcserével (pipe) ld. később Egy másik megoldás, ha a ::DuplicateHandle függvényt használjuk. HANDLE H2ndMutex; //ide fogjuk kapni a másik azonosítót ::DuplicateHandle (::GetCurrentProcess(), //A szülő process a ::GetCurrentProcess() függvénnyel tudja //lekérdezni a saját azonosítóját HMutex, //ezt akarjuk megduplázni ProcessInfo.hProcess, //A

gyermek folyamat handle-je. A //CreateProcess által visszaadott //ProcessInfo-t használjuk &H2ndMutex, //Ide kapjuk meg a másolat azonosítót NULL, //a hozzáférési jogok //Ha az utolsó paraméternek //DUPLICATE SAME ACCESS //az értéke, akkor mindegy mit írunk ide NULL, //nem örökölhető DUPLICATE SAME ACCESS); //egyéb opciók, ha nulla akkor //alapértelmezett Ezzel bármilyen windows objektumra vonatkozó azonosítót megduplázhatunk, így azt adatcserével később át lehet adni a létrehozott folyamatnak. Ezen módszerek előnye, hogy a szinkronizációs objektumokon kívül szálak, csatornák, és fájl leképezések megosztására is használható. Egyszerűen észrevehető hátránya pedig az, hogy nem egyszerű. Folyamatok közti Adatcsere (InterProcess Communication) Csatorna (pipe) használatával: A csatorna két folyamat (általában szülő és gyermek) között létesít kapcsolatot. A Win32 API elvileg megengedi névvel ellátott, és névtelen

(anonymous) csatornák használatát, de mivel a Windows 95 csak a névtelen csatornák használatát támogatja, erről egy pár szót. A csatornát a szülő folyamat készíti el egy ::CreatePipe API hívással. A függvény visszaad két azonosítót. Egyikkel írni tudunk a csatornába, a másikkal olvasni A függvényt úgy kell meghívni, hogy a visszakapott azonosítók örökölhetőek legyenek, hiszen a gyermek is ezek közül az egyiket fogja használni. Azért csak az egyiket, mert egy névtelen csatorna egyirányú adatcserét tesz lehetővé Ha kétirányú adatcserét akarunk, akkor két csatornára lesz szükségünk, az egyiknek csak az írási azonosítóját örökli az elindított folyamat, a másik csatornának pedig csak az olvasási azonosítóját. Azt az azonosítót, amit nem akarunk megosztani a gyermek folyamattal egy ::DuplicateHandle függvényhívással átmásoljuk egy nem örökölhető azonosítóba, majd az eredetit bezárjuk. Az örökléshez

természetesen a folyamat létrehozásánál is engedélyezni kell az öröklést (::CreateProcess 80 5. Paraméter) A gyermek folyamatnak persze valahonnan meg kell kapni az azonosító értékét A szülő a ::SetStdHandle API függvény segítségével adhatja át az azonosítókat. Ha adatot akar majd küldeni a csatornán, akkor az alapértelmezett bemenetet (STD INPUT HANDLE) a gyermek számára átállítja a csatorna olvasási azonosítójára. Ezt a gyermek a ::GetStdHandle API függvény segítségével tudja lekérdezni. Ha a szülő adatot akar fogadni a csatornán, akkor az alapértelmezett kimenetet (STD OUTPUT HANDLE) állítja át a csatorna írási azonosítójára, amit a gyermek szintén a ::GetStdHandle API függvénnyel tud meg. Az átállítgatott azonosítókat persze a szülőnek el kell menteni, és a gyermek folyamat elindítása után visszaállítani. Ha már megvannak az azonosítók, csak a ReadFile/WriteFile függvényeket kell használni a csatorna

írásához/olvasásához. Ez kicsit bonyolult lett, nézzünk példát: A szülő adatot küld, a gyermek fogad. A szülő folyamat: HANDLE hSaveStdIn, hPipeWr, hPipeRd, hPipeWrDup; DWORD dwWritten; SECURITY ATTRIBUTES Security = {sizeof (SECURITY ATTRIBUTES), //A méret az első érték NULL, //alapértlemezett biztonság TRUE}; //örökölhető //csatorna létrehozása ::CreatePipe(&hPipeRd, &hPipeWr, &Security, 0); //eredeti elmentése hSaveStdIn = GetStdHandle(STD INPUT HANDLE); //bemenet atirasa ::SetStdHandle(STD INPUT HANDLE, hPipeRd); //a csatorna eredeti írási azonosítóját megtartjuk magunknak ::DuplicateHandle (::GetCurrentProcess(), hPipeWr, ::GetCurrentProcess(), &hPipeWrDup , 0, FALSE, //<- nem örökölhetőbe másoljuk DUPLICATE SAME ACCESS); //eredetit bezárjuk, csak a másolat kell ::CloseHandle(hPipeWr); //a gyermek folyamat elindítása ::CreateProcess(,,,,TRUE,,,,,); //Az ötödik paraméter TRUE. Ezzel engedélyezzük az öröklést

//visszaállítjuk amit átraktunk ::SetStdHandle(STD INPUT HANDLE, hSaveStdIn); //Küldünk a gyermeknek ::WriteFile(hPipeWrDup, ”Egyszer volt hol nem volt”,26, &dwWritten, NULL); //bezárjuk, végeztünk ::CloseHandle(hPipeWrDup); ::CloseHandle(hPipeRd); A gyermek folyamat (adatot fogad): HANDLE hStdIn; char chBuf[26]; DWORD dwRead; //Ez tulajdonképpen a pipe olvasási azonosítója: hStdIn = ::GetStdHandle(STD INPUT HANDLE); //Adatok beolvasása ::ReadFile(hStdIn, chBuf, 26, &dwRead, NULL); 81 //Ezzel kész is vagyunk. Ha minden jól ment, //chBuf=”Egyszer volt hol nem volt” Ennek példájára mindenki el tudja képzelni, ha a szülő fogad. Ha fogadni is akar, és küldeni is, akkor kettő csatornát kell készítenie, és a gyermek is két azonosítót fog lekérdezni. Osztott Memória használatával: Két folyamat alapértelmezésben nem használhatja egymás adatait, hiszen egymástól jól elkülönített memóriatartományban futnak.

Állomány-leképezéssel viszont több folyamat is használhatja ugyanazt a memóriát. Az állomány-leképezés tulajdonképpen egy fájl memóriabeli leképezését jelenti Ez arra biztosít lehetőséget, hogy nem a lemezre, hanem az állomány memóriabeli tükörképére írhatunk, ezzel felgyorsul a fájlkezelés. Egy fájl-t pedig egyszerre több program is használhat Ha az állományleképezéshez nem rendelünk fájl-t, akkor osztott memóriát csinálhatunk Az osztott memóriát az operációs rendszer kezeli, ha kell ki is swappeli. Fájl-leképezés létrehozása a ::CreateFileMapping Win32 API függvénnyel történik. A függvényt mindegyik, az fájl-leképezést használni szándékozó folyamatból meg kell hívni. Az első meghíváskor létrehozódik a leképezés, a többi híváskor csak azonosítót ad rá vissza. Természetesen minden folyamatból ugyanolyan névvel kell rá hivatkozni. A leképezés létrehozása után mindegyik folyamatnak nézetet kell

hozzá létrehozni. Ez a ::MapViewOfFile API függvénnyel történik A függvény visszaad egy mutatót, ami a megosztott memóriára mutat. Mindegyik a megosztott memóriát használó függvény ugyanezt a mutatót kapja vissza, így egy tárterületen dolgoznak. Ebből következően szükség lehet a szinkornizációs objektumok valamelyikére. Ha már nincs szükségünk a megosztott memóriára, akkor előszor meg kell szüntetni a nézetet az ::UnmapViewOfFile függvénnyel, majd a fájl leképezést a ::CloseHandle függvénnyel. Osztott memória létrehozása mindegyik, a memóriát használó folyamatban: HANDLE HfileMapping; //A leképezés azonosítója char *SharedMemory; HfileMapping = ::CreateFileMapping ((HANDLE)0XFFFFFFFF, //nem adunk meg file-t NULL, //alapértlemezett biztonsági //és öröklési tulajdonságok PAGE READWRITE, //mindent megengedünk 0, //a maximális méret felső 32 bitje 1024, //a maximális méret alsó 32 bitje “LEKEPEZES”); //a leképezés

neve SharedMemory=(char *)::MapViewOfFile (HfileMapping, //a leképezés aznosítója FILE MAP ALL ACCESS, //hozzáférés 0, //felső 32 bitje a leképezésbeli //eltolásnak 0, //alsó 32 bitje a leképezésbeli //eltolásnak 1024); //a megosztott memóriablokk mérete //A memória használata . . //Ha már nem kell, felszabadítjuk ::UnmapViewOfFile(SharedMemory); ::CloseHandle(HfileMapping); 82 A vágólap használata A Windows vágólap (clipboard) adatok átvitelét teszi lehetővé egyik programból a másikba. Az átvitel nagyon egyszerű, mivel csak néhány utasítást lehet használni a programban a vágólap kezelésére. Két utasítást (Cut,Copy), amely adatot helyez a vágólapra, és egyet (Paste), amely ezt leemeli onnan. A vágólapon lévő adat technikailag minden programtól független, a rendszer által birtokolt memóriablokkok halmaza, kezelése az API függvények segítségével történik. A vágólap magyar elnevezés nem általános, lehet még

vágóasztal, illetve átmeneti tároló fordításokkal is találkozni. Én igyekszem a vágólap elnevezéshez tartani magam A gyakorlatban a vágólapot kezelő parancsok minden programban az Edit menüben találhatóak. A Cut (CTRL+X/ SHIFT+DEL)illetve Copy (CTRL+C/CTRL+INS) parancsra a program a kiválasztott adatot a vágólapra másolja, miután törölte annak tartalmát. A vágólapon lévő adat csak meghatározott formátumú lehet (szöveg, bittérkép, metafile, stb és persze saját formátumot is tudunk létrehozni). Mikor a felhasználó a Paste (CTRL+V/SHIFT+INS) parancsot kiadja (ugyanazon, vagy bármely más programból) a vágólapon lévő adat kimásolódik, de annak tartalma nem változik. A program csak akkor használja fel a vágólap tartalmát, ha ismeri az ott lévő adat formátumát. Az iméntiek mindenkinek nyilvánvalóak, de azért gondolkodjunk el rajta. Levonhatjuk például azt a hasznos tanulságot, hogy mikor kifogyunk a memóriából a legelső

dolog amit megtehetünk, hogy töröljük a vágólap tartalmát úgy, hogy valami nagyon kicsit (egy karaktert) rakunk bele. (ha egy 800X600-as színes kép maradt a vágólapon, az 1.37 Mb memória!) A szabványos vágólap-formátumok: Az azonosítók a windows.h -ban vannak definiálva Vannak olyan formátumok, amik használata általános (ASCII, bitmap), és persze vannak olyanok is ahol a tárolt adatok értelmezése a programra van bízva. Ilyenkor előfordul, hogy hibásan értelmezi az adatot (pl kép minden byte-ját egy szöveges karakterként teszi be). Nem írom le az összeset, csak a gyakran használatosakat: CF BITMAP Windows kompatibilis bittérkép. A bittérkép leírót kezeljük CF DIB Használata hasonló a bittérképhez, a leíró eszköz-független bittérképre vonatkozik. CF DIF A leíró globális memóriablokkra vonatkozik, ami ASCII formátumú adatot tárol, minden sort CR LF (kocsivissza, soremelés 10,13) zár le. A string végét NULL jelzi. CF

OEMTEXT OEM karaktereket tartalmazó string. Kocsivissza, és soremelés is lehet benne CF PALETTE Paletta. A paletta leírója van a vágólapnál 83 CF TEXT Egy globális memóriablokkban elhelyezett ANSI karaktereket tartalmazó string. minden sort CR LF (kocsivissza, soremelés 10,13) zár le. A string végét NULL jelzi. CF WAVE Az adatok hangokat írnak le. Szöveges adatok írása a vágólapra. A Cut illetve Copy parancsok üzenetkezelőit kell átírnunk, mert ezen parancsok hatására kerülhet adat a vágólapra. Először azt kell megoldanunk, hogy ezeknek a parancsoknak a menüpontjai csak akkor éljenek, ha van valami kiválasztva. A ClassWizard-ból rendeljünk kezelőfüggvényeket a menüpontok UPDATE COMMAND UI üzeneteihez. (A példában feltételezzük, hogy a m bSelection változó akkor igaz, ha van kijelölés): void CMyView::OnUpdateEditCut(CCmdUI* pCmdUI) { pCmdUI->Enable(m bSelection); } void CMyView::OnUpdateEditCopy(CCmdUI* pCmdUI) {

pCmdUI->Enable(m bSelection); } Ez eddig egyszerű. Vegyük sorra, milyen műveleteket kell elvégeznünk ASCII adatnak a vágólapra másolása során:  Memória lefoglalása, amelybe belefér a másolandó szöveg. Ehhez a ::GlobalAlloc API függvényt használhatjuk. HGLOBAL GlobalAlloc( UINT uFlags, DWORD dwBytes // memóriablokk tulajdonságai // blokk mérete byte-okban ); A visszatérési érték a lefoglalt memóriaterület azonosítója (handle). Az első paraméterként átadott tulajdonság vágólap esetében GMEM MOVEABLE | GMEM DDESHARE |GMEM ZEROINIT legyen.  Zároljuk a memóriablokkot a :: GlobalLock API függvénnyel. LPVOID GlobalLock( HGLOBAL hMem); Az átadott paraméter a ::GlobalAlloc függvény által visszaadott leíró (handler). Ha a zárolás sikeres volt, akkor a visszaadott mutató a memóriablokk első byte-jára mutat. Ha sikertelen, akkor NULL. A zárolásra azért van szükség, mert ekkor biztos, hogy csak mi használjuk a memóriát, és

nem lesz elmozgatva (ki swappel-ve), azonkívül így kapjuk meg a rá mutató pointert.   Az adatot a memóriaterületre másoljuk. Az imént megkapott pointer ismeretében egyszerű Szöveg esetében egy ::lstrcpy függvény is megteszi. Feloldjuk a memóriaterület zárolását a ::GlobalUnLock API függvénnyel. BOOL GlobalUnlock(HGLOBAL hMem); Az átadott paraméter az eddig is használt leíró (handler), a visszatérési érték akkor igaz, ha az eljárás sikeres volt. A feloldás után többet nem hivatkozhatunk a memóriaterületre az előbb használt pointerrel. 84  Megnyitjuk a vágólapot a Cwnd::OpenClipboard MFC függvénnyel. Amíg le nem zárjuk, addig más alkalmazás már nem férhet hozzá! BOOL Cwnd::OpenClipboard( ); A visszatérési érték igaz, ha sikeres volt a megnyitás.  Töröljük a vágólap tartalmát az ::EmptyClipboard API függvénnyel. Ezzel a vágólap egyedüli birtokosává váltunk. A függvény felszabadít minden olyan

memóriablokkot, amely kapcsolódott a vágólaphoz. A függvény meghívása előtt meg kell nyitnunk a vágólapot BOOL EmptyClipboard(void); A visszatérési érték akkor igaz, ha az ürítés sikeres volt.  A vágólap tartalmát a mi adatunkra állítjuk a ::SetClipboardData API függvénnyel. Paraméterként megadjuk a memóriaterület azonosítóját (handler), és az adat formátumát. HANDLE SetClipboardData(UINT uFormat, HANDLE hData); A visszakapott leíró (handle) a felírt adat azonosítója. Azért van rá szükség, mert az átadott azonosítót többet nem használhatjuk, mivel már nem a miénk a memóriablokk.  Lezárjuk a vágólapot a ::CloseClipboard API függvénnyel. BOOL CloseClipboard(void); A visszatérési értéke igaz, ha sikerült lezárni. Bármely más program csak ezután éri el a vágólapot, ezért fontos, hogy ne felejtsük el lezárni. Ennek a működése: Lefoglalunk a Windows-tól egy memóriablokkot, visszakapjuk a leíróját

(handler), zároljuk a memóriát, rámásoljuk az adatot, majd feloldjuk. A vágólap megnyitása és törlése után meghívjuk a ::SetClipboardData API függvényt a memóriablokk leírójával. A memóriablokk eddig úgy volt regisztrálva, hogy a mi programunkhoz tartozik. Ilyen esetekben a Windows törli a memóriablokkot, ha a program futása véget ér. Esetünkben a Windows átveszi magának a memóriablokkot, a ::GlobalReAlloc API függvénnyel. Ezek után a memóriaterület már nem létrehozó programé, így nem is hivatkozhat rá! A felsorolt lépések szöveges adatok átvitelét végzik, egy bittérkép annyiban különbözik, hogy ott egy bittérkép leíróját kell átadni a ::SetClipboardData API függvénynek. Így az első 4 lépés máshogy alakul. Példa szöveg felmásolására a vágólapra. A Copy menüpont COMMAND üzenetéhez rendelt kezelőfüggvény: void CClipboardView::OnEditCopy() { HANDLE HText; //leiro a globalis memoriablokkhoz char *pText; //mutato

a memoriblokkra char *pBuf; //a kivalsztott szoveg atmeneti tarolasara //A dokumentum CClipboardDoc* pDoc = GetDocument(); ASSERT VALID(pDoc); //a kivalasztott szoveg pBuf=new char[pDoc->SelectedSize()+1]; pDoc->GetSelectedText(pBuf); //Globalis memoriablokk HText=::GlobalAlloc( 85 GMEM MOVEABLE | GMEM DDESHARE |GMEM ZEROINIT, strlen(pBuf)+1); //memoriablokkra mutato pointer pText=(char *)::GlobalLock(HText); //szoveg atmasolasa a globalis memoriablokkra ::lstrcpy(pText,pBuf); //feloldas ::GlobalUnlock(HText); if (!OpenClipboard()) //vagolap megnyitasa CWnd::OpenClipboard { //csak akor sikerul, ha mas eppen nem hasznalja. GlobalFree(HText); AfxMessageBox("A vagolapot nem sikerult megnyitni"); delete [] pBuf; return; } //clipboard torlese, tulajdonosai leszunk ::EmptyClipboard(); //leiro atadasa a clipboardnak ::SetClipboardData(CF TEXT, HText); //vagolap lezarasa ::CloseClipboard(); delete [] pBuf; } Szöveges adatok olvasása, beillesztése a vágólapról. Az

adatok olvasása kicsit bonyolultabb, mint az írás. Először azt kell megtudni, hogy van –e a vágólapon általunk ismert formátumú adat. Ezt a ::IsClipboardFormatAvailable API függvény tudja megmondani. BOOL IsClipboardFormatAvailable(UINT format); Paraméterként csak a keresett formátumot kell megadni, a visszatérési érték akkor igaz, ha van keresett formátumú adat a vágólapon. Használata előtt nem kell megnyitnunk a vágólapot Ezt a függvényt használhatjuk arra, hogy ha nem ismert formátum van a vágólapon, akkor a Paste menü le legyen tiltva. Rendeljünk kezelőfüggvényt a Paste menü UPDATE COMMAND UI üzenetéhez. void CMyView::OnUpdateEditPaste(CCmdUI* pCmdUI) { pCmdUI->Enable(::IsClipboardFormatAvailable(CF TEXT)); } A fenti kódrészlet akkor engedi élni a Paste menüpontot, ha a vágólapon létezik szöveges formátumú adat. Eddig még nem említettem, de a vágólapon egyszerre több formátumban is fent lehet ugyanaz az adat, azonos

formátumban több adat viszont nem. Hogy pontosan hány féle formátum van a vágólapon, azt a ::CountClipboardFormats API függvény segítségével tudjuk lekérdezni. int CountClipboardFormats(void); A függvény visszatérési értéke a formátumok száma. 86 Az adatok formátumát a ::EnumCliboardFormats API függvény adja vissza. Meghívása előtt meg kell nyitni a vágólapot a ::OpenClipboard API függvénnyel. UINT EnumCliboardFormats(UINT format); Ha NULL-t adunk meg paraméterként, akkor a függvény az első formátumot adja vissza. Ha a vágólapon megtalálható formátumot adunk meg, akkor a következő formátumot kapjuk visszatérési értékként. Ha már nincs többféle formátum, akkor NULL-al tér vissza A vágólapon lévő formátumokból egy listát is kaphatunk a ::GetPriorityClipboardFormat API függvénytől, és ebben keresgélhetünk kedvünkre való formátumot. Használata előtt meg kell nyitni a vágólapot a ::OpenClipboard API

függvénnyel. int GetPriorityClipboardFormat(UINT *PriorityList, int cEntries); Az első paraméter a tömb címe, ahová a listát várjuk, a második pedig a lista elemeinek száma. A lista végét egy NULL elem jelzi (ha belefért a tömbbe). A tömb méretének meghatározásához használhatjuk a ::CountClipboardFormats API függvény-t. A tömb mérete egyel nagyobb kell hogy legyen, így elfér a lezáró NULL is. Az adatot definiáló leírót (handler) a ::GetClipboardData API függvény adja vissza. Szöveges adat esetén a leíró egy globális memóriablokkra vonatkozik, kép esetén pedig egy bittérképre. Használata előtt meg kell nyitni a vágólapot a ::OpenClipboard API függvénnyel. HANDLE GetClipboardData(UINT format); Paraméterként csak a formátumot kell megadnunk. Ezt a formátumot a fenti függvények bármelyikével meghatározhatjuk. Ha még sem talál megadott formátumot, akkor NULL-t ad vissza Mivel a visszaadott leírót nem a miénk, csak addig

használhatjuk, míg le nem zártuk a vágólapot. Ha későbbre is meg akarjuk őrizni, akkor a memóriablokk tartalmáról másolatot kell készítenünk. Tehát szövegnek a vágólapról való beolvasásához végezendő lépések.     Megnyitjuk a vágólapot a ::OpenClipboard API függvénnyel. A ::GetClipboardData API függvénnyel lekérdezzük a memóriablokk leíróját. Lekérdezzük a memóriára mutató pointert a memóriablokk zárolásával. ::GlobalLock Kimásoljuk az adatot magunknak, szöveg esetében a ::lstrcpy API függvénnyel. A másolás előtt szükség lehet az adat hosszának a lekérdezésére. Ezt a ::GlobalSize API függvény végzi el. DWORD GlobalSize(HGLOBAL hMem); Paraméterként a memóriablokk leíróját (handle) kell megadni. Visszatérési érték a blokk mérete.   Feloldjuk a memóriaterület zárolását a ::GlobalUnLock API függvénnyel. Bezárjuk a vágólapot a ::CloseClipboard API függvénnyel. void

CClipboardView::OnEditPaste() { HANDLE HPaste; //leiro a globalis memoriblokkhoz char *pBuf; //atmeneti buffer, a szovegnek char *pText; //pointer a globalis memoriblokkon levo szovegre CClipboardDoc* pDoc = GetDocument(); ASSERT VALID(pDoc); if (OpenClipboard()) //vagolap megnyitasa { //csak akor sikerul, ha mas eppen nem hasznalja. HPaste=::GetClipboardData(CF TEXT);//memoriablokk leirojat kapjuk 87 if (HPaste==NULL) //NULL, ha nincs olyan formatum { ::CloseClipboard(); //be kel zarni, mert mar megnyitottuk!! AfxMessageBox("Nincs szoveg formatum a vagolapon !"); return; } //buffer, ahova bemasoljuk a beszurando adatot: pBuf=new char[::GlobalSize(HPaste)]; //az adatra mutato pointer: pText=(char *)::GlobalLock(HPaste); //szoveg atmasolasa: ::lstrcpy(pBuf, pText); //feloldas: ::GlobalUnlock(HPaste); //vagolap lezarasa: ::CloseClipboard(); //szoveg beszurasa pDoc->InsertText(pBuf); //buffer legyilkolasa delete [] pBuf; Invalidate(); } } Kép másolása a vágólapra.

A Cut és Copy menüpontokat csak akkor kell engedélyeznünk, ha van másolásra kijelölt kép. Ezt a menüpontok UPDATE COMMAND UI üzeneteihez rendelt kezelőfüggvényekben oldhatjuk meg. void CMyView::OnUpdateEditCopy(CCmdUI* pCmdUI) { pCmdUI->Enable(m bSelection); } Bittérkép vágólapra másolása annyiban különbözik a szöveg kezelésétől, hogy egy globális memóriablokk leírója helyett itt egy bittérkép leíróját kell átadnunk. Tehát a lépések a Copy illetve Cut menüpontok COMMAND üzenetének kezelőfüggvényében:  Egy bittérkép létrehozása. MFC-ben a CBitmap osztály CreateCompaitbleBitmap tagfüggvényével. BOOL CBitmap::CreateCompatibleBitmap( CDC* pDC, int nWidth, int nHeight ); Az első paraméter egy mutató egy eszköz-környezet objektumra (Device Context). Ezzel kompatibilis lesz a létrehozott bittérkép. A második és harmadik paraméter a létrehozandó bittérkép méretét (szélesség, magasság) határozzák meg.  Egy

eszköz-környezet objektum létrehozása. Ennek segítségével tudunk majd műveleteket végezni a képen, ha majd hozzárendeltük. Pl rámásolni a másolásra kijelölt bittérképet A létrehozást a CDC osztály CreateCompatibleDC tagfüggvénye végzi. virtual BOOL CDC::CreateCompatibleDC( CDC* pDC ); 88 Az egyetlen paraméter egy másik eszköz-környezet objektum címe, amellyel a létrehozott kompatíbilis lesz. Itt ugyanannak az objektumnak a címét kell megadni, amit a bittérkép létrehozásakor, mert ekkor lesz kompatíbilis a bittérkép az új eszköz-környezettel.  A bittérkép hozzárendelése az imént létrehozott eszköz-környezethez. A CDC osztály SelectObject tagfüggvényével. A függvénynek többféle definíciója van, esetünkben erre lesz szükség: CBitmap* CDC::SelectObject( CBitmap pBitmap ); Paraméterként az első lépésben létrehozott bittérkép objektumot kell átadnunk. Így hozzárendeltük az eszköz-környezethez. A

visszatérési érték az eddig hozzárendelt bittérkép Nem lesz rá szükségünk.  Átmásoljuk a másolásra kijelölt bittérképet a létrehozott bittérképre. Ezt a bittérképhez rendelt eszköz-környezet objektum BitBlt tagfüggvényével tudjuk elvégezni. BOOL BitBlt( int x, int y, int nWidth, int nHeight, CDC* pSrcDC, int xSrc, int ySrc, DWORD dwRop ); Az első és második paraméter a bittérképre másolt kép bal felső sarkának koordinátáit határozzák meg, a harmadik és negyedik a kép méreteit, az ötödik az eszköz-környezet, amelyen a kép található, a hatodik és hetedik a kép bal felső sarka az eszköz-környezeten, az utolsó a másolás módját határozza meg (most legyen SRCCOPY).      Megnyitjuk a vágólapot a CWnd::OpenClipboard függvénnyel. Ürítjük a vágólapot a ::EmptyClipboard API függvénnyel, ezzel egyedüli tulajdonosai lettünk. Megadjuk a vágólapnak a bittérkép leíróját, és az adat formátumát

(most CF BITMAP) a ::SetClipboardData API függvénnyel. A bittérkép leíróját a bittérkép objektum m Object adattagjában találjuk. Meghívjuk a bittérkép objektum Detach() tagfüggvényét. Ezzel elérjük, hogy nem törlődik a bittérkép, amint a program az objektumot tartalmazó blokkból kilép (mert ugye normális esetben ez történne). Lezárjuk a vágólapot a ::CloseClipboard API függvénnyel. A következő példa az egész client area-t kirakja a vágólapra. void CClipboardView::OnEditCopy() { CClientDC ClientDC(this); //eszkoz kornyezet a client area-ra CBitmap Bitmap; //bitterkep objektum BITMAP BitmapInfo; //bitterkep informaciok CDC MemDC; //Memoria eszkoz kornyezet CRect Rect; //client-area merete GetClientRect(&Rect); //bitterkep letrehozasa, amire rafer az egesz client area Bitmap.CreateCompatibleBitmap(&ClientDC, RectWidth(), RectHeight()); //eszkoz-kornyezet letrehozasa MemDC.CreateCompatibleBitmap(&ClientDC); //eszkoz-kornyezet

hozzarendelese a bitmap-hez MemDC.SelectObject(&Bitmap); MemDC.BitBlt(0, 0, Rect.Width(), Rect.Height(), &ClientDC, 0, 0, SRCCOPY); 89 if (OpenClipboard()) //vagolap megnyitasa { //csak akor sikerul, ha mas eppen nem hasznalja. //vagolap torlese ::EmptyClipboard(); //leiro atadasa a vagolapnak ::SetClipboardData(CF BITMAP, Bitmap.m hObject); //hogy ne semmisuljon meg a kep Bitmap.Detach(); //vagolap lezarasa ::CloseClipboard(); } } Kép beillesztése a vágólapról. A beillesztés Paste menüponttal történik. A menüpontot csak akkor szabad engedélyezni, ha a vágólapon van bittérkép formátumú adat. A Paste menü UPDATE COMMAND UI üzenetéhez rendelt kezelőfüggvény: void CMyView::OnUpdateEditPaste(CCmdUI* pCmdUI) { pCmdUI->Enable(::IsClipboardFormatAvailable(CF BITMAP)); } A beillesztés a menüpontok COMMAND üzeneteinek kezelőfüggvényében történik. A lépések:    A vágólap megnyitása az ::OpenClipboard API függvénnyel. A bittérkép

leírójának lekérdezése a ::GetClipboardData API függvénynél. A függvényt CF BITMAP paraméterrel hívjuk. A leíróhoz hozzá tudunk rendelni egy CBitmap objektumot. CGDIObject (innen van származtatva a CBitmap) osztály Attach tagfüggvényének segítségével. BOOL Attach( HGDIOBJ hObject ); A függvény egyetlen paramétere a leíró.     Létre kell hozni, és a CBitmap objektumhoz hozzá kell rendelni egy eszköz-környezetet. A CDC osztály CreateCompatibleDC, és SelectObject tagfüggvényeivel. A képet ne próbáljuk meg módosítani, hiszen nem a miénk, csak a leíróját kaptuk meg. Kimásoljuk a képet egy másik eszközkörnyezetre a CDC osztály BitBlt tagfüggvényével. Megszüntetjük az összerendelést a CBitmap objektum és a vágólapon lévő leíró között. Az objektum Detach tagfüggvényével. Ha nem tennénk, a kezelőfüggvényből való kilépéskor az objektum törlésekor a kép is törlődni akarna, pedig a képet nem

írhatjuk, hiszen nem a miénk. Lezárjuk a vágólapot a ::CloseClipboard API függvénnyel. void CClipboardView::OnEditPaste() { CClientDC ClientDC(this); //eszkoz kornyezet a client area-ra CBitmap Bitmap; //bitterkep objektum BITMAP BitmapInfo; //bitterkep informaciok HANDLE HBitmap; //bitterkep azonositoja CDC MemDC; //Memoria eszkoz kornyezet if (OpenClipboard()) //vagolap megnyitasa { //csak akor sikerul, ha mas eppen nem hasznalja. HBitmap= ::GetClipboardData(CF BITMAP); //bitterkep leirojat megkapjuk if (HBitmap==NULL) //ha nincs bitterkep a vagolapon { ::CloseClipboard(); return; } 90 //objektum hozzarendelese a leirohoz Bitmap.Attach(HBitmap); //kep informacioi Bitmap.GetObject(sizeof(BITMAP),&BitmapInfo); //eszkoz kornyezet letrehozasa MemDC.CreateCompatibleDC(&ClientDC); //hozzarendelese a bitterkephez MemDC.SelectObject(&Bitmap); //kimasolas a kepernyore ClientDC.BitBlt(0, 0, BitmapInfo.bmWidth, BitmapInfo.bmHeight, &MemDC, 0, 0, SRCCOPY);

//hozzarendeles megszuntetese Bitmap.Detach(); //vagolap lezarasa ::CloseClipboard(); } } Saját vágólap formátumok regisztrálása. Előfordulhat, hogy a megengedett formátumok nem felelnek meg a továbbítani kívánt adat sajátosságainak. Ekkor saját formátumot kell regisztrálni Ennek az a hátulütője, hogy a vágólapon lévő ilyen formátumú adatot csak az általunk írt programok tudják helyesen értelmezni, hiszen más nem ismeri a formátumot. Erre az lehet a megoldás, ha szabványos formátumban is felrakjuk ugyanazt az adatot. Saját formátum regisztrálására a ::RegisterCliboardFormat API függvény való. UINT RegisterClipboardFormat(LPCTSTR lpszFormat); Paraméterként a formátum nevét kell megadni, visszatérési érték pedig a formátum-azonosító. Ezt az azonosítót kell átadni például a ::SetClipboardData és ::GetClipboardData függvényeknek. Példa: UINT MyFormat; MyFormat=::RegisterClipboardFormat(“MyClipboardFormat”); Ezt a

visszakapott azonosítót kell használnunk, a vágólapra másolásnál, illetve vágólapról való beillesztésnél. ::SetClipboardData(MyFormat, Hmydata); Az átadott leíró (Hmydata) egy globális memóriablokk leírója. A ::RegisterClipboardFormat meghívásakor, ha ugyanilyen névvel már létezik formátum, nem hoz létre újat, hanem visszaadja a már létező azonosítóját. Késleltetett adatszolgáltatás röviden Amint eddig is látszott, amikor adatot helyezünk el a vágólapon, az adatról előtte másolatot készítünk, és csak a másolatot adjuk át a vágólapnak. Ez gyakorlatilag annyit jelent, hogy az adat kétszer annyi memóriát foglal, mint amin elfér. Megvan egyszer annak a programnak, ami felrakta a vágólapra, meg megvan a vágólapnak. Ez főleg nagyméretű képek esetében zavaró (nem mindegy, hogy 15, vagy 3 Mb-al kevesebb a szabad memória) 91 A problémára a késleltetett adatszolgáltatás a kerülőút. Ekkor a ::SetClipboardData

függvénynek a leíró helyett nullát adunk át paraméterként. Tehát semmi nem kerül a vágólapra. Ha más program meghívja a ::GetClipboardData függvényt, akkor a Windows értesíti az adatszolgáltató programot, mire az felpakolja az adatot a vágólapra. Ekkor a szolgáltató program egy WM RENDERFORMAT üzenetet kap amelynek wParam paramétere a formátum, nem kell megnyitnia a vágólapot, csak egy ::SetClipboardData függvényhívással felrakja az adatot a vágólapra. Itt történik meg az adat másolása Elkerülni nem tudtuk, csak késleltettük Ha egy másik program törli a vágólapot, akkor a szolgáltató program adataira már nem lesz szükség, ekkor a WM DESTROYCLIPBOARD üzenetet kapja. Ha a szolgáltató program futása befejeződik, akkor kap egy WM RENDERALLFORMAT üzenetet, ami azt jelenti, hogy fel kell raknia a vágólapra az összes egyébként általa szolgáltatott adatot. AMŐBA Terjedelmes bevezető után nézzünk egy egyszerű

amőbaprogramot. A probléma nem igazán bonyolult, de pár dolgot azért tanulhatunk belőle:     Először is hogyan lehet amőbázásra bírni a számítógépet (ennek semmi köze a Visual C-hez, de érdekes). Hogyan tudja egy ablak a saját méretét változtatni (akkora legyen az ablak, mint az amőbapálya beállított mérete). Hogyan lehet egy ablak stílusát meghatározni (ne legyen rajta minimalizáló, maximalizáló gomb, ne legyen átméretezhető stb.) Ezen kívül látni fogjuk, hogy egy osztálynak egynél több őse is lehet. Kezdjünk először a program felépítésével. A legjobb, ha készítünk egy osztályt, amelyik tárolja, kezeli és létrehozza az amőbatáblát. Ennek az osztályunknak nem is lesz őse, és itt oldhatjuk meg a gép elleni játékot is. Lesz egy osztályunk az alkalmazásnak a CWinApp-ból származtatva, valamint egy osztály az ablaknak, aminek a CFrameWnd az őse. Az amőbatáblát kezelő osztály létrehozása Az eddig

létrehozott osztályaink mind MFC osztályokból voltak örökítve, most viszont egy olyat akarunk hozzáadni a programunkhoz, ami nem MFC, sőt nincs is őse. Majdnem ugyanolyan egyszerűen is eljárhatunk, mint eddig, meg persze egy kicsit bonyolultabban is. Először az egyszerűbb megoldás. A főmenüben InsertNew Class , vagy jobb egérgomb a ClassView-ban és new Class . Ezt eddig is így csináltuk. Az itt megjelenő dialógusablakon állítsuk át a Class Type mezőt Generic Class-ra Ekkor meg fog jelenni egy lista, ahová az ősöket tudjuk beírni, meg hogy milyen módon örökítünk belőle (ez szinte mindig public). Esetünkben a listát üresen hagyjuk, csak az új osztály nevét kell beírni, és készen vagyunk. A némileg bonyolultabb megoldás. A FileNew dialógusablakban létrehozunk egy fejléc (header) -és egy forrásfájlt az osztály számára, ügyelve arra, hogy az Add To Project ki legyen pipálva. Ekkor kaptunk két üres állományt A

fejlécállományban (valami.h) megadjuk az osztály deklarációját, a forrásállományban (valamicpp) pedig megvalósítjuk a tagfüggvényeket. Arra kell vigyázni, hogy a cpp fájlban include-oljuk a stdafxh-t , különben a fordítónak nem fog tetszeni a precompiled headerek (ezt nem tudtam lefordítani) miatt. Ha ezzel megvagyunk, alakítsuk ki a class-unkat. Kell egy mutató, ami a pályát leíró mátrixra mutat Minden eleme egy-egy byte, és vagy nulla (ekkor üres), vagy az ott lévő figura (kör vagy X) kódját 92 tartalmazza. Tudnunk kell, melyik játékos jön, milyen jellel van a számítógép, és ki kezd Ezeket is a figurákhoz tartozó kóddal tároljuk. Szintén rutinfeladat egy függvény elkészítése, ami végigpásztázza a pálya mátrixát, és jelzi, ha valaki nyert. És persze egy olyan sem árt, amivel a felhasználó lépni tud (a mátrixba ki kell csak írni az adott helyre a kódját). Az igazi feladat a gép elleni játék megvalósítása.

Az itt leírtakon kívül nyílván létezik sokkal hatékonyabb megoldás is a probléma megoldására, ez csak az én megoldásom. Abból az egyszerű ötletből induljunk ki, hogy ha jönne egy jótündér, és megmondaná, minden egyes pozícióra a pályán, hogy mennyire segít minket nyeréshez, akkor már könnyű dolgunk lenne. Csak meg kellene kérni: figyusz, nézd már meg az én szempontomból, hogy vannak ezek az értékek (továbbiakban hasznosság), meg az ellenfelem szempontjából. Egy lépés annál jobb, minél fontosabb lett volna az ellenfelemnek az elfoglalt pozíció, és minél jobban segít engem a győzelemhez. Tehát a tündérkétől kapott két értéket összeadjuk (vagy valamilyen más függvény szerint összegezzük) minden pozícióban, és oda lépünk, ahol a legnagyobb lett az összeg. Ha egy olyan helyre léptünk, ami nekünk nem volt hasznos, viszont az ellenfél számára annyira hasznos volt, hogy a hasznosságok összege itt volt a legnagyobb

a pályán, akkor védekeztünk. Ha az ellenfél számára érdektelen helyre léptünk, akkor támadtunk, hiszen számunkra nyílván nagyon hasznos lehetett, ha az összeg ilyen nagy volt. Tehát ha lenne egy jótündérünk, akkor már működne a dolog, védekezés is lenne, és támadás is. Próbáljuk meg helyettesíteni. Egy pozíció akkor bír hasznossággal (segít a győzelemhez), ha vízszintesen, függőlegesen, vagy valamelyik átlóban rá lehet illeszteni egy olyan 5 (ötöt kell kirakni) egység hosszú szakaszt, ahol az ellenfélnek nincs jele, nekem viszont van. Ekkor van esélyem, hogy azon a szakaszon kirakjam az ötöt. Ha az ellenfélnek van a szakaszon valahol jele, akkor nekem azon az ötös szakaszon tuti nem lesz meg az öt, így számomra haszontalan, nem segít nyeréshez. Összezavaró ábra: A kékkel bekeretezett cellák az X számára hasznosak, a pirossal bekeretezettek pedig az O számára. Amelyik cellák nincsenek bekeretezve, az egyik fél

számára sem bírnak hasznossággal, hiszen ott nem jöhet ki neki az 5. Bárki is következzen, biztos lehetünk benne, hogy egy olyan cellába fog rakni, ami pirossal és kékkel is be van keretezve, hiszen ezzel az ellenfélnek is keresztbetesz. Ez a módszer akkor is bejön, ha nagyobb a tábla, ekkor tovább tudjuk csúsztatni az ötös kereteket: 93 Mindig egyenként csúsztatunk, amíg nem értük el a pálya végét. Látszik, hogy egy jel több ötösben is játszhat szerepet, mint itt a második O. A csúsztatás után kapott hasznosságokat hozzáadjuk egyszerűen a már meglévőekhez. Arról is gondoskodnunk kell, hogy az a cella, ami két jelünk után jön, hasznosabbnak tűnjön, mint ami egy jelünk után jön. Ezt egy egyszerű táblázattal érjük el, amiben eltároljuk külön az ellenfél, és a mi szempontunkból, hogy mennyire hasznos adott számú jelet egymás mellé tenni. Nyilván sokkal hasznosabb egy négyest kialakítani, mint egy hármast. Az

ellenfél hasznossági értékei legyenek egy hajszálnyit alacsonyabbak, hiszen hasznosabb kirakni egy ötöst, mint megakadályozni az ellenfélét. A tömbök a programban: static int HaszonNekem[5]={0,3,50,200,6000}; static int HaszonNeki[5]={0,2,49,199,5999}; Tehát az nem hasznos, ha a nincs sehol semmi, nekem 3 egységnyi hasznossággal bír egy hely, ha mellette minimum öttel van egy saját jelem, és abban az ötösben az ellenfélnek nincs jele. 50, ha két jelem van, 200, ha három, és 6000 (nagyon magas, mert ha odateszek, akkor nyertem), ha négy jelem van mellette. Ezeknek az értékeknek a megváltoztatásával elérhető, hogy a program egész máshogy játsszon. A hasznosságokat szintén egy mátrixban fogjuk tárolni. Vigyázzunk arra, hogy ha egy pozíció nem üres, akkor oda ne definiáljunk hasznosságot, ciki lenne odalépni. Ezt a csúsztatásos dolgot horizontálisan, vertikálisan, a főátló és a mellékátló irányában eljátszva a hasznossági

mátrix minden egyes szabad helynek a hasznosságát tartalmazni fogja. Pl.: horizontálisan: //a csúsztatás végig a táblán: for (y=0; y<m TableSize.cy; y++) for (x=0; x<m TableSize.cx-4; x++) Haszon5H(x,y); //x és y a kezdőpont void CAmoba::Haszon5H(int x, int y) { int Nekem=0,Neki=0,n,h=0,c; int ellen=EllenFel(); //mi az ellenfel jele //összeszámoljuk, kinek hány jele van abban az ötösben for(n=x;n<x+5;n++) { c=TabAt(n,y); //az adott pozíción milyen jel van if (c==m Computer) Nekem++; //ha a számítógépé akkor növeli a nekem számlálót if(c==ellen) Neki++; //ha az ellenfélé, akkor a Neki-t növeljük } 94 if (Nekem==0) h=HaszonNeki[Neki]; //ha nekem nincs ott jelem, akkor neki hasznos if (Neki==0) h=HaszonNekem[Nekem]; //ha neki nincs ott jele, akkor nekem hasznos for(n=x;n<x+5;n++) AddHaszon(n,y,h); //mind az öt helyhez hozzáadjuk a hasznosságot } Ezt négy irányba megcsináljuk, és meg is vannak a hasznosságok. Ezek közül csak ki

kell keresni a legnagyobbat, és odalépni. Előfordulhat, hogy több ugyanolyan hasznosságú cella van, ekkor az emberi gondolkodáshoz hasonlóan választunk az ugyanolyan jónak tűnő lehetőségek közül. A véletlenre bízzuk A módszer messze nem tökéletes, de azért valahogy eldöcög. Azt is mondhatnám, hogy direkt ilyen, így legalább néha győzhetünk a gép ellen. De sajnos nem direkt Ennyit a játékstratégiáról. A program összeállításában ott tartunk, hogy van egy osztályunk, amit a CFrameWnd-ből származtattunk. Ez végzi az ablak kirajzolását, kezelését Az lenne az igazi, ha az imént megalkotott amőbázós osztály is az őse lenne, hiszen így rendelkezni fog annak minden tagjával, és szabadon használhatja őket. Ezt egyszerűen elérhetjük Megkeressük a Frame osztályunk deklarációját, és az ősök közé egyszerűen beírjuk ezt az osztályt: class CAmobaFrame : public CFrameWnd,CAmoba Ekkor az osztálynak több őse lesz, és

mindegyik összes tulajdonságával rendelkezni fog. Tehát ablakot kezelni is tud, meg amőbázni is, és nekünk pont ez kell. Az ablak kezeléséről és beállításáról Azt szeretnénk, hogy az ablak pont akkora legyen, amin elfér a pálya, se nagyobb, se kevesebb. Ehhez több dolog is kell. Először az ablak már abban a méretben jelenjen meg, tehát bele kell szólnunk az ablak elkészítésébe. Erre való a PreCreateWindow virtuális függvény Másodszor el kell érnünk, hogy a felhasználó ne tudja újraméretezni az ablakot, ha már olyan gondosan beállítottuk. Ezt szintén az előbb említett függvényben tehetjük meg. Harmadszor meg gondoskodni kell róla, ha menet közben változik a pálya mérete, az ablak újraméretezze magát. Adjunk hozzá az osztályunkhoz egy PreCreateWindow virtuális függvényt. Vagy a ClassWizard-ból, vagy ClassView-nál jobb gomb és Add Virtual function. Ebben a függvényben tudjuk megadni a létrehozandó ablak stílusát, és

méretét. Ezt a paraméterként megkapott CREATESTRUCT típusú cs változó átírásával tehetjük meg. Számos beállítást módosíthatunk, most a méretet és a stílust adjuk meg. A méretet a cx és cy tagokba kell beírni, a stílust a style-ba. Sokféle stílus létezik, amire szükségünk lesz: WS BORDER Az ablaknak nem méretezhető kerete van. WS OVERLAPPED Szokványos keretes ablak WS SYSMENU Bezáró kis X megjelenik a jobb felső sarokban Ennyi kell nekünk. A stílusokat OR kapcsolattal fűzhetjük össze Az ablak méretének meghatározásához szükségünk lesz a GetSystemMetrics Win32 API függvényre. Le lehet vele kérdezni a képernyő felbontását, az ablak címsorának magasságát, keretének 95 szélességét pixelekben. A méret meghatározásához ezekre az adatokra lesz szükségünk SIZE CAmobaFrame::GetWinSize() { SIZE result; result.cx=m Unitcx*m TableSize+2m Border.cx+ 2*::GetSystemMetrics(SM CXFRAME)+2; result.cy=m Unitcy*m TableSize+2m

Border.cy+ 2*::GetSystemMetrics(SM CYFRAME)+2 +::GetSystemMetrics(SM CYMENU) +::GetSystemMetrics(SM CYCAPTION); return result; } Ez a függvény adja vissza a létrehozandó ablak méretét. A pálya méretéből (m TableSize) és a cella méretéből (m Unit) megkapjuk a Client Area-t, ehhez még hozzá kell adni az ablak kereteit, címsorát, és menüjét. Tehát az ablak méretének és stílusának meghatározása, az a bizonyos PreCreateWindow: BOOL CAmobaFrame::PreCreateWindow(CREATESTRUCT& cs) { SIZE winsize=GetWinSize(); //a fenti függvény megadja a méretet cs.cx=winsizecx; //méret beállítása cs.cy=winsizecy; cs.style=WS OVERLAPPED|WS BORDER|WS SYSMENU; //stílus beállítása return CFrameWnd::PreCreateWindow(cs); } Ahhoz, hogy az ablak saját magát tudja méretezni, szükségünk lesz a CWnd::GetWindowPlacement, és a CWnd::SetWindowPlacement függvényekre. Ezek segítségével tudjuk lekérdezni és beállítani az ablak elhelyezkedését. Mindkét függvény

a WINDOWPLACEMENT típust használja Segítségével megtudhatjuk, hogy az ablak milyen állapotban van (minimalizálva, maximalizálva, normál), és mekkora a mérete, mikor normál állapotban van. Nekünk erre az utolsóra lesz szükségünk void CAmobaFrame::ReSize() { WINDOWPLACEMENT wp; SIZE winsize=GetWinSize(); ::ZeroMemory(&wp,sizeof(wp)); wp.length=sizeof(wp); //a struktúra hosszára kel állítani GetWindowPlacement(&wp); wp.rcNormalPositionright=wprcNormalPositionleft+winsizecx; //a bal felső sarok marad wp.rcNormalPositionbottom=wprcNormalPositiontop+winsizecy; SetWindowPlacement(&wp); } 96 Több fülből álló dialógusablak (tab dialog box) készítése Nyilván mindenki tudja miről van szó. Az ilyen ablakokkal általában a beállításoknál szoktunk találkozni. Ezeken az összetartozó Control-ok külön lapokon szerepelnek, és a fülekkel tudunk váltani köztük. Ezekre az ablakokra az alsó pár gomb is jellemző "OK",

"Apply", "Cancel" és esetleg "Help" A "Help" csak akkor látszik, ha létezik az ablaknak help-je. Ilyen ablakokat nagyon egyszerűen és kicsit bonyolultabban is készíthetünk MFC-ben. Az egyszerű megoldás hátránya, hogy az imént említett gombok, és csak azok, tőlünk függetlenül meg fognak jelenni az ablak alján. Ha ettől különböző gombokat, esetleg egyéb kontrolokat is akarunk, akkor a bonyolultabb megoldást kell használni. Tegyük fel, hogy van egy programunk, aminek az egyik üzenetkezelő függvényéből akarunk nyitni egy több fülből álló dialógusablakot. Kezdjük az egyszerűbb megoldással Két osztályt kell használnunk, ezek a CPropertyPage, és CPropertySheet. Az ablakot úgy tudjuk elképzelni, mint egy füzetet. A CPropertyPage az egyes fülekhez tartozó lapokat kezeli, a CPropertySheet pedig az egész ablakot, a füleket, és az ablak alján lévő gombokat, azaz a füzetet. Először készítsünk el

egy lapot. Induljunk ennek a lapnak az erőforrásával. Készítsük el a Dialog Editoral Sima dialógusablak erőforrásra van szükségünk a lapokhoz Az Insert/Resource/Dialog menüponttal tudunk ilyet beszúrni. A létrehozott erőforrásnak bizonyos tulajdonságokkal rendelkeznie kell, hogy lapot tudjunk belőle készíteni. Ezeket a Porperties ablak Styles fülén állíthatjuk be     Legyen Child, hiszen az egyes lapok a főablak gyermekei. A keret legyen Thin, azaz vékony. Nem kell rendszermenü, minimize/maximize box. Csak címsor kell, azaz Title bar legyen csak bejelölve. A More Style Fülön pedig válasszuk ki a Disabled opciót. Ha ezzel megvagyunk, írjuk be a Caption mezőbe a lap címét. Ez fog megjelenni a hozzá tartozó fülön. Ha megszerkesztettük a lapot, létre kell hoznunk hozzá egy osztályt Inset/New Class A CPropertyPage osztályból kell származtatnunk, az imént szerkesztett dialógus erőforrás azonosítóját pedig be kell írni az

ID mezőbe. Ezzel megvan az osztályunk az erőforrással együtt A CPropertyPageaCDialog osztályból származik, tehát ugyanúgy tudjuk kezelni. A ClassWizardból minden egyes kontrolhoz tudunk változókat rendelni. Ezek az adatmozgások az osztály DoDataExchange függvényében lesznek megvalósítva. Az ablak bezárása után ezek a változók, akár egy dialógusablaknál, tartalmazni fogják az egyes kontrolok értékeit. Tehát a beállított értékeket az ablak bezárása után a saját CPropertyPage-ből származtatott osztályunkból tudjuk lekérdezni. Minden egyes lapra el kell készítenünk az erőforrását, és kell hozzá származtatnunk egy osztályt a 97 CPropertyPage-ból. Az egyes lapoknak nem kell azonos méretűeknek lenniük, az ablak létrehozásánál a CPropertySheet objektumunk a legnagyobb lappal fog számolni. Összefoglalva egy lap létrehozásának lépéseit    Az erőforrás elkészítése az imént tárgyalt tulajdonságokkal Egy

osztály származtatása CPropertyPage-ből, az erőforrás ID-jét megadjuk neki. A ClassWizard-al a lap minden kontrolához adunk változót. Szinte ugyanaz, mintha dialógusablakot hoznánk létre. Ha megvannak a lapok, össze kell rakni őket egy ablakra. Ehhez létre kell hozni egy CPropertySheet objektumot, ahová be tudjuk fűzni a lapokat. A konstruktornak át tudjuk adni a létrehozandó ablak címét. Az objektum létrehozása annyit jelent, hogy definiálunk ebből a típusból egy példányt Létre kell hozni minden egyes laphoz is egy objektumot. A Lapok befűzése a CPropertySheet osztály AddPage tagfüggvényével történik. Paraméterként csak a befűzni kívánt lap objektumának címét kell megadni. Minden egyes lapra meghívjuk a függvényt, és már össze is vannak fűzve a lapok Ezután meg kell jeleníteni az egészet. Ha modális ablakot akarunk, akkor ezt a CPropertySheet::DoModal függvény végzi. Nem modális ablakra pedig a Create függvényt

használjuk Vegyük az egyszerűbb, és jóval valószínűbb esetet. Ekkor modális ablakra van szükségünk A DoModal függvény visszatérési értéke mondja meg, hogy hogyan zárult be az ablak. Az ablak bezáródása során az egyes lapok is bezáródnak, így nem kell velük törődnünk. A visszatérési érték egy dialógusablakhoz hasonlóan lehet IDOK és IDCANCEL. Ezután a lapokban beállított kontrolok értékeit a saját származtatott osztályainkból tudjuk lekérdezni. Példa: void CMainFrame::OnProperties() { //Egyes lapok objektumai CFejPage fejpage; CSzajPage szajpage; CSzemPage szempage; //Ide lesznek összefűzve CPropertySheet sheet("Beállítások"); //Összefűzés sheet.AddPage(&fejpage); sheet.AddPage(&szajpage); sheet.AddPage(&szempage); //a beállítások átadása a lapoknak fejpage.m FejType=m FejType; fejpage.m bHaj=m bHaj; szajpage.m SzajType=m SzajType; szajpage.m bBajusz=m bBajusz; szempage.m SzemType=m SzemType; szempage.m

bKancsal=m bKancsal; szempage.m bSzemoldok=m bSzemoldok; //ablak modális megjelenítése if (sheet.DoModal()==IDOK) { //beállítások lekérdezése a lapoktól m FejType=fejpage.m FejType; m SzemType=szempage.m SzemType; m SzajType=szajpage.m SzajType; m bBajusz=szajpage.m bBajusz; m bKancsal=szempage.m bKancsal; m bSzemoldok=szempage.m bSzemoldok; 98 m bHaj=fejpage.m bHaj; } } Amint látszik, a példaprogramban egy arc tulajdonságait állíthatjuk be (jobb ötlet híján). Három lapunk van, ezeken a fej, a szem, és a száj bizonyos paramétereit állíthatjuk. Az összefűzés után, amely a CPropertySheet AddPage tagfüggvényével történik, átadjuk a lapoknak a beállításokat. Persze előtte is megtehettük volna. Egyszerűen a kontrolokhoz rendelt tagváltozók értékeit állítjuk be Majd az ablak bezárása után ugyanezen változókban a beállított értékek fognak szerepelni, ezt mentjük vissza magunknak. Az ablakon lesz egy OK, egy Cancel, és egy Apply

gomb. Help is lenne, ha rendeltünk volna Helpet az ablakhoz. Az Apply végig szürke, azaz nem működik Az Apply használata Először is tisztázzuk, mire való az Apply gomb. Ugyanaz, mint az OK, csak nem zárja be az ablakot Tehát a beállítások, amiket tettünk, érvényessé válnak, de továbbra is az ablakban maradunk. Könnyen elképzelhető, hogy erre az akcióra nem is kívánunk lehetőséget nyújtani a felhasználónak, nem is akarjuk, hogy legyen Apply gomb. Ennek ellenére Apply gomb mindig lesz (persze letiltott állapotban), ha ezzel az egyszerű módszerrel készítjük el az ablakot. Sokkal egyszerűbb letiltva hagyni, mint az eltüntetésén erőlködni. Ha használni akarjuk, először is kell egy saját OnApply függvényt készítenünk minden lap osztályában. Ez egy virtuális tagfüggvény Persze hagyhatjuk a régit, akkor csak az OnOK függvényt hívja meg. Elképzelhető, hogy ez pont megfelel Ha nem, ClassWizard-al hozzáadhatjuk az osztályokhoz a

sajátunkat. Ebből a függvényből kell elvégeznünk a beállítások érvényesítését Ha ez sikerült, akkor igazzal kell visszatérni, egyébként hamissal. Ha minden lapra megvan ez a függvény, attól még nem fog működni a gomb, ugyanis alapértelmezés szerint le van tiltva. Az engedélyezéséhez a CPropertyPage SetModified tagfüggvényét kell meghívni. Ezzel egy flag-et állítunk át, ami azt jelzi, hogy a lap változott-e vagy sem Igazat adva át paraméterként, a gomb engedélyezve lesz. Hamisat adva át, újból letiltódik Bármelyik lap SetModified függvényét is hívtuk meg igazzal, a gomb engedélyezve lesz. Viszont letiltódni csak akkor fog, ha egyik lapban sincs beállítva ez a moidified flag ,azaz az összes lapból meghívtuk a SetModified függvényt hamissal. Minden kontrolhoz kell egy üzenetkezelő, amiben meghívjuk a SetModified függvényt, ha változott a kontrol állapota. void CSzajPage::OnChange() { SetModified(TRUE); } Ekkor az Apply

gomb is használható lesz. Az OnApply függvény végén meg kell hívnunk a SetModified függvényt hamis paraméterrel, hiszen a változtatásokat érvényesítettük, nincs mit újra Apply-zni. Ha ezt minden lapon megtesszük, akkor az Apply gomb ismét letiltódik Egy OnApply függvény: BOOL CSzajPage::OnApply() { //Adatok bekérése a kontroloktól UpdateData(TRUE); //főablak megkeresése CMainFrame *MainWnd=(CMainFrame ) ((CTabdiagApp ) AfxGetApp())>m pMainWnd; 99 //beállítások érvényesítése MainWnd->m SzajType=m SzajType; MainWnd->m bBajusz=m bBajusz; //újrarajzoltatjuk a főablakot MainWnd->Invalidate(); MainWnd->UpdateWindow(); SetModified(FALSE); return TRUE; } Először is le kell kérdezni a kontrolok állapotát az UpdateData függvénnyel. Ezután a függvény átállítja a főablak adatait, és az Invalidate, majd az UpdateWindow meghívása után kényszeríti, hogy rajzolódjon újra. Úgy tapasztaltam, hogy az OK gomb hatására is

meghívódik az OnApply függvényünk, ha nem írtunk saját OnOK függvényt. Tehát a beállítások érvényesítve lesznek, nem kell az ablak bezáródása után azt az üzenetkezelő függvényből megtennünk. Ha írtunk saját OnOK, vagy OnApply függvényeket amik érvényesítik a beállításokat, akkor már nem kell lekérdezni a beállításokat a lapoktól a DoModal visszatérése után. A bonyolultabb megoldás, a Tab kontrol használata Készítünk egy új dialógusablakot, amire helyezünk egy tab kontrolt is. Íme: A tárgyalásra kerülő módszer nagyvonalakban annyi, hogy a lapokat nem-modális dialógusablakként jelenítjük meg a tab kontrol üres területén. Mindig azt, ami a kiválasztott fülhöz tartozik Megjegyzés:  Modális dialógusablakon azt értjük, hogy a felhasználó csak az ablak bezárása után tud visszatérni az előző ablakra. Modális dialógusablakot a CDialog osztály DoModal függvényével tudunk létrehozni.  A nem-modális

dialógusablak azt jelenti, hogy a felhasználó bármikor visszaválthat az előzőre egy egyszerű egérkattintással, majd onnan vissza, az ablak bezárása nélkül. Nem-modális dialógusablakot a CDialog osztály Create függvényével tudunk létrehozni. Egy így létrehozott ablakot bezárhat a felhasználó például az OK gombbal, 100 de a programból önkényesen mi is be tudjuk zárni az osztály DestroyWindow függvényével. A tab kontrol mérete legyen akkora, hogy a később megjelíteni kívánt lapok elférjenek rajta. Ezután el kell készíteni a lapokat. A lapok egyszerű dialógusablakok lesznek Egy dialógusablak elkészítése:    A dialógusablak beszúrása (Insert/Resource/Dialog), majd megszerkesztése az editorral. Osztály származtatása a CDialog osztályból (Inset/New Class), az imént készített erőforrás ID-jét meg kell adni. Amely kontolok értékeire később kíváncsiak leszünk, rendelünk hozzájuk változókat.(ClassWizard)

A dialógusablak erőforrások tulajdonságai ránk vannak bízva, hiszen később mi fogjuk megjeleníteni az ablakokat. Egy dolog kötelező, hogy legyen Child Ez annyit tesz, hogy ha mozgatjuk a főablakot, a Child mozog vele. Én a példaprogramban a legegyszerűbb megoldást alkalmaztam. Azaz, a lap dialógusablak rendelkezik címsorral, a címét a tab kontrol fülén jelenítem meg, és a lap kirajzolása előtt lekapcsolom a címsort. Ez azért volt a legegyszerűbb, mert az erőforrások már így léteztek az előző egyszerűbb megoldásból. A child dialógusablakok osztályai ugyanezen okból nálam CPropertyPage-ekből vannak származtatva. Ezt csak azért tehettem meg, mert a CPropertyPage a CDialog-ból származik Egyébként új CDialog-ból származtatott osztályokat kellett volna létrehozni ehhez a megoldáshoz. De így is sima CDialog-ként kezelem őket. A módszer azért különösen ronda, mert a CPropertyPage osztálynak van egy csomó tagfüggvénye, ami csak

akkor kap értelmet ha be van fűzve egy CPropertySheet-be. Szóval most képzeljük azt, hogy a három lap osztálya közvetlen a CDialog-ból származik, csak a természetes lustaságom akadályozott meg, hogy így is csináljam. A dolog így is, úgy is működik Megvannak a lapok, azaz a child dialógusablakok, és a tab kontrolos dialógusablak is (mostantól szülőnek nevezzük). A szülő tab kontrolján egyetlen fül sem fog megjelenni addig, amíg hozzá nem adogatjuk egyenként. Ezt a CtabCtrl osztály IsertItem függvényével tehetjük meg. Az ilyen műveleteket természetesen az ablak OnInitDialog függvényéből tehetjük meg. (A WM INITDIALOG üzenethez rendelt kezelőfüggvény) BOOL CMySheet::OnInitDialog() { CDialog::OnInitDialog(); TC ITEM TabCtrlItem; CString Title; int i; //A fülek hozzáadása a tab kontrolhoz. A szöveg az ablak címe lesz TabCtrlItem.mask = TCIF TEXT; for(i=0;i<3;i++) { //hogy megtudjuk, milyen címet tartalmaz az ID által mutatott

eroforras //legegyszerubb, ha keszitunk egy ablakot az ID-vel CDialog dlg; dlg.Create(m IDs[i]); //és magától az ablaktól kérdezzük meg, mi a címe dlg.GetWindowText(Title); dlg.DestroyWindow(); //aztán be is zárjuk 101 //a cím lez a fül szövege TabCtrlItem.pszText = TitleGetBuffer(0); m tab.InsertItem( i, &TabCtrlItem ); } //A Tab Kontrol helye is kelleni fog, hiszen oda kell mozgatni //a child ablakokat m tab.GetWindowRect(&m TabRect); //ez screen koordinatat ad vissza ScreenToClient(&m TabRect); //nekünk Client kell //az első lappal kezdjük m ActualPage=0; ShowPage(m ActualPage); return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FALSE } A szülőnek szüksége van a három lap címére (m pPages tömb), és a hozzájuk rendelt erőforrás IDjére(m IDs tömb). Ezeket még az ablak létrehozása előtt megadtuk Egy fül hozzáadásához szükség van a TC ITEM struktúrára. A mask mezőbe

be kell írni, hogy milyen típusú adatot akarunk hozzárendelni a fülhöz. Ez szöveg esetén TCIF TEXT Ezután a pszTezt mezőbe kell átadni a string címét, és a struktúrával meghívni a kontrol InsertItem függvényét. A fülhöz rendelt szöveg az ablak címe lesz. Mivel az ablakok még nem léteznek, amiknek a címét meg akarjuk tudni, cselhez folyamodunk. Készítünk egy ablakot a fülhöz tartozó ablak dialog ID-jével, és az ablaktól kérdezzük meg mi a címe. Utána rögtön be is zárhatjuk az ablakot A megoldás nem szép de egyszerű. Az igazi az lenne, ha betöltenénk az ID-hez tartozó dialog template-et, és innen kiolvasnánk a címet. Ehhez viszont tudni kéne dialog template-et betölteni, és a pontos szerkezetét Ezt inkább hagyjuk. A gyermek ablakok a tab kontrolon kell majd hogy megjelenjenek, ezért kellenek a tab kontrol koordinátái. Ezt a CWnd osztály GetWindowRect függvénye mondja meg Igaz, ezek képernyőkoordináták. Mivel a lapok

gyermekablakok, a kliens koordinátákra lesz szükségünk az ablakok elhelyezésekor. A koordinátákat a CWnd osztály ScreenToClient függvénye fogja átkonvertálni. Ezután csak meg kell jeleníteni az első lapot. Egy lap megjelenítése: void CMySheet::ShowPage(int n) { //child dialogusablak letrehozasa, az aktiv ablak a szulo, azaz this m pPages[n]->Create(m IDs[n],this); //a tab kontrolra mozgatjuk m pPages[n]->MoveWindow(&m TabRect); //kikapcsoljuk a cimsort, es a letiltast m pPages[n]->ModifyStyle(WS CAPTION | WS DISABLED,NULL); //kirajzoltatjuk m pPages[n]->ShowWindow(SW SHOW); } Az ablakot nem-modálisan hozzuk létre, azaz a Create függvényt használjuk. Első paraméterként az ID-t kell megadni, másodiknak a szülőt. Ezután az ablakot rá kell mozgatni a tab kontrolra A mozgatást a CWnd osztály MoveWindow függvénye végzi el. Ezután ki kell kapcsolni a címsort, és 102 az esetleges letiltást. Az ilyen stílusmódosításokat a CWnd

osztály ModifyStyle függvénye végzi el Első paraméterként az eltávolítandó stílusokat adjuk meg, másodiknak pedig a hozzáadandókat. Most már ott van a gyermekablak, ahol kell, és címsor sincs rajta. Ki kell rajzoltatni a ShowWindow függvénnyel. Nyilván azt akarjuk, hogy mindig a kiválasztott fülhöz tartozó lap legyen látható. Kell egy üzenetkezelő, ami akkor hívódik meg, ha új fülre kattintottunk. A ClassWizard-al a tab kontrol TCN SELCHANGE üzenetéhez kell rendelni egy üzenetkezelőt. Ebben az üzenetkezelőben kell bezárni az éppen látható gyermekablakot, és kirajzoltatni az újat. void CMySheet::OnSelchangeTab(NMHDR* pNMHDR, LRESULT pResult) { //a most lathato child bezarasa m pPages[m ActualPage]->UpdateData(); m pPages[m ActualPage]->DestroyWindow(); //a most kivalasztott megnyitasa m ActualPage=m tab.GetCurSel(); ShowPage(m ActualPage); *pResult = 0; } Fontos kötelességünk, hogy mielőtt bezárnánk az ablakot, a felhasználó

által végzett változtatásokat lekérdezzük. Meghívjuk a gyermek UpdateData függvényét, ezzel a változások megjelennek a kontrolokhoz rendelt tagváltozókban, ahonnan később lekérdezhetjük a beállításokat. A DestroyWindow meghívásával kivégeztük az ablakot. A kiválasztott fül számát a CTabCtrl osztály GetCurSel függvényével tudjuk lekérdezni. Ha megvan, csak meg kell jeleníteni a fülhöz tartozó ablakot. Ezeken kívül van még egy fontos dolgunk. A szülőablak bezárásakor be kell zárni az éppen látható gyermekablakot. A szülőablak WM DESTROY üzenetéhez kell rendelni egy kezelőt void CMySheet::OnDestroy() { //a most lathato child bezarasa m pPages[m ActualPage]->UpdateData(); m pPages[m ActualPage]->DestroyWindow(); CDialog::OnDestroy(); } Persze itt is meg kell hívni az UpdateData függvényt, hogy az utoljára módosított adatok se vesszenek el. Ezzel működőképes a füles dialógusablakunk. Látszólag ugyanazt az eredményt

értük el, mint a jóval egyszerűbb megoldásnál, csak több munkával. Ez nem így van, gondoljunk arra, hogy a szülőablak kinézetét mi magunk szerkesztettük meg. Mi mondjuk meg hol lesz a tab kontrol, mekkora lesz, és milyen gombok, kontrolok szerepeljenek még mellette. Nem csak OK, Cancel, és Apply lesz az ablak alján. Az egyszerűbb megoldásnál ebbe semmi beleszólásunk nincs Ezen kívül némi betekintést kaptunk abba, hogy körülbelül milyen elven működhet a CPropertySheet osztály 103