IV. fejezet - A Microsoft Windows programozása C++ nyelven

Tartalom
IV.1. A CLI specialitásai, a szabványos C++ és a C++/CLI
IV.1.1. A nativ kód fordítási és futtatási folyamata Windows alatt
IV.1.2. Problémák a natív kódú programok fejlesztése és használata során.
IV.1.3. Platformfüggetlenség
IV.1.4. Az MSIL kód futtatása
IV.1.5. Integrált fejlesztő környezet
IV.1.6. A vezérlők, vizuális programozás
IV.1.7. A .NET keretrendszer
IV.1.8. C#
IV.1.9. A C++ bővitése a CLI-hez
IV.1.10. A C++/CLI bővitett adattípusai
IV.1.11. Az előredefiniált referencia osztály: String
IV.1.12. A System::Convert statikus osztály
IV.1.13. A CLI array template-tel megvalósitott tömb referencia osztálya
IV.1.14. C++/CLI: Gyakorlati megvalósitás pl. a Visual Studio 2008-as változatban
IV.1.15. Az Intellisense beépitett segítség
IV.1.16. A CLR-es program típusának beállitása.
IV.2. Az ablakmodell és az alapvezérlők.
IV.2.1. A Form alapvezérlő
IV.2.2. A Form vezérlő gyakran használt tulajdonságai
IV.2.3. A Form vezérlő eseményei
IV.2.4. A vezérlők állapotának aktualizálása
IV.2.5. Alapvezérlők: Label (címke) vezérlő
IV.2.6. Alapvezérlők: TextBox (szövegmező) vezérlő
IV.2.7. Alapvezérlők: a Button (nyomógomb) vezérlő
IV.2.8. Logikai értékekhez használható vezérlők: a CheckBox (jelölő négyzet)
IV.2.9. Logikai értékekhez használható vezérlők: a RadioButton (opciós gomb)
IV.2.10. Konténerobjektum vezérlő: a GroupBox (csoport mező)
IV.2.11. Diszkrét értékeket bevivő vezérlők: a HscrollBar (vízszintes csúszka) és a VscrollBar (függőleges csúszka)
IV.2.12. Egész szám beviteli vezérlője: NumericUpDown
IV.2.13. Több objektumból választásra képes vezérlők: ListBox és a ComboBox
IV.2.14. Feldolgozás állapotát mutató vezérlő: ProgressBar
IV.2.15. Pixelgrafikus képeket megjeleníteni képes vezérlő: a PictureBox (képmező)
IV.2.16. Az ablakunk felső részén lévő menüsor: a MenuStrip (menüsor) vezérlő
IV.2.17. Az alaphelyzetben nem látható ContextMenuStrip vezérlő
IV.2.18. Az eszközkészlet menüsora: a ToolStrip vezérlő
IV.2.19. Az ablak alsó sorában megjelenő állapotsor, a StatusStrip vezérlő
IV.2.20. A fileok használatában segítő dialógusablakok: OpenFileDialog, SaveFileDialog és FolderBrowserDialog
IV.2.21. Az előre definiált üzenetablak: MessageBox
IV.2.22. Az időzítésre használt vezérlő: Timer
IV.2.23. A SerialPort
IV.3. Szöveges, bináris állományok, adatfolyamok.
IV.3.1. Előkészületek a fájlkezeléshez
IV.3.2. A statikus File osztály metódusai
IV.3.3. A FileStream referencia osztály
IV.3.4. A BinaryReader referencia osztály
IV.3.5. A BinaryWriter referencia osztály
IV.3.6. Szövegfájlok kezelése: StreamReader és StreamWriter referencia osztályok
IV.3.7. A MemoryStream referencia osztály
IV.4. A GDI+
IV.4.1. A GDI+használata
IV.4.2. A GDI rajzolási lehetőségei
IV.4.3. A Graphics osztály
IV.4.4. Koordináta-rendszerek
IV.4.5. Koordináta-transzformáció
IV.4.6. A GDI+ színkezelése (Color)
IV.4.7. Geometriai adatok (Point, Size, Rectangle, GraphicsPath)
IV.4.7.1. Méretek tárolása
IV.4.7.2. Síkbeli pontok tárolása
IV.4.7.3. Síkbeli téglalapok tárolása
IV.4.7.4. Geometriai alakzatok
IV.4.8. Régiók
IV.4.9. Képek kezelése (Image, Bitmap, MetaFile, Icon)
IV.4.10. Ecsetek
IV.4.11. Tollak
IV.4.12. Font, FontFamily
IV.4.13. Rajzrutinok
IV.4.14. Nyomtatás
Irodalmak:

Ebben a fejezetben a C++ nyelv Windows-specifikus alkalmazását mutatjuk be.

IV.1. A CLI specialitásai, a szabványos C++ és a C++/CLI

A Windows operációs rendszert futtató számítógépre többféle módon fejleszthetünk alkalmazásokat:

  1. Valamely fejlesztő környezet segítségével elkészítjük az alkalmazást, ami ebben a futtató környezetben fog működni. Az operációs rendszer által a file közvetlenül nem futtatható (pl. MatLab, LabView), mert nem a számítógép CPU-ja, hanem a futtató környezet számára tartalmaz parancsokat. Néha a fejlesztő környezet mellett tiszta futtató (run-time) környezet is rendelkezésre áll a kész alkalmazás használatához, vagy a fejlesztő készít a programunkból egy futtatható (exe) állományt, amelybe beleteszi a futtatáshoz szükséges run-time-ot is.

  2. A fejlesztő környezet elkészít a forrásfilejainkból egy önállóan futtatható alkalmazás fájlt (exe), amely az adott operációs rendszeren és processzoron futtatható gépi kódú utasításokat tartalmazza (natív kód). A fejlesztés közbeni kipróbálás során is ezt a fájlt futtatjuk. Ilyen eszközök pl. a Borland Delphi és az iparban jellemzően használt Microsoft Visual Studio.

Mindkét fejlesztési mód jellemzője, hogy amennyiben grafikus felhasználói felülete lesz az alkalmazásunknak, a felhasznált elemeket grafikus editorral hozzuk létre, fejlesztés közben látható az elem működés közbeni állapota. Ezt az elvet RAD-nak (gyors alkalmazásfejlesztés) nevezzük. A C++ nyelven való fejlesztés a 2. csoportba tartozik, míg a C++/CLI-ben mindkét alkalmazásfejlesztési módszer megjelenik.

IV.1.1. A nativ kód fordítási és futtatási folyamata Windows alatt

Amennyiben Windows alatt úgynevezett konzolalkalmazást készítünk, a szabványos C++-nak megfelelő szintaktikát alkalmaz a Visual Studio. Így fordíthatóak le Unix, Mac vagy más rendszerre készített programok, programrészletek (pl. a WinSock a sockets programcsomagból, vagy a MySql adatbáziskezelő). Ekkor a következő a fordítás folyamata

  • A C++ forrás(ok) .cpp kiterjesztésű fájlokban, headerek .h kiterjesztésű állományokban tároltak. Ezekből több is lehet, ha a logikailag összetartozó részeket külön helyeztük el, vagy ha a programot többen fejlesztették.

  • Előfeldolgozó (preprocesszor): #define definíciók feloldása, #include állományok beszúrása a forrásba

  • Előfeldolgozott C forrás: minden szükséges függvénydefiníciót tartalmaz.

  • C fordító: előfeldolgozott forrásokból relokálható .OBJ tárgymodult készít.

  • OBJ fájlok: gépi kódú programrészleteket (ezek nevét publikussá téve – export) és külső (external) hivatkozásokat tartalmaznak más fájlokban lévő részletekre.

  • Linker: az OBJ fájlokból és az előre lefordított függvényeket (pl. printf()) tartalmazó .LIB kiterjesztésű állományokból a hivatkozások feloldása, és a felesleges függvények kitakarítása, valamint a belépési pont (main() függvény) megadása után elkészül a futtatható .EXE kiterjesztésű fájl, ami az adott processzoron futtatható gépi kódú utasításokat tartalmazza.

IV.1.2. Problémák a natív kódú programok fejlesztése és használata során.

Mint azt az előző fejezetekben láttuk, a natív kódú programokban a programozó igény szerint használhat dinamikus memóriafoglalást az adatoknak/objektumoknak. Ezeknek a változóknak csak címük van, névvel nem hivatkozhatunk rájuk, csak pointerrel, a címüket a pointerbe töltve. Például, ilyen memóriafoglalás a malloc() függvény és a new operátor kimenete, amely lefoglal egy egybefüggő területet, és visszaadja annak címét, amit valamilyen pointerbe teszünk be értékadó utasítással. Ezután a változót (a pointeren keresztül) használhatjuk, sőt a területet fel is szabadíthatjuk. A pointer hatékony, de veszélyes eszköz: értéke pointeraritmetikával megváltoztatható, úgy, hogy már nem az általunk lefoglalt memóriaterületre mutat, hanem azon túl. Tipikus példa kezdők esetén a tömböknél fordul elő: készítenek egy 5 elemű tömböt (a definícióban 5 szerepel), majd hivatkoznak az 5 indexű elemre, amely jó esetben valahol a programjuk memóriaterületén található, csak nem a tömbben (az elemek indexei 0..4-ig értelmezhetőek). Értékadó utasítás használata esetén az adott érték szinte vakon bekerül a tömb mellé a memóriába, az ott lévő, másik változót „véletlenül” megváltoztatva. Ez a hiba lehet, hogy rejtve is marad előttünk, mert a másik változó értékének megváltozása fel sem tűnik, csak a „program néha furcsa eredményeket ad”. Könnyebb a hibát észrevenni, ha a pointer már nem a saját memóriaterületünkre mutat, hanem pl. az operációs rendszerére. Ekkor ugyanis hibaüzenet keletkezik, és az operációs rendszer a programunkat „kidobja” a memóriából.

Ha nagyon odafigyelünk a pointereinkre, a véletlenszerű megváltoztatást elkerüljük. Nem kerülhetjük el viszont a memória fragmentálódását. Ugyanis, amennyiben nem pontosan fordított sorrendben szabadítjuk fel a memóriablokkokat a foglaláshoz képest, a memóriában „lyukak” keletkeznek – két foglalt blokk közt egy szabad blokk. Többfeladatos operációs rendszer esetén a többi program is használja a memóriát, így pontosan fordított sorrendű felszabadítás esetén is keletkeznek lyukak. A foglalásnak mindig egybefüggőnek kell lennie, így ha nagyobb terület kell a felhasználónak, mint a szabad blokk, azt nem tudja lefoglalni, ott marad felhasználatlanul a kisméretű memóriablokk. Vagyis a memória „töredezik”. A jelenség ugyanaz, mint a háttértárolók töredezettsége az állományok törlése, felülírása után. A háttértárolóhoz van segédprogram, amely az állományokat egybefüggő területen helyezi el, viszonylag hosszú idő alatt, a natív kódú programok memóriájához nincs töredezettségmentesítő. Ugyanis nem tudhatja az operációs rendszer, hogy melyik memóriablokkunk címét melyik pointerünk tartalmazza, a blokk mozgatása esetén a pointerbe a blokk új címét kellene betöltenie.

Vagyis két igény fogalmazódik meg a memóriával kapcsolatban: egyik a változók véletlenszerű megváltozásának, illetve az operációs rendszer által leállított programok elkerülése (mindenki látott már kék képernyőt) a program változóinak menedzselésével, valamint a takarítás, rendrakás, szemétgyűjtés (garbage collection). Az alábbi, MSDN-ről származó ábra a garbage collection működését mutatja be, takarítás előtt (IV.1. ábra) és takarítás (GC::Collect()) után (IV.2. ábra)

Memória takarítás előtt
IV.1. ábra - Memória takarítás előtt


Memória takarítás után
IV.2. ábra - Memória takarítás után


Látható, hogy azok a memóriaterületek (objektumok), amelyekre nem hivatkozik semmi, eltűntek, a hivatkozások az új címekre mutatnak, a szabad terület helyét kijelölő pointer alacsonyabb címre került (vagyis nagyobb lett a szabad egybefüggő memória).

IV.1.3. Platformfüggetlenség

A számítógépek egy része mára kiköltözött a kezdeti fémdobozból, mindenhova magunkkal visszük a zsebünkben, esetleg ruharabként vagy kiegészítőként viseljük. Ezeknél a számítógépeknél (nem is így hívjuk már őket: telefon, e-könyv olvasó, tablet, médialejátszó, szemüveg, autó) a viszonylag kisebb számítási teljesítmény mellett a fogyasztás minimalizálására törekednek a gyártók, ugyanis nem áll rendelkezésre a működtetéshez néhány 100 W-os teljesítmény. Az Intel (és másodgyártói) processzorok inkább a számítási teljesítményre összpontosítottak, így a mobil, telepes táplálású eszközökbe a többi gyártó (ARM, MIPS, stb) CPU-jai kerültek. A programfejlesztők dolga elbonyolódott: minden egyes CPU-ra (és platformra) el kellett (volna) készíteni az alkalmazást. Célszerűnek látszott egy futtató környezetet kialakítani platformonként, majd az alkalmazásokat csak egyszer elkészíteni, és lefordítani valamely köztes kódra, a szellemi termék védelme érdekében. Ezt jól ismerte fel a Sun Microsystems, amikor a C és C++ nyelvekből megalkotta az egyszerű objektummodellel rendelkező, pointermentes Java nyelvet. A Java nyelvről úgynevezett bájtkódra fordítják az alkalmazást, amelyet egy virtuális gépen (Java VM) futtatnak, azonban natív kóddá is alakítható, és futtatható. Manapság több ismert platform is használja a közben Oracle tulajdonba került nyelvet: pl. ilyen a Google által felkarolt Android operációs rendszer. Természetesen ahol védjegy, ott pereskedés is van: az Oracle nem engedélyezte a Java név használatát, mert „tudatosan megsértették a szellemi jogait.” Hasonlóan járt a Microsoft is a Windows-ba épített Java-val: a mai Windows-ban nincs Java támogatás, a JRE-t (a futtató környezet) vagy a JDK-t (a fejlesztő környezet) az Oracle oldaláról kell letölteni. A PC-s világban enélkül is fennáll még néhány évig egy átmeneti állapot: a 32-bites operációs rendszer 4 GB memóriát már nem tud kezelni. Az AMD kifejlesztette a 64-bites utasításkészlet bővítést, amit később az Intel is átvett. A Windows az XP óta két memóriakezeléssel kapható: 32-bites a régebbi, 4 GB alatti memóriájú PC-khez, és a 64-bites az újabb CPU-jú, 4 GB-ot vagy többet tartalmazó PC-khez. A 64-bites változat egy emuláció segítségével (WoW64 – Windows on Windows) futtatja a régebbi, 32-bites alkalmazásokat. Amikor egy programot lefordítunk 64-bites operációs rendszer alatt a Visual Studio 2005 vagy újabb (2008, 2010, 2013) változattal, kiválaszthatjuk az „x64” üzemmódot is, ekkor 64-bites alkalmazást kapunk. Adódott tehát az igény, hogy egy program fusson mindkét konfigurációjú (x86, x64) és az összes változatú (XP,Vista, 7,8) Windows operációs rendszeren is úgy, hogy a fordítás pillanatában még nem tudjuk, hogy milyen környezet lesz később, de nem szeretnénk több exe fájlt készíteni. Ez a követelmény, mint ahogy láttuk, csak egy közbenső futtató/fordító szint közbeiktatásával lehetséges. Szükséges lehet még nagyobb programok esetén, hogy többen is részt vegyenek a fejlesztésben, esetleg különböző programozási nyelvek ismeretével. A közbenső szint esetén erre is lehetőség van: minden nyelvi fordító (C++, C#, Basic, F# stb.) erre a közbenső (intermediate) nyelvre fordít, majd az alkalmazás erről fordul le futtathatóra. A közbenső nyelvet elnevezték MSIL-nek, amely egy gépi kódra hasonlító, veremorientált nyelv. Az MSIL elnevezést, amelynek első két betűje a gyártó cég nevét jelenti, később CIL-re változtatták, vagyis „Common Intermediate Language”, amelyet úgy is tekinthetjük, mint a Microsoft megoldását a Java alapötletére.

IV.1.4. Az MSIL kód futtatása

Az előző bekezdésben ismertetett CIL kód egy .EXE kiterjesztésű fájlba kerül, ahonnan futtatható. Azonban ez a kód nem a processzor natív kódja, így az operációs rendszernek fel kell ismernie, hogy még egy lépésre van szükség. Ez a lépés kétféle lehet, a Java rendszerben használt elveknek megfelelően:

  1. az utasításokat egyenként értelmezve és lefuttatva. A módszert JIT (Just In Time) futtatásnak nevezzük. A forrásszöveg lépésenkénti futtatásánál és töréspontokat tartalmazó hibakeresésénél (debug) célszerű a használata.

  2. az összes utasításból egyszerre natív kódot generálva és elindítva. A módszer neve AOT (Ahead of Time), és a Native Image Generator (NGEN) képes elkészíteni. Jól működő, letesztelt, kész programoknál (release) használjuk.

IV.1.5. Integrált fejlesztő környezet

Az eddig tárgyalt natív kód fejlesztési folyamatában még nem esett szó a felhasznált programeszközökről. Kezdetben minden lépést 1 vagy több (parancssoros) program látott el: a fejlesztő tetszőleges szövegszerkesztővel létrehozta/bővítette/javította a .C és .H forrásfájlokat, majd az előfeldolgozó és a C fordító, végül linkelő következett. Ha hibakereső (debug) módban futtatta az alkalmazást, ez egy újabb programot (a debugger-t) jelentette. Amennyiben a program több forrásfájlból állt, azok közül csak a módosítottakat kell újrafordítani. Ezt a célt szolgálta a make segédprogram. Ha a források közt keresni kellett (pl. melyik .H fileban található egy általunk készített függvény definíciója), akkor a grep segédprogramot használhattuk. A fordításhoz gyakran készültek batch fájlok is, amelyek a fordítót megfelelően felparaméterezték. Fordítási hiba esetén kiíródott a konzolra a hibás sor száma, a szövegszerkesztőt újra betöltöttük, odanavigáltunk a hibás sorra, kijavítottuk a hibás sort, majd újra fordítottunk. Ha lefordult, és elindult, néha hibás eredményt adott. Ekkor a debuggerrel futtattuk, majd a hibás programrészlet megtalálása után megint a szövegszerkesztő következett. Ez a folyamat a sok programindítgatás, információ kézi bevitel (sorszám) miatt nem volt hatékony. Azonban már a 70-es, 80-as években, a PC előtti korszakban készültek termékek, amelyek tartalmazták a szövegszerkesztőt, a fordítót és a futtatót is. Ezt az elvet nevezték integrált fejlesztő környezetnek (IDE). Ilyen IDE környezet volt a Borland által készített Turbo Pascal is, amely már a 8 bites számítógépeken egyetlen programban tartalmazott szövegszerkesztőt, fordítót, és futtatót (debuggert ekkor még nem). A programot egy bizonyos Anders Hejlsberg készítette, aki a később a Microsoftnál dolgozott, programozási nyelvek kifejlesztésén. Ilyen nyelvek például a J++ és a C#. A Microsoftnál is készült IDE eszköz karakteres képernyőre: a DOS-ban lévő BASIC-et felváltotta az editort és debuggert is tartalmazó Quick Basic.

IV.1.6. A vezérlők, vizuális programozás

A grafikus felületű (GUI) operációs rendszereken futtatható alkalmazások legalább két részből állnak: a program algoritmusát tartalmazó kódrészből és a felületet megvalósító felhasználói interfészből (UI). A két rész logikailag össze van kötve: a felhasználói interfészben történő események (event) kiváltják az algoritmus rész meghatározott alprogramjainak (C típusú nyelvekben ezeket függvénynek hívjuk) lefutását. Emiatt ezeket a függvényeket „eseménykezelő függvény”-nek hívjuk, és az operációs rendszer fejlesztő készletében (SDK) találhatunk is a megírásukhoz szükséges definíciókat fejállományokban. Kezdetben a felhasználói felülettel rendelkező programok tartalmazták az UI-hoz szükséges programrészleteket is: 50-100 soros C-nyelvű program képes volt egy üres ablakot megjeleníteni a Windows-ban, és az „ablak zárása” eseményt lekezelni (vagyis be lehetett csukni). Ekkor a fejlesztés nagy részét az UI elkészítése tette ki, az algoritmus programozása csak ezután jöhetett. Az UI programban minden koordináta pl. számként szerepelt, amelynek módosítása után a programot újra lefordítva megnézhettük, hogy a változtatás után miként néz ki a felület. A Microsoft első ilyen terméke (egyúttal a mai feljesztőeszköz névadója) a Visual Basic volt, amelynek első változatában előre legyártott vezérlőket tudtunk egy GUI-val az űrlapunkra (amely tulajdonképpen a készülő programunk felhasználói felülete) rátenni. A felhasználó által megrajzolt vezérlőkből szöveg formátumú kód készült, amely szükség esetén a beépített szövegszerkesztővel módosítható volt, majd a futtatás előtt lefordult. A futtatáshoz szükség volt egy könyvtárra, amely a vezérlők futtatható részét tartalmazta. Jellemzően emiatt kisméretű exe-fájlok készültek, de az elkészült programhoz mindig telepíteni kellett a run-time környezet azon verzióját, amelyben a program készült. A Visual Basic-et később a Visual C++ (VC++) és társai követték, majd – az office mintájára – a különálló termékek helyett egyetlen termékbe integrálták a fejlesztőeszközöket – ezt nevezték el Visual Studio-nak.

IV.1.7. A .NET keretrendszer

Az eddig tárgyalt elveket megvalósító programrészleteket a Microsoft egy közös, egyetlen fájlból telepíthető szoftvercsomaggá fogta össze, és elnevezte .net-nek. A fejlesztés során több verziója is megjelent, jelen könyv írásakor a 4.0 a stabil, és a 4.5 az előzetes teszt változat. A telepítéshez természetesen szükség van a Windows típusára is, minden Windows-hoz, minden CPU-ra valamint 32, 64 biten különböző változatokat kell telepíteni (lásd a "platformfüggetlenség" fejezetben.

A keretrendszer részei:

  • Common Language Infrastructure (CLI): ennek implementálásaként Common Language Runtime (CLR): a közös nyelvi fordító és futtató környezet. MSIL compilert, debuggert és run-timeot tartalmaz. Többek között képes a memóriában szemétgyűjtésre (Garbage Collection, GC) és a kivételek kezelésére (Exception Handling).

  • Base Class Library: az alaposztályok könyvtára. A GUI-k kényelmesen csak OOP-ben programozhatók, jól elkészített alaposztályokkal. Ezeket közvetlenül nem tudjuk példányosítani (legtöbbször nem is lehet: absztrakt osztályokról van szó). Például tartalmaz egy "Object" nevű interface osztályt (lásd IV.1.10. szakasz).

  • WinForms: a Windows alkalmazások számára előre elkészített vezérlők, a Base Class Library-ból leszármaztatva. Ezeket rakjuk fel a formra fejlesztés közben, belőlük áll majd a programunk felülete. Nyelvfüggetlen vezérlők, minden alkalmazásból használhatjuk őket, az adott nyelv szintaktikájának megfelelően. Fontos megjegyezni, hogy a programunk nemcsak a fejlesztés alatt rárajzolt vezérlőket használhatja, hanem futás közben is létrehozhat ezek közül példányokat. Ugyanis, a rárajzolás közben is egy programkód-részlet keletkezik, ami a programunk indításakor lefut. A létrehozó kódrészlet általunk is megírható (odamásolható), és később is lefuttatható.

  • Egyéb részek: ide tartoznak például a webes alkalmazásfejlesztést támogató ASP.NET rendszer, az adatbázisok elérését lehetővé tevő ADO.NET, és a többprocesszoros rendszereket támogató Task Parallel Library. Ezekkel terjedelmi okok miatt nem foglalkozunk.

IV.1.8. C#

A .NET keretrendszert és a tisztán menedzselt kódot egyszerűen programozhatjuk C# nyelven. A nyelv készítője Anders Hejlsbergm aki a C++ és Pascal nyelvből származtatta, azok pozitívumait megtartotta, és leegyszerűsítette, a nehezebben használható elemeket (pl. pointerek) opcionálissá tette. Hobbistáknak és általában felsőoktatásban tanulóknak ajánlják (nem programozóknak – nekik a K&R C, valamint a C++ az univerzálisan használható eszközük). A .NET keretrendszer tartalmaz parancssoros C# fordítót, és ingyenesen letölthetünk Visual C# Express Edition-t a Microsofttól. Céljuk ezzel az, hogy a C# (és a .NET rendszer) terjedjen. Magyar nyelvű, ingyenes tankönyveket ugyancsak találhatunk az Interneten a C#-hoz.

IV.1.9. A C++ bővitése a CLI-hez

A Microsoft által készített C++ fordító, amennyiben natív Win32 alkalmazást fordítunk vele, szabványos C++-nak tekinthető. Azonban a CLI eléréséhez új adattípusokra és műveletekre volt szükség. A menedzselt kód (GC) kezeléséhez szükséges utasítások először a Visual Studio .NET 2002-es változatában jelentek meg, majd a 2005-ös változatban egyszerűsödtek. A definiált nyelv nem tekinthető C++-nak, mivel annak szabványos definíciójába (ISO/IEC 14882:2003) nem férnek bele a GC utasításai és adattípusai. A nyelvet C++/CLI-nek nevezték el, és szabványosították (ECMA-372). Megjegyezzük, hogy a szabványosítás célja általában az, hogy a többi gyártó is képes legyen kapcsolódó termékekkel a piacra lépni, esetünkben ez nem történt meg: a C++/CLI-t csak a Visual Studio képes lefordítani.

IV.1.10. A C++/CLI bővitett adattípusai

A menedzselt halmon lévő változókat máshogy kell deklarálnunk, mint a natív kód változóit. Azért nem automatikus a helyfoglalás, mert a fordító nem dönthet helyettünk: a natív és a menedzselt kód egy programon belül keverhető (erre csak a C++/CLI képes, a többi fordító mindig tisztán menedzselt kódot fordít, pl. C#-ban nincs natív int típus, az Int32 (aminek rövidítése az int) már egy osztály). A C++-ban a menedzselt halmon lévő osztályt referencia osztálynak (ref class) nevezzük. Ezzel a kulcsszóval kell deklarálni is, ugyanúgy, mint a natív osztályt. Pl. a .NET rendszer tartalmaz egy beépített "ref class String" típust, ékezetes karakterláncok tárolására és kezelésére. Ha "CLR/Windows Forms Application"-t készítünk a Visual Studio-val, a programunk ablaka (Form1) egy referencia osztály lesz. A referencia osztályon belül natív osztály nem definiálható. A referencia osztály a C++ osztályhoz képest máshogy viselkedik:

  • Statikus példány nem létezik belőle, csak dinamikus (vagyis programkódból kell létrehoznunk a példányát). Az alábbi deklaráció hibás: String szoveg;

  • Nem pointer mutat rá, hanem handle (kezelő), amelynek jele a ^. A handle-nek pointerszerű tulajdonságai vannak, például a tagfüggvényre hivatkozás jele a ->. Helyes deklaráció: String ^szoveg; - ekkor a szövegnek nincs még tartalma, ugyanis az alapértelmezett konstruktora üres, 0 hosszúságú sztringet (””) készít.

  • Létrehozásakor nem a new operátort használjuk, hanem a gcnew-t. Példa: szöveg= gcnew String(""); 0 hosszú string létrehozása konstruktorral. Ide már nem kell a ^ jel, használata hibás lenne.

  • A megszüntetése nem a delete operátorral történik, hanem a kezelő nullptr értékűvé tételével. Ekkor a garbage collector idővel automatikusan felszabadítja a felhasznált területet. Példa: szoveg=nullptr; A delete is használható, az lefuttatja a destruktort, de az objektum bent marad a memóriában.

  • Csak publikusan tud öröklődni és csak egy őstől (többszörös öröklődést interface osztállyal tudunk megvalósítani).

  • Készíthető úgynevezett interior pointer a referencia osztályra, amit a garbage collector aktualizál. Ezzel a menedzselt kód biztonsági előnyeit (pl. memória túlcímzés megakadályozása) elveszítjük.

  • A referencia osztálynak – a natívhoz hasonlóan – lehetnek adattagjai, metódusai, konstruktorai (túlterheléssel). Készíthetünk tulajdonságokat (property) is, amelyek vagy önmagukban tartalmazzák az adatot (triviális property), vagy függvények (skaláris property) az adat ellenőrzött eléréséhez (pl. az életkor nem állítható be negatív számnak). A property lehet virtuális is, valamint többdimenziós is, ekkor indexe is lesz. A property nagy előnye a natív C++ tagadat-elérő függvényéhez képest, hogy nincs zárójel utána. Példa: int hossz=szoveg -> Length; a Length a csak olvasható tulajdonság, a stringben lévő karakterek számát adja.

  • A destruktoron kívül, ami az osztály megszüntetésekor lefut (emiatt determinisztikusnak is hívhatjuk) tartalmazhat finalizer() metódust is, ami a GC (garbage collector) hív meg akkor, amikor a memóriából kitakarítja az objektumot. Nem tudjuk, hogy a GC mikor hívja meg a finalizert, emiatt nem-determinisztikusnak is hívhatjuk.

  • Az abstract és override kulcsszavakat minden esetben kötelező kiírni, ha virtuális metódust vagy adatot tartalmaz az ős.

  • Az összes adat és metódus, ha nem írunk elérési módosítót, private lesz.

  • Ha egy virtuális függvénynek nincs kifejtése, abstract-nak kell dekralálni: virtual típus függvénynév() abstract; vagy virtual típus függvénynév() =0; (az =0 a szabvány C++, az abstract =0-nak lett definiálva). Kötelező felüldefiniálni (override) az örökösben. Ha nem akarjuk felülírni a (nem tisztán) virtuális metódust, new kulcsszóval újat is készíthetünk belőle.

  • A referencia osztálynál megadható, hogy belőle ne lehessen öröklődéssel újabb osztályt létrehozni (a metódusok felülbírálásával), csak példányosíthatjuk. Ekkor az osztályt sealed-nek definiáljuk. Sok előre definiált osztályt tartalmaz a C++/CLI nyelv, amelyeket nem változtathatunk meg, ilyen pl. a már említett String osztály.

  • A többszörös öröklődéshez készíthetünk interface osztálytípust. Referencia helyett interface class/struct is írható (interface-nél ugyanaz a jelentésük). Az interface összes tagja (adattagok, metódusok, események, property-k) elérése automatikusan public. A metódusok, property-k nem kifejthetők (kötelezően abstract), az adatok pedig csak statikusak lehetnek. Konstruktor sem definiálható. Az interface nem példányosítható, csak ref/value class/struct hozható létre belőle, öröklődéssel. Az interfészből másik interface is örököltethető. Egy származtatott (derived) referencia osztálynak (ref class) akárhány interface lehet a szülő (base) osztálya. Az interface osztályt az osztályhierarchia elején szokás alkalmazni, például ilyen az Object nevű osztály, amelyet szinte mindenki örököl.

  • Adattároláshoz value class-t használhatunk. Erre nem kezelő hivatkozik, statikus osztálytípus (vagyis egy speciális jel nélküli változó). Csak interface osztályból származhat (vagy öröklődés nélküli, helyben lehet definiálni).

  • A függvénypointerek mellett egy referencia osztály metódusaihoz definiálhatunk delegate-t is, amely egy önállóan használható eljárásként jelenik meg. Az eljárás biztonságos, a natív kódú pointereknél lehetséges típuskeveredési hibák nem lépnek fel. A delegate-t alkalmazza a .NET rendszer a vezérlők eseményeihez (event) tartozó eseménykezelő metódusok meghívására és beállítására is.

Az alábbi táblázatban összefoglaltuk a memóriafoglalás és felszabadítás műveleteit:

Művelet

K&R C

C++

Managed C++ (VS 2002) (már nem használt)

C++/CLI (VS 2005-)

Memória foglalás az objektumnak (dinamikus változó)

malloc(…), calloc()

new …

_gc new ...

gcnew ...

Memória felszabaditása

free(...)

delete ...

Automatikus, ...=nullptr után GC::Collect()

<- mint a 2002-ben

Hivatkozás az objektumra

Mutató (*)

Mutató (*)

_nogc Pointer: natív adatra,

_gc Pointer: menedzselt adatra

Mutató (*): natív adatra,

Kezelő (^): menedzselt adatra

IV.1.11. Az előredefiniált referencia osztály: String

A C++ string típus mintájára készült egy System::String osztály, szövegek tárolására. Definíciója: public sealed ref class String. A szöveget Unicode karakterek (wchar_t) sorozatával tárolja (ékezettel semmi probléma nincs, programszövegben az „L” betű kirakása a konstans elé nem kötelező, a fordító „odaképzeli”: L”cica” és ”cica” is használható). Alapértelmezett konstruktora 0 hosszú szöveget (””) hoz létre. Többi konstruktora lehetővé teszi, hogy char *-ból, natív string-ből, wchar_t *-ból, egy sztringeket tartalmazó tömbből hozzuk létre. Miután referencia osztály, handle-t (^) készítünk hozzá, és tulajdonságait, metódusait ->-lal érjük el. Gyakran használt tulajdonságok és metódusok:

  • String->Length hossz. Példa: s=”cicamica”; int i=s->Length; után i értéke 8 lesz.

  • String[hanyadik] karakter (0.. mint a tömböknél). Példa: s[1] értéke az ‘i’ karakter lesz.

  • String->Substring(hányadiktól, mennyit) részlet kiemelése. Példa: s->Substring(1,3) értéke ”ica” lesz.

  • String->Split(delimiter) : a sztringet szétszedi az elválasztóval (delimiter), a benne található szavak tömbjére. Példa: s=”12;34”; t=s->Split(‘;’); után t egy 2-elemű, sztringeket tartalmazó tömb lesz (deklarálni kell a sztringtömböt), 0. eleme ”12”, 1. eleme ”34”.

  • miben->IndexOf(mit) keresés. Egy számot kapunk vissza, a mit paraméter kezdő pozícióját a miben sztringben (0-val, mint tömbindexszel kezdve). Ha nem találta meg a részletet, -1-et ad vissza. Vigyázat, nem 0-t, mert a 0 érvényes karakterpozíció. Például (a „cicamica” értékű s-sel) s->IndexOf(“mi”) értéke 4 lesz, s->IndexOf(“kutya”) értéke -1.

  • Szabványos operátorok definiálva: ==, !=, +, +=. A natív (char *) sztringeknél az összehasonlító operátor a két pointer egyezését vizsgálta, és nem a tartalmak egyezőségét. A String típusnál operátor túlherheléssel a tartalmak egyezését vizsgálja az == operátor. Az összeadás operátor konkatenációt jelent. Például s+”, haj” értéke ”cicamica, haj” lesz.

  • String->ToString() is létezik, öröklés miatt. Gyakorlati jelentősége nincs, hiszen az eredeti stringet adja vissza. Viszont nincs natív C sztring-re (char *) konvertáló metódus. Lássunk egy példafüggvényt, amely ezt a konverziót elvégzi:

    char * Managed2char(String ^s) 
    {
      int i,hossz=s->Length;
      char *ered=(char *)malloc(hossz+1); // hely a konvertált stringnek
      memset(ered,0,hossz+1); // végjelekkel töltjük fel a konvertáltat.
      for (i=0; i<hossz;i++) // végigmegyünk a karaktereken
          ered[i]=(char)s[i];     //itt fogunk kapni egy warning-ot: s[i] 
    //2 byteon tárolt unicode wchar_t típusú
    //karakter. Ebből ascii-t konvertálva az 
    //ékezetek el fognak tűnni.
      return ered; // visszatérünk az eredményre mutató pointerrel.
    }
    

IV.1.12. A System::Convert statikus osztály

Adatainkat mindig célszerűen megválasztott típusú változókban tároljuk. Ha például meg kell számolni egy út adott pontján az óránként áthaladó járműveket, többnyire int típust alkalmazunk, még akkor is, ha tisztában vagyunk vele, hogy a változó értéke sosem lesz negatív. A negatív értéket (amely a változónak adható, hiszen előjeles) ebben a példában valamely kivétel (még nem volt mérés, hiba stb.) jelzésére használhatjuk. Az int (és a többi numerikus típus is) a memóriában binárisan tárolja a számokat, lehetővé téve az aritmetikai műveletek (például összeadás, kivonás elvégzését, a matematikai függvények (például sqrt, sin) hívását. Amikor felhasználói adatbevitel történik (a programunk bekér egy számot), a felhasználó karaktereket gépel be. A 10-es számot egy ‘1’ és egy ‘0‘ karakter begépelésével írja be, amiből egy sztring keletkezik: ”10”. Ha ehhez hozzá szeretnénk adni 20-at, és hasonlóan, sztringként vittük be, az eredmény ”1020” lesz, a String osztály „+” operátora ugyanis egymás után másolja a sztringeket. A natív Win32 kód scanf() függvénye, és a cin szabványos beviteli adatfolyam használatakor, ha numerikus típusba került az input, az olvasás közben létrejött a konverzió, a scanf paraméterében, vagy a cin >> operátora után lévő típusra. A windows-os előre elkészített bemeneti vezérlők esetében ez nincs így: azok kimenete mindig String típusú. Hasonlóan a kimenethez is mindig String típust kell előállítanunk, mert csak ezt tudjuk megjeleníteni a vezérlőkön. A programok közti kommunikációra (export/import) használt szöveges állományok is sztringekből állnak, amelyekben számok, vagy egyéb adattípusok (dátum, logikai, valuta) is lehetnek. A System névtér tartalmaz egy Convert nevű osztályt, amelynek statikus metódusai nagyszámú túlterheléssel (overload) segítik az adatkonverziós munkát. A leggyakoribb szöveg <-> szám konverzióhoz a Convert::ToString(NumerikusTipus) és a Convert::ToNumerikusTipus(String) metódusok definiáltak. Például, ha a fenti példában s1=”10” és s2=”20”, így adjuk őket össze egészként értelmezve:

int osszeg=Convert::ToInt32(s1)+Convert::ToInt32(s2);

Amennyiben s1, vagy s2 nem konvertálható számmá (például valamelyik 0 hosszú, vagy illegális karaktert tartalmaz), kivétel keletkezik. A kivétel egy try/catch blokkal lekezelhető. Valós számok esetében még egy dologra kell figyelni: ezek a nyelvi beállítások. Mint ismeretes, Magyarországon a számok tizedes részét vesszővel választjuk el az egész résztől: 1,5. Az angol nyelvterületen erre a célra a pontot használják: 1.5. A program szövegében mindig pontot használunk a valós számoknál. A Convert osztály viszont a nyelvi beállításoknak (CultureInfo) megfelelően végzi el a valós <-> sztring konverziót. A CultureInfo be is állítható az aktuális programhoz, ha például kaptunk egy szöveges állományt, amely angol valós számokat tartalmaz. Az alábbi programrészlet átállítja a saját kultúra információját, hogy jól tudja az ilyen file-t kezelni:

// egy kultúra típusú referencia osztály példánya.
System::Globalization::CultureInfo^ c; 
// Mintha az USA-ban lennénk
c= gcnew System::Globalization::CultureInfo("en-US"); 
System::Threading::Thread::CurrentThread->CurrentCulture=c; 
// innentől a programban a tizedesjel a pont, a lista elválasztó a vessző

A Convert osztály metódusai megjelenhetnek az adatosztály metódusai közt is. Például az Int32 osztállyal létrehozott példánynak is van ToString() metódusa a sztringgé konvertáláshoz, és Parse() metódusa a sztringből konvertáláshoz. Ezek a metódusok többféleképpen is paraméterezhetők. Gyakran használunk számítógép/hardverközeli programokban hexadecimális számokat. Az alábbi példa soros porton keresztül, hexadecimális számokat is tartalmazó sztringekkel kommunikál egy külső hardverrel:

if (checkBox7->Checked) c|=0x40;
if (checkBox8->Checked) c|=0x80;
sc="C"+String::Format("{0:X2}",c);    // 2 jegyű hex szám lett a byte típusú 
//c-ből. A C parancs, ha c értéke 
//8 volt: „C08” fog kimenni, ha 255 volt //„CFF”.
serialPort1->Write(sc); // kiküldtük a hardvernek
s=serialPort1->ReadLine(); // visszajött a válasz
status= Int32::Parse(s,System::Globalization::NumberStyles::AllowHexSpecifier);
// konvertáljuk a választ egésszé.

IV.1.13. A CLI array template-tel megvalósitott tömb referencia osztálya

A tömb a programozásban gyakran előforduló adatszerkezet, algoritmuselemekkel. A .NET fejlesztői készítettek egy általános tömbdefiníciós osztálysablont(template), amiből a felhasználó, mint „gyártószerszámból” definiálhat referencia osztályt a neki szükséges adattípusból, a C++-ban bevezetett template jelölést használva (<>). Többdimenziós tömbökhöz is használható. A tömb elemeinek elérése a hagyományos szögletes zárójelek közé tett egész index-szel, a [ ] operátorral történik.

Deklaráció: cli::array<típus,dimenzió=1>^ tömbnév; a dimenzió elhagyható, ekkor értéke 1. A ^ a ref class jele, a cli:: elhagyható, ha a forrásfájlunk elején használjuk a using namespace cli; utasitást.

A tömbnek – miután referencia osztály, deklarációnál csak a handle készül el, de még nem hivatkozik sehova - helyet kell foglalni használat előtt a gcnew operátorral. A deklarációs utasításban is megtehetjük a helyfoglalást: a tömb elemeit felsorolva is megadhatjuk a C++-ban szokásos módon, {} jelek között.

Tulajdonsága: Length az egydimenziós tömb elemeinek száma, a függvénynek átadott tömbnél nem kell méret, mint az alap C-ben, a ciklusutasításban felhasználható, segítségével nem címzünk ki a tömbből :

for (i=0;i<tomb->Length;i++)….

Az alapvető tömbalgoritmusokra készítettek statikus metódusokat, ezeket a System::Array osztály tárolja:

Clear(tömb,honnan,mennyit) törlés. A tömbelemek értéke 0, false, null, nullptr lesz (a tömböt alkotó alaptípustól függően), vagyis töröljük a tömb elemeit.

Resize(tömb, új méret) átméretezés, bővülés esetén a régi elemek után a Clear()-nál használt értékekkel tölti fel a tömböt

Sort(tömb) a tömb elemeinek sorba rendezése. Alaphelyzetben numerikus adatok növekvő sorrendbe állításához használható fel. Megadhatunk kulcsokat és összehasonlító függvényt is, hogy tetszőleges típusú adatokat is sorba tudjunk rendezni.

CopyTo(céltömb,kezdőindex) elemek másolása. Vigyázat: az = operátor csak a referenciát duplikálja, ha a tömb adata megváltozik, a másik hivatkozás felől elérve is megváltozik a belső adat. Hasonlóan az == is azt jelzi, hogy a két referencia ugyanaz, és nem az elemeket hasonlítja össze.

Ha a típus, amiből a tömböt készítjük, egy másik referencia osztály (pl. String^), akkor azt kell megadnunk a definícióban. A tömb létrehozása után az elemeket is létre kell hozni egyesével, mert alaphelyzetben nullptr-eket tartalmaz. Példa: String^ mint tömbelem, kezdeti értékadással. Ha nem írjuk ki a nulla hosszúságú sztringeket, nullptr-ek lettek volna a tömbelemek

array<String^>^ sn= gcnew array<String^>(4){"","","",""};

A következő példában lottószámokat készítünk egy tömbbe, majd ellenőrizzük, hogy felhasználhatóak-e egy játékban: vagyis nincs köztük két azonos. Ehhez sorba rendezzük az elemeket, hogy csak a szomszédos elemeket kelljen vizsgálni, és emelkedő számsorrendben lehessen kiíratni az eredményt:

array<int>^ szamok; // managed tömb típus, referencia
Random ^r=gcnew Random();// véletlenszám generátor példány
int db=5,max=90,i; // beállítjuk, hány szám kell, és mennyiből a maximális szám. Inputként is megadható lenne, konverzió után.
szamok=gcnew array<int>(db); // managed heap-en lévő tömb legyártva.
for(i=0;i<szamok->Length;i++)
    szamok[i]=r->Next(max)+1;// nyers véletlenszámok a tömbben vannak
Array::Sort(szamok); // a beépitett metódussal sorba rakjuk a számokat.
// ellenőrzés: két egyforma egymás mellett ?
bool joszam=true;
for (i=0;i<szamok->Length-1;i++)
    if (szamok[i]==szamok[i+1]) joszam=false;

IV.1.14. C++/CLI: Gyakorlati megvalósitás pl. a Visual Studio 2008-as változatban

Ha CLR-es (vagyis .NET-es, ablakos) programot szeretnénk készíteni Visual Studio-val, az új elem varázslónál válasszuk a CLR kategória valamelyik „Application” elemét. A CLR console ugyanúgy fog kinézni, mint a „Win32 console app”, vagyis szöveges felülete lesz. Tehát ne a Console-t válasszuk, hanem a „Windows Forms Application”-t. Ekkor létrejön a programunk ablaka, a Form1 nevű konténerobjektum, amelynek kódja a Form1.h fejállományban lesz. Itt helyezi el a Form Designer a megrajzolt vezérlők kódját (és elé ír egy megjegyzést, hogy ne módosítsuk, de természetesen néha szükséges a módosítás). A mellékelt ábrán látható a kiválasztandó elem:

Projekttípus
IV.3. ábra - Projekttípus


A választás után létrejön a projektünk könyvtárszerkezete, benne a szükséges fájlokkal, - ekkor már kezdhetünk vezérlőket elhelyezni a formon. A „Solution Explorer” ablakban megkereshetjük a forrásfájlokat, és mindegyiket módosíthatjuk is. Az alábbi ábrán egy éppen elkezdett projekt látható:

A projekt
IV.4. ábra - A projekt


A Form1.h-ban található a programunk (form ikonja van, szokásos az stdafx.h-ba is kódot elhelyezni). A főprogramban (mea_1.cpp) számunkra nincs semmi változtatható. A „View” menü „Designer” menüpontjából a grafikus editort, „Code” menüpontjából a forrásprogramot választhatjuk ki. A „View/Designer” menüpont után így néz ki az ablakunk:

Az ablakmodell
IV.5. ábra - Az ablakmodell


A „View/Code” után pedig így:

A kódablak
IV.6. ábra - A kódablak


A „View/Designer” menüpontra állva szükségünk lesz még a Toolbox-ra, itt vannak a felrakható vezérlők (Az eszköztár csak designer állapotban tartalmaz felrakható elemeket). Ha esetleg nem lenne látható, a „View/Toolbox” menüponttal megjeleníthetjük. A toolbox esetérzékeny segítséget is tartalmaz: a vezérlők felett hagyva az egérmutatót, rövid összefoglalót kapunk a vezérlő működéséről, lásd az alábbi ábrát, ahol a label vezérlőt választottuk ki:

A szerszámos láda
IV.7. ábra - A szerszámos láda


A vezérlő kiválasztása a szokásos bal egérgombbal lehetséges. Ezután a kiválasztott vezérlő befoglaló téglalapja a formra rajzolható, amennyiben látható vezérlőt választottunk. A nem látható vezérlők (pl. timer) külön sávban, a form alatt kapnak helyet. Megrajzolás után a formunkra kerül egy példány a vezérlőből, automatikus elnevezéssel. Az ábrán a „Label” vezérlőt (nagybetűs: típus) választottuk ki, amennyiben ebből a vezérlőből az elsőt rajzoljuk, „label1”-re (kisbetűs: példány) fogja a fejlesztő környezet elnevezni. A vezérlők megrajzolása után, amennyiben szükséges, beállíthatjuk a tulajdonságaikat, és az eseményeikhez tartozó függvényeket. A vezérlőt kijelölve, a jobb gomb benyomása után, a „Properties” menüpont által előhozott ablakban tehetjük ezt meg. Fontos megjegyezni, hogy az itteni beállítások csak az aktuális vezérlőre vonatkoznak, és az egyes vezérlők tulajdonság ablakai egymástól eltérőek. Az ábrán a label1 vezérlő ablakát kapcsoljuk be:

A vezérlő menüje
IV.8. ábra - A vezérlő menüje


Ezután egy külön ablakban tudjuk megadni a tulajdonságok értékét:

A vezérlő tulajdonságai
IV.9. ábra - A vezérlő tulajdonságai


Ugyanez az ablak szolgál az eseménykezelők kiválasztására is. Az eseménykezelők megadásához a villám ikonra () kell kattintani. Ekkor az adott vezérlő összes eseményre adott válaszlehetősége megjelenik. Ahol a lista jobb oldala üres, arra az eseményre a vezérlő nem fog reagálni.

A vezérlő eseményei
IV.10. ábra - A vezérlő eseményei


A példában a label1 vezérlő nem reagál, ha rákattintanak (a label1 vezérlő le tudja kezelni a Click eseményt, de nem szokás a vezérlőt ilyen módon használni). Kétféle módon kerülhet a listába függvény: ha meglévő függvényt szeretnénk a vezérlő eseményekor lefuttatni (és a paraméterei megegyeznek az esemény paramétereivel), akkor a legördülő listából kiválaszthatjuk, ha még nincs ilyen, az üres helyre történő dupla kattintással egy úgy függvény fejléce készül el, és a kódszerkesztőbe kerülünk. Minden vezérlőnek van egy alapértelmezett eseménye (például a button-nak a click), a tervező ablakban a vezérlőre kettőt kattintva, az esemény kódszerkesztőjébe kerülünk, ha még nincs ilyen függvény, a fejléce is létrejön és a hozzárendelés is. Vigyázat, a vezérlő a hozzárendelés nélkül nem működik! Tipikus hiba például, megírni a button1_Click függvényt, a paramétereket is helyesen megadva, összerendelés nélkül. Ekkor – hibátlan fordítás után – a gomb nem reagál a kattintásra. Csak akkor fog reagálni, ha az events ablakban a click sorban szerepel a button1_Click függvény neve.

A vezérlő Click eseménye
IV.11. ábra - A vezérlő Click eseménye


IV.1.15. Az Intellisense beépitett segítség

Mint az előző ábrán láttuk, egy vezérlőnek rengeteg tulajdonsága lehet. Ezeket a program szövegében használhatjuk, de mivel azonosítónak minősülnek, karakterről-karakterre egyezniük kell a definícióban szereplő tulajdonsággal, a kis- és nagybetűket figyelembe véve. A tulajdonságok neve gyakran hosszú (például UseCompatibleTextRendering). A programozónak ezeket kellene begépelnie hibátlanul. A szövegszerkesztő tartalmaz egy segítséget: az objektum neve (button1) után az adattagra utaló jelet (->) téve listát készít a lehetséges tulajdonságokból, egy kis menüben megjeleníti őket, a nyilakkal vagy egérrel kiválaszthatjuk a nekünk szükségeset, majd a tab billentyűvel beírja a programunk szövegébe. Akkor is előjön a segítő lista, ha elkezdjük gépelni a tulajdonság nevét. A Visual Studio ezeket a vezérlő-tulajdonságokat egy nagyméretű .NCB kiterjesztésű fájlban tárolja, és amennyiben letöröljük (pl. pen drive-on viszünk át egy forrásprogramot másik gépre), megnyitáskor újragenerálja. Az intellisense néhány esetben nem működik: ha a programunk szintaktikailag hibás, a kapcsos zárójelek száma nem egyezik, akkor megáll. Hasonlóan nem működik a Visual Studio 2010-es változatában sem, ha CLR kódot írunk. Az ábrán a label7->Text tulajdonságot szeretnénk megváltoztatni, a rengeteg tulajdonság miatt beírjuk a T betűt, majd az egérrel kiválasztjuk a Text-et.

A beépített segítség
IV.12. ábra - A beépített segítség


IV.1.16. A CLR-es program típusának beállitása.

Mint ahogy azt az előzőekben már említettük, a C++/CLR képes a kevert módú (natív+menedzselt) programok készítésére. Amennyiben az előző pontban leírt beállításokat használjuk, a programunk tisztán menedzselt kódú lesz, a natív kód nem fordítható le. Kezdetben azonban mégis célszerű ezekkel a beállításokkal elindulni, mert így a programunk ablaka és a vezérlőink használhatók lesznek. A projekt tulajdonságaiban (kijelöljük a projektet, majd jobb gomb és „Properties”) tudjuk ezeket a beállításokat megtenni. Vigyázat, nem a legfelső szinten álló solution, hanem az alatta lévő projekt tulajdonságairól van szó!

A projekt adatai
IV.13. ábra - A projekt adatai


A "Property Pages" ablakban adható meg a natív/kevert/tisztán menedzselt kód beállítás, a "Common Language Runtime Support" sorban. Ötféle beállítás közül választhatunk:

A program típusa
IV.14. ábra - A program típusa


Az egyes lehetőségek a következők:

  • "No common Language Runtime Support" – nincs menedzselt kód. Egyenértékű, ha Win32 konzolalkalmazást, vagy natív Win32-es projektet készítünk. Ebben a beállításban nem is képes a .NET-es rendszer elemeinek (handle-k, garbage collector, referencia osztályok, assembly-k) fordítására.

  • "Common Language Runtime Support" – van natív és menedzselt kódú fordítás is. Ezzel a beállítással kell a kevert módú programokat elkészíteni, vagyis amennyiben a programot az ablakos alapbeállításokkal kezdtük el fejleszteni, és szeretnénk natív kódú függvényeket használni, ezt a menüpontot kell kiválasztanunk.

  • "Pure MSIL Common Language Runtime Support" – tisztán menedzselt kódú fordítás. A "Windows Form Application" template-ből készített programok alapértelmezése. (A C# fordítónak csak ez az egy beállítása lehetséges.) Megjegyzés: ez a típusú kód C++-ban tartalmazhat natív kódú adatokat is, amelyeket menedzselt kódú programokkal érünk el.

  • "Safe MSIL Common Language Runtime Support" – mint az előző, csak natív kódú adatokat sem tartalmazhat, és engedélyezi a CRL kód biztonsági ellenőrzését, egy erre a célra készített eszközzel (peverify.exe).

  • "Common Language Runtime Support, Old Syntax" - ez is kevert kódú programot készít, de a Visual Studio 2002-es szintaktikával. (_gc new a gcnew helyett). A régebben készült programokkal való kompatibilitás miatt maradt meg ez a beállítás. Ne használjuk!

IV.2. Az ablakmodell és az alapvezérlők.

IV.2.1. A Form alapvezérlő

A Form nem rakható fel a toolbox-ból, az új projektnél létrejön. Alaphelyzetben üres, téglalap alakú ablakot hoz létre. A properties-ben találhatók a beállitásai, pl. Text a címsora, amibe alaphelyzetben a vezérlő neve (ha ez az első form, Form1) kerül bele. Ehhez hozzáférünk itt is és a programból is (this->Text=…), mert a Form1.h-ban használható a formunk referencia osztálya, ami a Form osztályból örököltetett példány, amire a programukon belül a this referenciával hivatkozhatunk. Lehet állítani a tulajdonságokat, és a beállításokból készült programrészletet felhasználni a programunk másik részén. Ugyanis az beállított tulajdonságokból (is) egy programrészlet lesz, a form1.h elején, egy külön szakaszban:

#pragma region Windows Form Designer generated code
    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    // button1
    this->button1->Location = System::Drawing::Point(16, 214);
    this->button1->Name = L"button1";
    this->button1->Size = System::Drawing::Size(75, 23);
    this->button1->TabIndex = 0;
    this->button1->Text = L"button1";
    this->button1->UseVisualStyleBackColor = true;
    this->button1->Click += gcnew System::EventHandler(this, &Form1::button1_Click);

IV.2.2. A Form vezérlő gyakran használt tulajdonságai

  • Text – a form felirata. Ez a tulajdonság minden olyan vezérlőnél megtalálható, amely szöveget (is) tartalmaz.

  • Size – a form mérete, alaphelyzetben pixelben. Tartalmazza a Width (szélesség) és a Height (magasság) közvetlenül elérhető tulajdonságokat is. A látható vezérlőknek is vannak ilyen tulajdonságaik.

  • BackColor – háttér színe. Alaphelyzetben ugyanolyan színű, mint a rendszerben definiált vezérlők háttere (System::Drawing::SystemColors::Control). Ennek a beállításnak akkor lesz jelentősége, amikor a formon lévő grafikát le szeretnénk törölni, mert a törlés egy színnel való kitöltést jelent.

  • ControlBox – az ablak rendszermenüje (minimalizáló, maximalizáló gomb és a bal oldalon található windows menü) engedélyezhető (alapértelmezés) és letiltható

  • FormBorderStyle – itt adhatjuk meg, hogy az ablakunk átméretezhető vagy fix méretű legyen, esetleg még keretet sem tartalmazzon.

  • Locked – az ablak átméretezését és elmozgatását tilthatjuk le a segítségével.

  • AutoSize – az ablak képes változtatni a méretét, a benne lévő tartalomhoz igazodva.

  • StartPosition – a program indításkor hol jelenjen meg a windows asztalon. Alkalmazása: ha többmonitoros rendszert használunk, megadhatóak a második monitor x,y koordinátái, a programunk ott fog elindulni. Ezt a beállítást célszerű programból, feltételes utasítással megtenni, mert amennyiben csak egy monitoros rendszeren indítják el, nem fog látszani a program ablaka.

  • WindowState – itt adhatjuk meg, hogy a programunk ablaka legyen normál (Normal), a teljes képernyőn fusson (Maximized), vagy a háttérben fusson (Minimized). Természetesen, mint a tulajdonságok többsége, futási időben is elérhető, vagyis amennyiben a kis ablakban elindított program szeretné magát maximalizálni (mert például sok mindent meg szeretne mutatni), arra is van lehetőség: this->WindowState=FormWindowState::Maximized;

IV.2.3. A Form vezérlő eseményei

Load – a programunk elindulásakor, még a megjelenés előtt lefutó programrészlet. A Form vezérlő alapértelmezett eseménye, vagyis a címsorára kettőt kattintva a „Designer”-ben ennek a kezelőfüggvényébe kerül az editor. A függvény szokásosan – mivel a program indulásakor egyszer lefut, inicializáló szerepet tölthet be programunkban. Értékeket adhatunk a változóknak, legyárthatjuk a szükséges dinamikus változókat, és a többi vezérlőnkön (ha eddig nem tettük meg) feliratot cserélhetünk, a form méretét dinamikusan beállíthatjuk stb.

Példaként nézzük meg a másodfokú egyenlet programjának Load eseménykezelőjét!

private: System::Void Form1_Load(System::Object^  sender,  
        System::EventArgs^  e) {
    this->Text = "masodfoku"; 
    textBox1->Text = "1";  textBox2->Text = "-2"; textBox3->Text = "1";
    label1->Text = "x^2+"; label2->Text = "x+";   label3->Text = "=0";
    label4->Text = "x1=";  label5->Text = "x2=";  label6->Text = "";
    label7->Text = "";     label8->Text = "";
    button1->Text = "Oldd meg";
    if (this->ClientRectangle.Width < label3->Left + label3->Width) 
        this->Width = label3->Left + label3->Width + 24;
        // nem elég széles a form
    if (this->ClientRectangle.Height < label8->Top + label8->Height)
        this->Height = label8->Top + label8->Height + 48; 
        // nem elég magas
    button1->Left = this->ClientRectangle.Width - button1->Width-10; 
    // pixel
    button1->Top = this->ClientRectangle.Height - button1->Height - 10;
}

A fenti példában a kezdeti értékek beállítása után a feliratokat állítottuk be, majd a form méretét akkorára vettük, hogy a teljes egyenlet és az eredmények (label3 volt a jobb oldali szélső vezérlő és label8 a form alján lévő) is láthatóak legyenek.

Amennyiben ebben a függvényben azt állapítjuk meg, hogy a program futása értelmetlen (feldolgoznánk egy fájlt, de nem találjuk, kommunikálnánk egy hardverrel, de nem elérhető, Interneteznénk, de nincs kapcsolat stb.) egy hibaüzenet ablak megjelenítése után a programból ki is léphetünk. Itt látható egy hardveres példa:

if (!vezerlo_van) {
  MessageBox::Show("No iCorset controller.",
  "Error",MessageBoxButtons::OK);
  Application::Exit();
} else {
  // van vezérlő, vezérlő inicializálása.
}

Egy dologra azonban ügyeljünk: az Application::Exit() nem azonnal lép ki a programból, csak a Window üzenetsorba helyez be egy nekünk szóló kilépésre felszólító üzenetet. Vagyis az if… utáni programrészlet is le fog futni, sőt a programunk ablaka is megjelenik egy pillatatra a kilépés előtt. Ha a további részlet lefutását el szeretnénk kerülni (kommunikálnánk a nem létező hardverrel), akkor azt a fenti if utasítás else ágában tegyük meg, és az else ág a Load függvény végéig tartson. Így biztosíthatjuk, hogy az ilyenkor már valószínűleg hibát okozó programrészlet ne fusson le.

Resize – A formunk átméretezésekor (a minimalizálás, maximalizálás, normál méretre állítás is ennek számít) lefutó eseménykezelő. A betöltéskor is lefut, így a fenti példában lévő form méretnövelést ide is tehettük volna, ekkor a formot nem is lehetne kisebbre méretezni, a vezérlőink láthatósága miatt. Amennyiben ablakméretfüggő grafikát készítettünk, itt át lehet méretezni.

Paint – a formot újra kell festeni. Részletesen lásd a "GDI+ használata" című fejezetben, IV.4.1. szakasz.

MouseClick, MouseDoubleClick – a Formra egyet vagy kettőt kattintottunk az egérrel. Amennyiben vannak egyéb vezérlők is a formon, ez az esemény akkor fut le, ha egyik egyéb vezérlőre sem kattintottunk, csak az üres területre. Az eseménykezelő egyik paraméterében megkapjuk a

System::Windows::Forms::MouseEventArgs^ e

referencia osztályra mutató kezelőt, amely többek közt a kattintás koordinátáit (X,Y) is tartalmazza.

MouseDown, MouseUp – a Formon valamelyik egérgombot lenyomtuk vagy felengedtük. A paraméter tartalmazza a Button paraméterben is, hogy melyik gombot nyomtuk le, illetve engedtük fel. A következő programrészlet a kattintások koordinátáit egy fájlba menti, ezzel pl. egy egyszerű rajzolóprogramot készíthetünk:

// ha mentés bekapcsolva és bal gombot nyomtunk
    if (toolStripMenuItem2->Checked &&  (e->Button ==     System::Windows::Forms::MouseButtons::Left))  {
    // kiirjuk a fileba a két koordinátát, x-et és y-t, mint int32-t
                bw->Write(e->X); // int32-t irunk, 
                bw->Write(e->Y); // vagyis 2*4 byte/pont
    }

MouseMove: - az egér mozgatásakor lefutó függvény. Az egér gombjaitól függetlenül dolgozik, amennyiben az egerünk a form felett jár, a koordinátái kiolvashatók. A lenti programrészlet ezeket a koordinátákat az ablak fejlécében (vagyis a Text tulajdonságában) jeleníti meg, természetesen a szükséges konverziók után:

private: System::Void Form1_MouseMove(System::Object^  sender, System::Windows::Forms::MouseEventArgs^  e) {
             // koordináták a fejlécbe, azt soha senki nem nézi meg
             this->Text = "x:" + Convert::ToString(e->X) + " y=" + Convert::ToString(e->Y);
         }

FormClosing – a programunk valamilyen okból kapott egy Terminate() Windows üzenetet. Az üzenet forrása bármi lehetett: saját maga az Application::Exit()-tel, a felhasználó az „ablak bezárása" kattintással vagy az alt-F4 billentyű kombinációval, az operációs rendszer leállítás előtt stb. A Form ezen függvény lefutása után bezáródik, ablaka eltűnik, az általa lefoglalt erőforrások felszabadulnak. Amennyiben a programunk úgy dönt, hogy még ez nem tehető meg, az eseménykezelő paraméterének Cancel nevű tagjának true értékre állításával a program leállítása megszüntethető, vagyis tovább fog futni. Az operációs rendszer természetesen, amennyiben a leállítását akarjuk ezzel megakadályozni, adott idő múlva becsukja a programunkat. A következő példában a program egy dialógusablakban történő kérdés után hagyja csak kiléptetni magát:

Void Form1_FormClosing(System::Object^  sender, System::Windows::Forms::FormClosingEventArgs^  e) {
   System::Windows::Forms::DialogResult d;
   d=MessageBox::Show("Biztos, hogy használni akarja a légzsákot ?”,
  "Fontos biztonsági  figyelmeztetés", MessageBoxButtons::YesNo);
  if (d == System::Windows::Forms::DialogResult::No) e->Cancel=true;
}

FormClosed – a programunk már a kilépés utolsó lépésében jár, az ablak már nem létezik. Itt már nincs visszaút, ez az utolsó esemény.

IV.2.4. A vezérlők állapotának aktualizálása

Az eseménykezelők (Load, Click) lefutása után a rendszer aktualizálja a vezérlők állapotát, hogy a következő eseményre már az új állapotban legyenek. Azonban tekintsük a következő mintaprogramrészletet, amely a form címsorába növekvő számokat ír, elhelyezhető pl. a button_click eseménykezelőben:

int i;
for (i=0;i<100;i++)
{
     this->Text=Convert::ToString(i);
}

A programrészlet lefutása alatt (leszámítva a sebességproblémát) semmit sem változik a form fejléce. A Text tulajdonságot átírtuk ugyan, de ettől a vezérlőn a régi felirat látszik. Csak akkor fog változni, ha véget ért az adott eseménykezelő függvény. Ekkor viszont a „99” fog a vezérlőre kerülni. Hosszabb műveletek esetén (képfeldolgozás, nagyméretű szövegfájlok) jó lenne valahogy jelezni a felhasználónak, hol tartunk, megjeleníteni az aktuális állapotot, ennek hiányában úgy is érezheti, hogy a programunk lefagyott. Ezt a célt szolgálja az Application::DoEvents() függvényhívás, amely a vezérlők aktuális állapotát aktualizálja: a példánkban lecseréli a form fejlécét. Ebből sajnos semmit sem látunk, csak akkor tudjuk elolvasni a számokat, ha egy várakozást (Sleep) is beépítünk a ciklusba:

int i;
for (i=0;i<100;i++)
{ 
 this->Text=Convert::ToString(i);
 Application::DoEvents();
 Threading::Thread::Sleep(500);
}

A Sleep() függvény argumentuma ezredmásodpercben a várakozás ideje. Ezzel szimuláltuk a lassú műveletet. Ha adott időnként ismétlődő algoritmuselemekre van szükségünk, a Timer vezérlőt használjuk (lásd IV.2.22. szakasz).

IV.2.5. Alapvezérlők: Label (címke) vezérlő

A legegyszerűbb vezérlő a szöveg megjelenítésére szolgáló Label vezérlő. A „Text” nevű, String ^ típusú tulajdonsága tartalmazza a megjelenítendő szöveget. Alaphelyzetben a szélessége a megjelenítendő szöveghez igazodik (AutoSize=true). Amennyiben szöveget jelenítünk meg segítségével, eseményeit (pl. Click) nem szokás használni. A Label-nek nincs alaphelyzetben kerete (BorderStyle=None), de bekeretezhető (FixedSingle). A háttérszín a BackColor, a szöveg színe pedig a ForeColor tulajdonságban található. Ha a megjelenített szöveget el szeretnénk tüntetni, arra két lehetőségünk is van: vagy a Visible nevű, logikai típusú tulajdonságot false-ra állítjuk, ekkor a Text tulajdonság nem változik, de a vezérlő nem látszik, vagy a Text tulajdonságok állítjuk be egy üres, 0 hosszúságú sztringre (””). Visible tulajdonsága minden látható vezérlőnek van, a tulajdonság false-ra állításával a vezérlő eltűnik, majd true-ra állításával újra láthatóvá válik. Ha biztosak vagyunk benne, hogy a legközelebbi megjelenítéskor más lesz a vezérlő felirata, célszerű az üres sztringes megoldást választani, mert ebben az esetben az értékadáskor az új érték egyből meg is jelenik, míg a Visible-s megoldáskor egy újabb, engedélyező programsorra is szükség van.

IV.2.6. Alapvezérlők: TextBox (szövegmező) vezérlő

A TextBox vezérlő szöveg (String^) bevitelére használható (amennyiben szám bevitelére van szükség, akkor is ezt használjuk, csak a feldolgozás egy konverzióval kezdődik). A „Text” tulajdonsága tartalmazza a szövegét, amelyet programból is átírhatunk, és a felhasználó is átírhat a program futása közben. A már említett Visible tulajdonság itt is megjelenik, emellett egy „Enabled” tulajdonság is rendelkezésre áll. Az Enabled-et false-re állítva a vezérlő látszik a form-on, csak szürke színű, és a felhasználó által nem használható: megváltoztatni sem tudja és rákattintani sem tud. Ilyen állapotú telepítés közben a „Tovább” feliratú parancsgomb, amíg a felhasználói szerződést nem fogadtuk el. A TextBox-nak alapértelmezett eseménye is van: TextChanged, amely minden változtatás után (karakterenként) lefut. Többjegyű számoknál, egy karakternél hosszabb adatoknál, több input adat esetén (több szövegdobozban) nem szoktuk megírni, mert értelmetlen lenne. Például, a felhasználónak be kell írnia a nevét a TextBox-ba, a program pedig elrakja egy fájlba. Minden egyes karakternél felesleges lenne kiírni a fájlba az eddigi tartalmat, mert nem tudjuk, melyik lesz az utolsó karakter. Ehelyett megvárjuk, amíg véget ér a szerkesztés, kész az adatbevitel (esetleg több szövegmező is van a formunkon), és egy célszerűen elnevezett (kész, mentés, feldolgozás) parancsgomb megnyomásával jelezheti a felhasználó, hogy a szövegmezőkben benne vannak a program input adatai. Ellentétben a többi Text-et tartalmazó vezérlővel, amelynél a „Designer” beleírja a Text tulajdonságba a vezérlő nevét, itt ez nem történik meg, a Text tulajdonság üresen marad. A TextBox többsorosra is állítható a MultiLine tulajdonság true-ra kapcsolásával. Ekkor a Text-ben soremelések szerepelnek, és a Lines tulajdonság tartalmazza a sorokat, mint egy sztringekből készített tömb elemeit. Néhány programozó kimenetre is a szövegmező vezérlőt használja, a ReadOnly tulajdonság true-ra állításával. Amennyiben nem szeretnénk visszaírni a bevitt karaktereket, jelszó beviteli módba is kapcsolhatjuk a szövegmezőt a UseSystemPasswordChar tulajdonság true-ra állításával. A másodfokú egyenlet indításakor lefutó függvényt már láttuk, most nézzük meg a „számolós” részt. A felhasználó beírta az együtthatókat (a,b,c) a megfelelő szövegmezőkbe, és benyomta a „megold” gombot. Első dolgunk, hogy a szövegmezőkből kivesszük az adatokat, konverziót alkalmazva. Majd jöhet a számítás, és az eredmények kiíratása.

double a, b, c, d, x1, x2, e1, e2; // lokális változók
// valós megoldás lett ? legyünk optimisták 
bool vmo = true;
// ha elfelejtenék értéket adni bármelyik változónak -> error
a = Convert::ToDouble(textBox1->Text); // String -> double
b = Convert::ToDouble(textBox2->Text);
c = Convert::ToDouble(textBox3->Text);
d = Math::Pow(b,2) - 4 * a * c; // hatványozás művelet is létezik
if (d >= 0) // valós gyökök
{
  x1=(-b+Math::Sqrt(d))/(2*a);
  x2=(-b-Math::Sqrt(d))/(2*a);
  label4->Text = "x1=" + Convert::ToString(x1);
  label5->Text = "x2=" + Convert::ToString(x2);
  // ellenőrzés
  e1 = a * x1 * x1 + b * x1 + c;// igy kevesebbet gépeltünk, mint a Pow-ban
  e2 = a * x2 * x2 + b * x2 + c;
  label6->Text = "...=" + Convert::ToString(e1);
  label7->Text = "...=" + Convert::ToString(e2);
}

IV.2.7. Alapvezérlők: a Button (nyomógomb) vezérlő

A Button vezérlő egy parancsgombot jelöl, amely rákattintáskor „besüllyed”. Ha az aktuálisan kiválasztható funckiók száma alacsony, akkor alkalmazzuk a parancsgombo(ka)t. A funkció lehet bonyolult is, ekkor hosszú függvény tartozik hozzá. A látható vezérlők szokásos tulajdonságait tartalmazza: Text a felirata, Click az esemény, amely a gombra való kattintáskor lefut - ez az alapértelmezett, szokásosan használt eseménye. Az eseménykezelő paraméteréből nem derül ki a kattintás koordinátája. Az eseménykezelő váza:

private: System::Void button1_Click(System::Object^  sender, System::EventArgs^  e) {
}

A Button vezérlő lehetőséget ad arra is, hogy a manapság divatos parancsikonos üzemeltetést alkalmazzuk, feliratok helyett kis ábrákat felhasználva. Ehhez a következő lépésekre van szükség: betöltjük a kis ábrát egy Bitmap típusú változóba, beállítjuk a Button méreteit a bitmap méreteire, végül a gomb Image tulajdonságába betesszük a Bitmap típusú hivatkozást, mint ahogy az alábbi példában történt: a szerviz.png nevű kép a button2 parancsgombon jelenik meg.

Bitmap^ bm;
bm=gcnew Bitmap("szerviz.png");
button2->Width=bm->Width;
button2->Height=bm->Height;
button2->Image=bm;

IV.2.8. Logikai értékekhez használható vezérlők: a CheckBox (jelölő négyzet)

A CheckBox vezérlő egy kijelölő négyzet. Text tulajdonsága a melléírt szöveg (String^ típusú), logikai érték típusú tulajdonsága a Checked, amely true, ha a pipa bekapcsolt állapotban van. A CheckedState tulajdonság háromféle értéket vehet fel: a ki/bekapcsolton kívül létezik egy harmadik, közepes értéke is, amit futás közben csak programból állíthatunk be, viszont jelöltnek minősül. Amennyiben egy Formon több is van a CheckBoxból, ezek függetlenek egymástól: bármelyiket beállíthatjuk bármely logikai állapotúra. Eseménye: a CheckedChanged lefut, amikor a felhasználó a logikai értéket megváltoztatja. Az alábbi mintaprogram egy intervallumfelező program részlete: ha bekapcsoltuk a "Lépésenként" feliratú jelölő négyzetet, egy lépést hajt végre az algoritmusból, ha nincs bekapcsolva, egy ciklussal az eredményt szolgáltatja. Amennyiben lépésenként kezdtük el futtatni a programot, már nem lehet kikapcsolni a pipát: lépésenként kell a további számolást végrehajtani.

switch (checkBox1->Checked)
{
  case true:
    checkBox1->Enabled = false;
    lepes();
    break;
  case false:
    while (Math::Abs(f(xko)) > eps) lepes();
    break;
}
kiir();

IV.2.9. Logikai értékekhez használható vezérlők: a RadioButton (opciós gomb)

A RadioButton kör alakú kijelölő. A CheckBox-hoz hasonló, de egy konténerobjektumon (ilyen például a Form is: más objektumokat teszünk bele) belül csak egy lehet aktív közülük. Nevét a régi, hullámváltót tartalmazó rádióról kapta: amikor az egyik gombot benyomtuk, a többi kiugrott. Amikor az egyikre odakerül a kijelölő kör (programból Checked = true, vagy a felhasználó kattintása által), az előzőről (és az összes többiről) lekerül (a Checked tulajdonsága false-ra változik). Text tulajdonságában a kör melletti szöveget tároljuk. Felvetődhet a kérdés, ha egyszerre két opciós listából is kell választani RadioButton-okkal, akkor hogyan oldjuk meg, hiszen csak egy lehet aktív ? A válasz egyszerű: konténerobjektumonként lehet egy aktív RadioButton, így el kell helyezni a formon néhány konténerobjektumot.

IV.2.10. Konténerobjektum vezérlő: a GroupBox (csoport mező)

A GroupBox egy téglalap alakú keret, a bal felső vonalon szöveggel (Text, String ^ típusú). Vezérlőket helyezhetünk el benne, amiket egy keretbe foglal. Ez egyrészt esztétikus, mert a logikailag összefüggő vezérlők keretben jelennek meg, másrészt hasznos a RadioButton típusú vezérlőknél, valamint az itt lévő vezérlők egy utasítással máshova mozgathatók, és eltüntethetők, a GroupBox megfelelő tulajdonságainak állításával. A következő példában osztályozás történik: kevés számú diszkrét elem bevitele és feldolgozása következik:

groupBox1->Text="Progterv-I"; 
radioButton1->Text="jeles";
radioButton2->Text="jó";
radioButton3->Text="közepes";// más jegyet nem is lehet itt szerezni
…
int jegy;
if (radioButton1->Checked) jegy=5;
if (radioButton2->Checked) jegy=4;

A program ablakának részlete
IV.15. ábra - A program ablakának részlete


IV.2.11. Diszkrét értékeket bevivő vezérlők: a HscrollBar (vízszintes csúszka) és a VscrollBar (függőleges csúszka)

A két vezérlő, amit a magyar fordításokban csúszkának neveznek, csak irányában különbözik egymástól. Feliratuk nincs, amennyiben a véghelyzeteket vagy az aktuális állapotukat jelezni szeretnénk, külön címkék segítségével oldhatjuk meg. Az aktuális érték, ahol a csúszka áll, a Value tulajdonságban található egész numerikus érték, ez a Minimum és a Maximum tulajdonságok közé esik. A Minimum tulajdonságra beállítható a csúszka, ez a bal/felső oldali véghelyzete, a jobb/alsó oldali véghelyzetre viszont egy képlet van, amelyben szerepel a csúszka gyors változtatási egysége, a LargeChange tulajdonság (ennyit akkor változik az érték, amikor egérrel az üres részre kattintunk): Value_max=1+Maximum-LargeChange. LargeChange és SmallChange általunk beállított tulajdonságértékek. A csúszka mozgatásakor a Change esemény fut le, már az aktualizált Value értékkel. A példában 1 bájtos értékeket (0..255) szeretnénk előállitani 3, egyforma méretű vizszintes csúszkával. A csúszkákat beállító programrészlet a Form_Load eseménykezelőben:

int mx;
  mx=254 + hScrollBar1->LargeChange; // 255-öt szeretnénk a jobb oldali állásban
  hScrollBar1->Maximum = mx; // 1 bájt max
  hScrollBar2->Maximum = mx; 
  hScrollBar3->Maximum = mx; 

A csúszkák megváltozásakor az értéket kiolvassuk, és egy színt állítunk elő belőlük, és a csúszkák melletti címkékre ellenőrzésképpen kiírjuk az értéket:

System::Drawing::Color c; // szín típusú változó
r = hScrollBar1->Value ; // 0..255-ig
g = hScrollBar2->Value;
b = hScrollBar3->Value;
c = Color::FromArgb(Convert::ToByte(r),Convert::ToByte(g),Convert::ToByte(b));
label1->Text = "R=" + Convert::ToString(r); // ki is irathatjuk
label2->Text = "G=" + Convert::ToString(g);
label3->Text = "B=" + Convert::ToString(b);

IV.2.12. Egész szám beviteli vezérlője: NumericUpDown

A NumericUpDown vezérlővel egy egész számot vihetünk be, ami bekerül a Value tulajdonságba, a Minimum és Maximum értékek közt. A felhasználó egyesével lépkedhet az értékek közt a fel/le nyilakra kattintással. A Minimum és Maximum érték között minden egész szám megjelenik a választható értékek közt. Esemény: ValueChanged lefut, minden változtatás után.

IV.2.13. Több objektumból választásra képes vezérlők: ListBox és a ComboBox

 

A ListBox vezérlő felkínál egy tetszőlegesen feltölthető listát, amelyből a felhasználó választhat. A ComboBox a listán felül egy TextBox-ot is tartalmaz, amely felveheti a kijelölt elemet, illetve szabadon be is gépelhet a felhasználó egy szöveget, ez a Text tulajdonsága. A lista tulajdonságot Items-nek hívjuk, Add() metódussal bővíthető, és indexelve olvasható. Az aktuális elemre SelectedIndex mutat. Ha megváltozik a kijelölés, lefut a SelectedIndexChanged esemény. A ComboBox vezérlőt több, még bonyolultabb funkciót megvalósító vezérlő is használja: például az OpenFileDialog. A példában a ComboBox listáját feltöltjük elemekkel, amit a label4-ben jelenítünk meg, amennyiben a kijelölt elem megváltozik.

comboBox1->Items->Add("Jeles");
comboBox1->Items->Add("Jó");
comboBox1->Items->Add("Közepes");
comboBox1->Items->Add("Elégséges");
comboBox1->Items->Add("Elégtelen");
private: System::Void comboBox1_SelectedIndexChanged(System::Object^  sender, System::EventArgs^  e) {
  if (comboBox1->SelectedIndex>=0) 
    label4->Text=comboBox1->Items[comboBox1->SelectedIndex]->ToString();
}

IV.2.14. Feldolgozás állapotát mutató vezérlő: ProgressBar

A ProgressBar vezérlővel lehet jelezni egy feldolgozás állapotát, még mennyi van hátra, nem fagyott le a program, dolgozik. Előre tudnunk kell, hogy mikor leszünk készen, mikor kell elérnie a maximumot. A HscrollBar-hoz hasonlóak a vezérlő tulajdonságai, azonban egérrel nem módosítható az érték. Egyes Windows verziók animálják a vezérlőt akkor is, ha konstans értéket mutat. A vezérlőt célszerűen a StatusStrip-ben (állapotsorban) az ablakunk bal alsó sarkában szokásos elhelyezni. Rákattintáskor ugyan lefut a Click eseménye, de ezt nem szokásos lekezelni.

IV.2.15. Pixelgrafikus képeket megjeleníteni képes vezérlő: a PictureBox (képmező)

A vezérlő képes egy képet megjeleníteni. Image tulajdonsága tartalmazza a megjelenitendő Bitmap referenciáját. A Height, Width tulajdonságai a méreteit, a Left, Top tulajdonságai az ablak bal szélétől és tetejétől mért távolságát pixelben tárolják. Célszerű, ha a PictureBox mérete akkora, mint a megjelenítendő bitkép (amelyet szinte bármilyen képfájlból képes betölteni), hogy ne legyen átméretezés. A SizeMode tulajdonság tartalmazza, hogy mit tegyen, ha át kell méretezni a képet: Normal értéknél nincs átméretezés, a bal felső sarokba teszi a képet, amennyiben a Bitmap nagyobb, azt a részt nem jeleníti meg. A StretchImage értéknél a bitképet akkorára méretezi, amekkora a PictureBox. Az AutoSize beállításnál a PictureBox méreteit állítja be a bitkép méreteinek megfelelően. Az alábbi programban egy, az idokep.hu-n található hőmérsékleti térképet jelenítünk meg PictureBox vezérlőben, a SizeMode tulajdonságot a Designer-ben előre beállítva, a képek méretparamétereit label-ben kiíratva:

Bitmap ^ bm=gcnew Bitmap("mo.png");
label1->Text="PictureBox:"+Convert::ToString(pictureBox1->Width)+"x"+
     Convert::ToString(pictureBox1->Height)+" Bitmap:"+
     Convert::ToString(bm->Width)+"x"+Convert::ToString(bm->Height);
pictureBox1->Image=bm;

A fenti programrészlet futásának eredménye SizeMode=Normal-nál:

A program ablaka
IV.16. ábra - A program ablaka


SizeMode=StretchImage beállítással. Az arányok sem egyeznek: a térkép téglalap alakú, a PictureBox pedig négyzet:

A program ablaka
IV.17. ábra - A program ablaka


SizeMode=AutoSize a PictureBox megnőtt, de a Form nem. A formot kézzel kellett átméretezni ekkorára:

Az átméretezett ablak
IV.18. ábra - Az átméretezett ablak


SizeMode=CenterImage beállitás: átméretezés nincs, a Bitmap közepe került a PictureBox-ba:

A Bitmap középen
IV.19. ábra - A Bitmap középen


SizeMode=Zoom a bitkép arányait megtartva méretezi át úgy, hogy beférjen az PictureBox-ba:

A zoomolt Bitmap
IV.20. ábra - A zoomolt Bitmap


IV.2.16. Az ablakunk felső részén lévő menüsor: a MenuStrip (menüsor) vezérlő

Amennyiben a programunk annyi funkciót valósít meg, hogy elindításukhoz nagyszámú parancsgombra lenne szükség, célszerű a funkciókat hierarchikusan csoportosítani, és menübe rendezni. Képzeljük csak el, hogy pl. a Visual Studió összes funkcióját parancsgombokkal lehetne működtetni, el sem férne a szerkesztő ablak a sok gombtól. A menü a főmenüből indul, ez mindig látszik a program ablakának tetején, és minden menüelemnek lehet almenüje. A hagyományos menü feliratokat (Text) tartalmaz, de az újabb menüelemek bitképet is meg tudnak mutatni, vagy lehetnek szerkesztésre használható TextBox-ok, ComboBox-ok is. A (nem főmenüben lévő) menüelemek közé elválasztó vonal (separator) tehető, valamint a menüelemek elé kijelölő pipa is kerülhet. A menüelemek a parancsgombokhoz hasonlóan egyedi azonosítót kapnak. Az azonosító lehet általunk begépelt, vagy – a többi vezérlőhöz hasonlóan automatikusan létrehozott. A MenuStrip vezérlő a form alatt jelenik meg, de közben a programunk felső sorában már írhatjuk a menüelem nevét, vagy kiválaszthatjuk a „MenuItem” felirattal az automatikus elnevezést. Az ábrán a menüszerkesztés kezdete látszik, amikor beraktuk a MenuStrip-et:

A menü
IV.21. ábra - A menü


Válasszunk ki a fenti menübe egy menüelemet!

A menü definíció
IV.22. ábra - A menü definíció


A főmenüben lévő menüelem neve toolStripMenuItem1 lett. A mellette lévő menüelem (a főmenüben) helyére gépeljük be: Súgó!

A Súgó menü
IV.23. ábra - A Súgó menü


Az új menüelem neve súgóToolStripMenuItem lett, viszont a Text tulajdonságát már nem kell beállítani: az már megtörtént. Készítsünk a toolStripMenuItem1 alá három almenüelemet, a harmadik előtt elválasztót használva!

Almenük
IV.24. ábra - Almenük


Innentől a programozás ugyanúgy történik, mint a gombok esetén: a form_load függvényben megadjuk a feliratukat (text), majd a szerkesztőben rákattintva megírjuk a Click eseménykezelő függvényt:

private: System::Void toolStripMenuItem2_Click(System::Object^  sender, System::EventArgs^  e) {
// ide kell beírni, hogy mi történjen, ha a menüelemet kiválasztottuk.
 }

IV.2.17. Az alaphelyzetben nem látható ContextMenuStrip vezérlő

A MenuStrip vezérlő főmenüje mindig látható. A ContextMenuStrip csak tervezés közben látható, futás közben csak akkor, ha programból megjelenítjük. Célszerű alkalmazása a jobb egérgomb lenyomására az egérmutatónál megjelenő helyi menü. A tervezőben elkészítjük a menü elemeit, a szerkesztőben megírjuk a menüelemek Click eseménykezelőjét, majd a Form MouseDown vagy MouseClick eseménykezelőjében (nekik van koordináta paraméterük) megjelenítjük a ContextMenu-t.

Az alábbi ablak egy ContextMenu-t tartalmaz, három menüelemmel:

Kontextus menü
IV.25. ábra - Kontextus menü


A Form_MouseClick eseménykezelő a következőképpen néz ki:

private: System::Void Form1_MouseClick(System::Object^  sender, System::Windows::Forms::MouseEventArgs^  e) {
  if (e->Button == Windows::Forms::MouseButtons::Right) 
    contextMenuStrip1->Show(this->ActiveForm, e->X, e->Y); 
    // csak a jobb egérgombra 
    // mutatjuk a menüt a form-hoz képest.
}

IV.2.18. Az eszközkészlet menüsora: a ToolStrip vezérlő

Az eszközkészlet egymás mellett elhelyezkedő grafikus nyomógombokat tartalmaz. A gombok Image tulajdonsága hivatkozik a megjelenített képre. Miután nyomógombokról van szó, a Click eseményt futtatja le kattintás esetén. Az alábbi ábrán az eszközkészlet elemválasztéka látható, legfelül a képet tartalmazó gomb, amelynek automatikus neve „toolStripButton”-nal kezdődő lesz:

Menülem készlet
IV.26. ábra - Menülem készlet


IV.2.19. Az ablak alsó sorában megjelenő állapotsor, a StatusStrip vezérlő

Az állapotsor állapotinformációk megjelenítésére szolgál, emiatt célszerű, ha címkét, és progressbart készítünk hozzá. A ráhelyezett címke neve „toolStripLabel”-lel, a progressbar „toolStripProgressBar”–ral fog kezdődni. Nem szokás kattintani rajta, így nem írjuk meg a Click eseménykezelőket.

IV.2.20. A fileok használatában segítő dialógusablakok: OpenFileDialog, SaveFileDialog és FolderBrowserDialog

Szinte minden program, amelyben a munkánkat el lehet menteni, tartalmaz fájlmegnyitás, -mentés funkciókat. A fájl kijelölése mentésre, vagy beolvasásra minden programban ugyanolyan ablakkal történik. Ugyanis a .NET készítői ezeket a szokásos ablakokat megvalósították vezérlő szintjén. A vezérlőket a tervezési fázisban elhelyezzük a formunk alatt, de a program futása közben csak akkor látszanak, ha programból aktivizáljuk őket. Az aktivizálás a vezérlőobjektum egy metódusával (függvény) történik, a kimenet a dialógus eredménye (DialogResult: OK, Cancel stb). Az alábbi programrészletben kiválasztunk egy .CSV kiterjesztésű fájlt, és megnyitjuk az OpenFileDialog vezérlő segítségével, amennyiben a felhasználó nem a Cancel gombot nyomta le az Open helyett:

System::Windows::Forms::DialogResult dr;
openFileDialog1->FileName = "";
openFileDialog1->Filter = "CSV fileok (*.csv)|*.csv";
dr=openFileDialog1->ShowDialog(); // egy open ablak, csv fájlra
filenev = openFileDialog1->FileName; // kiszedjük a fájl nevét.
if (dr==System::Windows::Forms::DialogResult::OK ) // ha nem cancel-t nyomott
{
  sr = gcnew StreamReader(filenev); // olvasásra nyitjuk

A SaveFileDialog használata ugyanilyen, csak azt új fájl mentésére is használhatjuk. A FolderBrowserDialog-gal egy könyvtárat választhatunk ki, amelyben például az összes képet feldolgozhatjuk.

IV.2.21. Az előre definiált üzenetablak: MessageBox

A MessageBox egy üzenetet tartalmazó ablak, amit számunkra a Windows biztosít. Az üzenetablakban a felhasználó választhat egyet a parancsgombok közül. A gombok kombinációi előre definiált konstansok, és az ablak megjelenítésekor argumentumként adjuk meg. A vezérlőt ne keressük a ToolBoxban, nem kell a Formra rajzolni! Ugyanis statikus osztályként lett definiálva, a megjelenítése úgy történik, hogy meghívjuk a Show() metódusát, aminek visszatérési értéke az a gomb, amit a felhasználó megnyomott. Amennyiben biztosak vagyunk benne, hogy csak egy gombot választhat (mert csak az OK gombot tartalmazza a MessageBox), a metódus hívható utasításként is. A programunk futását a MessageBox felfüggeszti: vezérlői nem működnek, az aktuális eseménykezelő nem fut tovább, amíg valamit ki nem választunk a parancsgombok közül. A hívás szintaktikája: eredmény=MessageBox::Show(ablakban található üzenet, ablak fejléce, gombok);, ahol az egyes elemek adattípusai a következők:

  • eredmény: System::Windows::Forms::DialogResult típusú változó

  • ablakban található üzenet és az ablak fejléce: String^ tipusú adat

  • gombok: MessageBoxButtons felsorolás osztály valamelyik eleme, az alábbiak közül:

A MessageBox gombjainak beállítása
IV.27. ábra - A MessageBox gombjainak beállítása


A MessageBox meghívható egyetlen szöveg argumentummal is, ekkor fejléce nem lesz, és egyetlen „OK” gomb jelenik meg. Nem nevezhető szép megoldásnak.

Az alábbi példát már láttuk, most már értelmezni is tudjuk:

System::Windows::Forms::DialogResult d;

d=MessageBox::Show("Biztos, hogy használni akarja a légzsákot ?”,
"Fontos biztonsági figyelmeztetés",MessageBoxButtons::YesNo);
if (d == System::Windows::Forms::DialogResult::No) e->Cancel=true;

A programrészlet futásának eredménye:

A MessageBox
IV.28. ábra - A MessageBox


IV.2.22. Az időzítésre használt vezérlő: Timer

A Timer vezérlő programrészlet periodikus futtatását teszi lehetővé, adott időközönként. A form alá rajzoljuk meg a tervezőben, mert futás közben nem látható. Interval tulajdonsága tartalmazza az időközt ezredmásodpercben, míg az Enabled tulajdonságának false-re állításával az időzítő letiltható. Amennyiben engedélyezve van, és az Interval használható értéket tartalmaz (>=20 ms), a Timer lefuttatja a Tick alapértelmezett eseménykezelőjét. A programozónak ügyelnie kell arra, hogy a programrészlet gyorsabban lefusson, mint a következő esemény érkezése (vagyis az idő lejárta). Az alábbi programrészlet felprogramozza a Timer1-et másodpercenkénti futásra:

timer1->Interval=1000; // másodpercenként
timer1->Enabled=true; // indulj !

A következő pedig az előbb felprogramozott időzítővel minden másodpercben kiírja az aktuális időt a form fejlécébe:

DateTime^ most=gcnew DateTime(); // idő típusú változó.
most=most->Now; // most mennyi idő van ?
this->Text=Convert::ToString(most); // form fejlécébe írjuk.

Ez a programrészlet továbbfejleszthető úgy, hogy ha (most->Minute==0) && (most->Second == 0), játssza le a kakukk.wav, 1sec-nél hosszabb hangfájt. Amennyiben úgy érezzük, hogy nem kell többször futnia, a Tick eseményben is kiadható az Enabled=false, ekkor a mostani esemény volt az utolsó.

IV.2.23. A SerialPort

A SerialPort vezérlő az rs-232-es szabványú soros port és a rákapcsolt periféria (modem, SOC, mikrokontroller, Bluetooth eszköz) közti kommunikációt teszi lehetővé. A portot fel kell paraméterezni, majd kinyitni. Ezután szöveges információkat (Write,WriteLine,Read,ReadLine) , valamint bináris adatokat (WriteByte, ReadByte) küldhetünk és fogadhatunk. Eseménykezelő függvény is rendelkezésre áll: amennyiben adat érkezik, a DataReceived esemény fut le. Az alábbi programrészlet végignézi az összes fellelhető soros portot, az "iCorset" nevű hardvert keresve, a "?" karakter kiküldésével. Ha nem találja meg, hibaüzenettel kilép. A virtuális soros portoknál (USB) a BaudRate paraméter értéke tetszőleges.

array<String^>^ portnevek; 
bool vezerlo_van=false;
int i;
portnevek=Ports::SerialPort::GetPortNames();
i=0;
while (i<portnevek->Length && (!vezerlo_van)) 
{
    if (serialPort1->IsOpen) serialPort1->Close();
    serialPort1->PortName=portnevek[i];
    serialPort1->BaudRate=9600;
serialPort1->DataBits=8;
    serialPort1->StopBits=Ports::StopBits::One;
serialPort1->Parity = Ports::Parity::None;
    serialPort1->Handshake = Ports::Handshake::None;
    serialPort1->RtsEnable = true;
    serialPort1->DtrEnable = false;
    serialPort1->ReadTimeout=200; // 0.2 s
    serialPort1->NewLine="\r\n";
    try {
        serialPort1->Open();
        serialPort1->DiscardInBuffer();// usb !
        serialPort1->Write("?");
        s=serialPort1->ReadLine();
        serialPort1->Close();
    } catch (Exception^ ex) {
        s="Timeout";
    }
    if (s=="iCorset") vezerlo_van=true;
    i++;
}
if (! vezerlo_van) {
    MessageBox::Show("No iCorset Controller.",
"Error",MessageBoxButtons::OK);
    Application::Exit();
}

IV.3. Szöveges, bináris állományok, adatfolyamok.

Számítógépünk operatív tára kisméretű (a tárolni kívánt adatmennyiséghez képest), és könnyen felejt: ki sem kell kapcsolni a számítógépet, elég kilépni a programból, és a memóriában tárolt változók értéke máris elvész. Emiatt már az első számítógépek, amelyek a ferritgyűrűs tárolók után készültek, háttértárat alkalmaztak, amelyen az éppen nem használt programokat, és az éppen nem használt adatokat tárolták. A tárolási egység a lemezfájl, amely logikailag összefüggő adatok összessége. A logikai fájl a lemez fizikai szervezésével való összekapcsolása, a programok számára az elérés biztosítása, a fájlrendszer kialakítása és kezelése az operációs rendszer feladata. Az alkalmazói programok a lemezfájlokra nevükkel hivatkoznak. A műveletek elkülönülnek aszerint, hogy kezeljük-e a fájl tartalmát, vagy sem. Például, egy fájl átnevezéséhez, vagy letörléséhez nem szükséges a tartalmának kezelése, elég a neve. A fájl neve tartalmazhatja az elérési utat is, ha nem tartalmazza, fejlesztés alatt a projektünk könyvtára az alapértelmezett, míg a lefordított program futtatásakor az exe fájl könyvtára az alapértelmezett.

IV.3.1. Előkészületek a fájlkezeléshez

Ellentétben a grafikával vagy a vezérlőkkel, a fájlkezelő névtér nem kerül be a formunkba, az új projekt készítésekor. Ezt nekünk kell megadni, a form1.h azon részében, ahol a többi névteret:

using namespace System::IO;

További teendő eldönteni, hogy mit tartalmaz a kezelendő fájl, és mit szeretnénk vele tenni:

  • Csak törölni, átnevezni, másolni szeretnénk, megvizsgálni a létezését.

  • Bájtonként (bájtokat tartalmazó blokként) szeretnénk kezelni, ha „buherátorok” vagyunk: vírusfelismerés, karakterkódolás stb.

  • Bináris fájl ismert, állandó hosszúságú rekordszerkezettel.

  • Szövegfájl, változó hosszúságú szövegsorokból, a sorok végén soremeléssel.

A fájl neve is kétféleképpen adható meg:

  • Beírjuk a programunkba a fájlnevet, „bedrótozzuk”. Ha csak saját magunknak készítjük mindig ugyanazt a fájlt használva, akkor egyszerű és gyors módszer a névmegadásra.

  • OpenFileDialog-ot vagy SaveFileDialog-ot használva (lásd IV.2.20. szakasz) a felhasználó kijelölheti a fájlt, amelyet kezelünk. Ilyenkor a fájl nevét a dialógusablakok FileName tulajdonsága tárolja.

IV.3.2. A statikus File osztály metódusai

bool File::Exists(String^ fájlnév) A fájlnévben megadott állomány létezését vizsgálja, amennyiben létezik, true a kimenet, amennyiben nem, false. Használatával elejét vehetjük néhány hibának: nem létező fájl megnyitása, fontos adatfájl véletlen felülírása.

void File::Delete(String^ fájlnév) Törli a megadott nevű állományt. A mai operációs rendszerekkel ellentétben a törlés nem egy „kuka”-ba való mozgatást jelent, hanem tényleges törlést.

void File::Move(String^ réginév, String^ újnév) A réginév-vel megadott lemezfájlt átnevezi az újnévre. Amennyiben a nevekben különböző elérési út található, az állomány másik könyvtárba kerül.

void File::Copy(String^ forrásfájl, String^ célfájl) A Move-hoz hasonló metódus, azzal a különbséggel, hogy a forrásfájl nem tűnik el, hanem megduplázódik. A lemezen egy új fájl jön létre, a forrásfájl tartalmával.

FileStream^ File::Open(String^ fájlnév,FileMode üzemmód) A fájlnév nevű állomány megnyitása. A FileStream^ nem gcnew-val kap értéket, hanem ezzel a metódussal. A szövegfájlnál nem kell használni, de az összes többi fájlnál (a bájtokat tartalmazó és a rekordokat tartalmazó bináris) is ezt kell alkalmazni. Az üzemmód értékei:

  • FileMode::Append a szövegfájl végére állunk, és írás üzemmódot kapcsolunk be. Ha a fájl nem létezik, új fájl készül.

  • FileMode::Create ez a mód új fájlt készít. Ha a fájl már létezik, felülíródik. Az elérési út könyvtárában az aktuális felhasználónak írási joggal kell rendelkeznie.

  • FileMode::CreateNew ez a mód is új fájlt készít, de ha a fájl már létezik, nem írja felül, hanem kivételt kapunk.

  • FileMode::Open létező fájl megnyitása, írásra/olvasásra. Általában, ha már a fájl készítésen túl vagyunk, pl. a FileOpenDialog után ezt az üzemmódot használjuk.

  • FileMode::OpenOrCreate a létező fájlt megnyitjuk, ha nem létezik, készítünk egy adott nevű fájlt.

  • FileMode::Truncate megnyitjuk a létező fájlt, és a tartalmát töröljük. A fájl hossza 0 bájt lesz.

IV.3.3. A FileStream referencia osztály

Amennyiben a fájlokat bájtonként, vagy binárisként kezeljük, deklaráljunk egy a fájl eléréséhez egy FileStream-et. A FileStream típusú osztálypéldányt nem gcnew-val hozzuk létre, hanem File::Open()-nel, ezáltal a fizikai lemezfájl és a FileStream összerendelődnek. A FileStream segítségével a lemezfile aktuális filepozíciója elérhető, és mozgatható. A pozíció és a mozgatás mértékegysége a bájt, adattípusa 64-bites egész, hogy 2 gigabájtosnál nagyobb fájlokat is kezelni tudjon. Gyakran használt tulajdonságai és metódusai:

  • Length: olvasható tulajdonság, fájl aktuális mérete bájtban.

  • Name: a lemezfájl neve, amit megnyitottunk.

  • Position: írható/olvasható tulajdonság, az aktuális fájlpozíció bájtban. A következő írási művelet erre a pozícióra fog írni, a következő olvasás innen fog olvasni.

  • Seek(mennyit, mihez képest) metódus a fájlpozíció mozgatására. A Position tulajdonsághoz képest megadható, hogy honnan értelmezzük az eltolást: a fájl elejétől (SeekOrigin::Begin), az aktuális pozíciótól (SeekOrigin::Current) , a fájl végétől (SeekOrigin::End). Ezt a műveletet kell akkor is használni, ha a FileStream-re BinaryReader-t vagy BinaryWriter-t kapcsolunk, azoknak nincs Seek() metódusa.

  • int ReadByte(), WriteByte(unsigned char) egy bájtnyi adatot olvasó, illetve író metódusok. Az irás/olvasás az aktuális pozícióban történik. Az operációs rendszer szintjén a fájlolvasás egy bájt típusú tömbbe történik, mert ezek a függvények egy egyelemű, bájtokat tartalmazó tömb olvasásaként vannak megvalósítva.

  • int Read(array<unsigned char>,eltolás,darab): bájt típusú tömb beolvasó metódus. Az olvasott adatok az eltolás indexű elemnél kezdődnek, és darab számú lesz belőlük. Visszatérő értéke, hogy hány bájtot sikerült olvasni.

  • Write(array<unsigned char>,eltolás,darab): bájt típusú tömböt kiíró metódus. Az írást az eltolás indexű elemnél kezdi, és darab számú elemet ír ki.

  • Flush(void): a buffereket aktualizálja, ha még a memóriában voltak adatok, kiírja a lemezre.

  • Close() : a FileStream bezárása. A fájlokat használat után mindig be kell csukni, hogy elkerüljük az adatvesztést és az erőforrások kifogyását.

IV.3.4. A BinaryReader referencia osztály

Amennyiben nem bájt típusú bináris adatokat szeretnénk fájlból olvasni, a megnyitott FileStream-et argumentumként megadva a konstruktorban BinaryReader-t használunk. A BinaryReader-t a szabványos gcnew operátorral hozzuk létre. Fontos megjegyezni, hogy a BinaryReader nem képes megnyitni a lemezfájlt, és hozzárendelni a FileStream-hez. A BinaryReader az alapvető adattípusokhoz tartalmaz metódusokat: ReadBool() , ReadChar(), ReadDouble(), ReadInt16(), ReadInt32(), ReadInt64(), ReadUInt16(), ReadString(), ReadSingle() stb. A file pozíció az olvasott adat hosszával inkrementálódik. A BinaryReader-t is be kell csukni használat után a Close() metódusával, a FileStream bezárása előtt.

IV.3.5. A BinaryWriter referencia osztály

Amennyiben bináris adatokat szeretnénk kiírni a FileStream-be, a BinaryWriter-t használjuk. Létrehozása a BinaryReader-hez hasonlóan, gcnew operátorral történik. A különbség annyi, hogy míg a Reader adott visszatérő adattípusú metódusokat tartalmaz, a Writer adott paraméterű, visszatérő érték nélküli metódust tartalmaz, nagyszámú túlterhelt változatot. A metódus neve Write, és többféle adattípussal hívható, bonyolultsági sorrendben a Bool-tól a cli::Array^ -ig. A bináris fájlkezelés összefoglaló ábrája az alábbiakban látható:

A fájlkezelés modellje
IV.29. ábra - A fájlkezelés modellje


IV.3.6. Szövegfájlok kezelése: StreamReader és StreamWriter referencia osztályok

Az előzőekben tárgyalt bináris fájlok állandó hosszúságú rekordokból állnak, így a programunk egy egyszerű szorzás művelettel ki tudja számítani, hogy hányadik bájton kezdődik a keresett adat a fájlban, és a seek() művelettel oda tud állni. Például 32-bites egész típusú adat tárolásánál, amely 4 bájtot foglal, a 10-es indexű adat (igazából a 11. adat) a 10*4=40-edik bájton kezdődik. Ekkor – mivel a filemutatót bárhova mozgathatjuk – tetszőleges (random) elérésről beszélünk. A bináris fájlokat csak az a program tudja feldolgozni, aki ismeri a rekordszerkezetét, vagyis általában az a program, ami létrehozta.

A szövegfájlok változó hosszúságú, ember által is olvasható sorokból állnak. A fájlban a karakterek ASCII, Unicode, UTF-8 stb. kódolással tárolódnak, a szöveges állomány egy sora a fordító String^ adattípusának felel meg. A sorok végén CR/LF (két karakter) található DOS/Windows alapú rendszereknél. A változó sorhossz miatt a szövegfájlok csak szekvenciálisan kezelhetők: a 10. sor beolvasása úgy történik, hogy a fájl elejéről beolvasunk 9 sort, majd a nekünk kellő 10-ediket. A fájl megnyitása után nem tudjuk megmondani, hogy a fájlban hányadik bájton kezdődik a 10. sor, csak végigolvasás után.

A szöveges állományok fontos alkalmazása a különböző programok közti kommunikáció. Mivel olvashatók például Jegyzettömbbel, képesek vagyunk feldolgozni egy másik program által készített szövegfájlt. A szövegfájlokat használják például az adatbáziskezelők mentésre (ott dumpnak hívják a szövegfájlt, ami SQL utasításokat tartalmaz, amik a lementett adatbázist egy üres rendszeren létrehozzák), az Excellel való kommunikációra (a vesszővel vagy tabulátorral elválasztott fájl-ok CSV kiterjesztéssel), valamint az e-mailek is szövegfájlként mennek a küldő és a fogadó szerver között. A mérőberendezések is gyakran készítenek a mérésből valamilyen szöveges fájlt, amelyben soronként egy mérési adat szerepel, hogy tetszőleges programmal (akár Excellel) a mérést végző felhasználó feldolgozhassa, kirajzolhassa az eredményeket.

A szövegfájlok kezelését a StreamReader és a StreamWriter típusú referencia változókkal oldhatjuk meg. A gcnew operátor után a konstruktorban a fájl nevét adhatjuk meg, nem kell FileStreamet definiálnunk. Ennek oka, hogy a StreamReader és a StreamWriter kizárólagosan használja a lemezfájlt, emiatt elkészítheti magának a saját FileStreamjét (BaseStream), amivel a programozónak nem kell foglalkoznia. A StreamReader leggyakrabban használt metódusa a ReadLine(), amely a szövegfájl következő sorát olvassa be, és leggyakrabban használt tulajdonsága az EndOfStream, amely a fájl végén igazzá válik. Vigyázat: az EndOfStream az utolsó olvasás állapotát mutatja, a fájl végén a ReadLine() nulla hosszúságú sztringgel tér vissza, és az EndOfStream értéke true lesz! Vagyis a szokásos elöltesztelt ciklus használható (while (! StreamReader->EndOfStream) …), csak a beolvasás után meg kell vizsgálni, hogy a beolvasott sztring hossza nagyobb-e, mint 0. A StreamWriter leggyakrabban használt metódusa a WriteLine(String), amely a paraméterként megkapott sztringet és egy soremelést kiír a textfájlba. Létezik még Write(String) is, amely nem ír soremelést. A soremelés (CR,LF,CR/LF) beállítható a NewLine tulajdonsággal.

Az alábbi, önállóan, segédfüggvények és inicializáló rész nélkül nem működő programrészlet egy CSV kiterjesztésű, pontosvesszőkkel elválasztott sorokból álló szövegfájlt dolgoz fel. A felhasználóval kijelölteti a feldolgozandó fájlt, beolvassa az adatokat soronként, valamit kiszámol az adatokból, majd a beolvasott sor végére írja az eredményt. A feldolgozás végén számol egy összesített eredményt is. Az összes kimenetet egy átmeneti fájlba írja, hiszen az eredeti szövegfájl olvasásra van nyitva. Ha kész a feldolgozás, az eredeti fájlt letörli, és az átmeneti fájlt átnevezi az eredeti nevére. Eredmény: az eredeti szövegfájlba belekerültek a számítás eredményei.

private: System::Void button1_Click(System::Object^  sender, System::EventArgs^  e) {
int m, kp, jegy, ok = 0, oj = 0;
System::Windows::Forms::DialogResult dr;
String^ targy, ^sor, ^kiir = "";
openFileDialog1->FileName = "";
openFileDialog1->Filter = "CSV fileok (*.csv)|*.csv";
dr=openFileDialog1->ShowDialog(); // egy open ablak, csv file-ra
filenev = openFileDialog1->FileName; // kiszedjük a fájl nevét.
if (dr==System::Windows::Forms::DialogResult::OK ) // ha nem cancel-t nyomott
{
  sr = gcnew StreamReader(filenev); // olvasásra
  sw = gcnew StreamWriter(tmpnev); // írásra nyitom meg
  while (!sr->EndOfStream) // mindig elöltesztelttel
  {
    sor = sr->ReadLine(); // beolvasunk egy sort.
    if ((sor->Substring(0,1) != csk) && (sor->Length > 0)) 
    // ha nem elválasztó : feldolgozzuk 
    {
      m = 0; // minden sort az első karaktertől nézzük
      targy = ujadat(sor,  m); // szétszedjük a sorokat
      kp = Convert::ToInt32(ujadat(sor, m)); // 3 részre
      jegy = Convert::ToInt32(ujadat(sor,  m));
      // kiiratást készítünk címkébe
      kiir = kiir + "tárgy:"+targy + " kredit:" + 
             Convert::ToString(kp) +" jegy:" + 
             Convert::ToString(jegy) + "\n";
      // és súlyozott átlagot számolunk
      ok = ok + kp;
      oj = oj + kp * jegy;
      sw->WriteLine(sor+csk+Convert::ToString(kp*jegy));
   } else { // nem dolgozzuk fel, de visszaírjuk.
      sw->WriteLine(sor);
    } // if
  } // while
  sr->Close(); // ne felejtsük el bezárni
  sa = (double) oj / ok; // különben az eredmény egész.
  // az előző végén \n volt, új sorba írjuk az eredményt
  kiir = kiir + "súlyozott átlag:" + Convert::ToString(sa);
  label1->Text = kiir; // ékezetből baj lenne. utf-8
  sw->WriteLine(csk + "sulyozott atlag"+ csk+Convert::ToString(sa)); 
  sw->Close(); // a kimenetit is bezárom,
  File::Delete(filenev); // a régi adatfile-t letörlöm
  // és az átmenetit átnevezem az adatfile nevére.
  File::Move(tmpnev, filenev); 
  }
}

IV.3.7. A MemoryStream referencia osztály

Készíthetünk olyan szekvenciális, bájtokból álló fájlt, amely nem a lemezen található, hanem a memóriában. A memóriában létrehozott adatfolyamnak nagy előnye a sebessége (a memória legalább egy nagyságrenddel gyorsabb, mint a háttértár), hátránya a kisebb méret, és az a tulajdonság, hogy a programból való kilépéskor tartalma elvész. A MemoryStream-nek ugyanazok a metódusai, mint a FileStream-nek: egy bájtot vagy bájtokból álló tömböt írhatunk/olvashatunk a segítségével. Létrehozása a gcnew operátorral történik, megadható paraméteres konstruktor, ekkor beállíthatjuk a MemoryStream maximális méretét. Amennyiben nem adtunk meg paramétert, a MemoryStream dinamikusan foglal memóriát az írás műveletnél. Az osztály használatának két előnye van a tömbhöz képest: az automatikus helyfoglalás, valamint ha kinőnénk a memóriát, a MemoryStream könnyen FileStream-mé alakítható át, a gcnew helyett File::Open() utasítással.

IV.4. A GDI+

A GDI Graphics Device Interface a Windows rendszerek eszközfüggetlen, 2D-s grafika készítéséhez és megjelenítéséhez (nyomtatón, képernyőn) használt modulja (gdi.dll, gdi32.dll). A GDI+ ennek a modulnak a továbbfejlesztett – először a Windows XP és a Windows Server 2003 operációs rendszerekben megjelent – szintén 2D-s változata (GdiPlus.dll). Az újabb Windows rendszerek biztosítják a kompatibilitást a régebbi verzióhoz készült programokkal, a programozók azonban a GDI+ optimalizált, örökölt és új funkcióit is használhatják.

IV.4.1. A GDI+használata

A GDI+ objktum-orientált szemlélettel készült, azaz egy olyan 32/64 bites grafikai programozási felület, mely C++ osztályokat kínál a rajzoláshoz akár menedzselt (.NET), akár nem menedzselt (natív) programokban. A GDI nem közvetlenül a grafikus hardverrel, hanem annak meghajtó-programjával tartja a kapcsolatot. A GDI egyaránt alkalmas 2D-s vektorgrafikus ábrák rajzolására, képek kezelésére és kiadványok szöveges információinak rugalmas megjelenítésére:

  • A 2D-s vektorgrafika. A rajzok készítésekor koordináta-rendszerekben megadott pontok által meghatározott egyeneseket, görbéket és ezek által határolt alakzatokat rajzolhatunk tollakkal és festhetünk ki ecsetekkel.

  • Képek tárolására és megjelenítése. A rugalmas képtárolási és képkezelési lehetőségek mellett többféle képformátum (BMP, GIF, JPEG, Exif, PNG, TIFF, ICON, WMF, EMFll) beolvasására és mentésére van lehetőség.

  • Szövegek megjelenítése. A rengeteg betűtípus megjelenítése képernyőn és nyomtatón a GDI+ rendszer feladata.

A GDI+ alapfunkcióit a System::Drawing névtér tartalmazza. A 2D-s rajzolás további lehetőségeit elérhetjük a System::Drawing::Drawing2D névtér elemeivel. A képek haladószintű kezeléséhez tartalmaz osztályokat a System::Drawing::Imaging , míg a System::Drawing::Text névtér a szöveges megjelenítés speciális lehetőségeit biztosítja. [4.1.]

IV.4.2. A GDI rajzolási lehetőségei

Ahhoz, hogy programunkban elérjük a GDI+ alapfunkcióit használjuk a System::Drawing névteret! (Ennek eléréséhez a referenciák között – Project / Properties / References: - szerepelnie kell a System.Drawing DLL állománynak.).

using namespace System::Drawing;

A System::Drawing névtér tartalmazza a rajzoláshoz szükséges osztályokat (IV.30. ábra). Kiemelést érdemel a Graphicsosztály, amelyik – mint egy rajzlap – a rajzolási felületet modellezi. A rajzlapon való rajzoláshoz szükséges néhány adatstruktúra, amelyeket a szintén System::Drawing névtér definiál. A rajzlapon megadhatjuk, hogy milyen koordináta-rendszert használunk. A GDI+ színmodellje az alfa-r-g-b modell (a Color struktúra), ami azt jelenti, hogy színeket nemcsak a hagyományos piros, zöld, kék színből keverhetünk, hanem a szín átlátszóságát is beállíthatjuk az alfa paraméterrel. Pontok egész típusú x és y koordinátáit tárolhatjuk a Point típusú struktúrákban, valós (float) kordináták esetében a PointF használható. A pontokhoz hasonlóan típusfüggően használhatjuk a Rectangle és RectangleF struktúrákat a téglalapok átellenes sarokpontjainak többféle módon való elérésére és tárolására. Alakzatok vízszintes és függőleges méreteinek tárolását segítik a Size és SizeF struktúrák. Az igazi rajzeszközöket osztályok modellezik. Alapvető rajzeszközünk a toll (nem örököltethető Pen osztály) melynek beállíthatjuk a színét, a vastagságát és a mintázatát. A síkbeli alakzatok kifestésének eszköze az ecset (Brush ősosztály). A Brush leszármazottai az egyszínű festékbe „mártható” ecset modellje a SolidBrush és a bitkép mintázatú nyomot hagyó TextureBrush. A HatchBrush sraffozott mintát fest, míg a színjátszó festéknyomot hagynak a LinearGradientBrush és a PathGradientBrush. Utóbbi osztályok használatához a System::Drawing::Drawing2D névtér is szükséges. A leszármazott ecseteket nem örököltethetjük tovább. Az Image absztrakt osztály bitképek és metafájlok tárolására, kezelésére szolgáló adattagokkal és tagfüggvényekkel rendelkezik. A Font osztály tartalmazza a karakterek különböző megjelenítési formáit, a betűtípusokat. A FontFamily a betűtípuscsaládok modellje. Sem a Font, sem a FontFamily nem örököltethető tovább. A Region (nem örököltethető) osztály a grafikus eszköz téglalapok oldalaival és törtvonalak által határolt területét modellezi. Az Icon osztály a Windows ikonok (kisméretű bitképek) kezelésére szolgáló osztály.

A GDI+ osztályai
IV.30. ábra - A GDI+ osztályai


IV.4.3. A Graphics osztály

Mielőtt közelebbről megismerkednénk a Graphics osztállyal, néhány szót kell ejteni a vezérlők Paint eseményéről. Az ablakokban megjelenő grafikus objektumokat gyakran újra kell rajzolni, például újrafestéskor. Ez úgy történik, hogy az újrarajzoláshoz a Windows automatikusan érvényteleníti az újrafesteni kívánt területet, és meghívja az objektumok Paint eseményének kezelő függvényét. Ha tehát azt szeretnénk, hogy az általunk készített rajz a takarásból előbukkanó objektumokon frissüljön, akkor a Graphics osztály rajzoló metódusait a Paint esemény kezelőjében kell elhelyeznünk. A Paint eseménykezelőknek van egy PaintEventArgs típusú referencia-paramétere, amit a Windows a híváskor definiál [4.2.] ;

private: System::Void Form1_Paint(
    System::Object^ sender,
    System::Windows::Forms::PaintEventArgs^ e){ }

A PaintEventArgs osztály két tulajdonsága a Rectangle típusú ClipRectangle, amely az újrafestendő terület befoglaló adatait tartalmazza, és a Graphics típusú csak olvasható Graphics, amelyik az újrafestés során rajzeszközként használható rajzlapot azonosítja.

A későbbiekben részletesebben is megmutatjuk azt, hogy a rajzoláshoz Pen típusú tollat kell létrehoznunk a gcnew operátorral, mely a konstruktorának paramétereként megadható a toll színe a System::Drawing névtér Color::Red konstansával. Ha már van tollunk, akkor azzal a Graphics osztály Line() metódusa vonalat rajzol (IV.31. ábra).

private: System::Void Form1_Paint(System::Object^ sender, 
    System::Windows::Forms::PaintEventArgs^ e){
    Pen ^ p= gcnew Pen(Color::Red);
    e->Graphics->DrawLine(p,10,10,100,100);
}

A rajzolt vonal minden átméretezés után automatikusan megjelenik
IV.31. ábra - A rajzolt vonal minden átméretezés után automatikusan megjelenik


A programban bárhol kezdeményezhetjük mi is az ablakok és a vezérlők újrafestését. A Control osztály Invalidate(), Invalidate(Rectangle) és Invalidate(Region) metódusai érvénytelenítik az egész ablakot / vezérlőt, illetve annak kijelölt területét, és aktiválják a Paint eseményt. A Control osztály Refresh() metódusa érvényteleníti az ablak / vezérlő területét, és azonnali újrafestést kezdeményez.

Az ablakra bárhonnan rajzolhatunk a programból, ha magunk készítünk rajzlappéldányt a Graphics osztályból a tartalmazó vezérlő / ablak CreateGraphics() metódusával. Ekkor azonban csak egyszer jelenik meg a kék tollal rajzolt a vonal, és takarás után eltűnik.

    Pen ^ p= gcnew Pen(Color::Blue);
    Graphics ^ g=this->CreateGraphics(); 
    g->DrawLine(p,100,10,10,100);

Ha nem a Paint-ben rajzolunk, minimalizálás utáni nagyításkor eltűnik a kék vonal
IV.32. ábra - Ha nem a Paint-ben rajzolunk, minimalizálás utáni nagyításkor eltűnik a kék vonal


Az adott rajzlapot a megadott háttérszínnel törölhetjük a Graphics osztály

    Clear(Color & Color);

metódusával

IV.4.4. Koordináta-rendszerek

A GDI+ segítségével három koordináta-rendszerben is gondolkodhatunk. A világ koordináta-rendszer az, amiben elkészíthetjük modellünket, amit 2D-ben szeretnénk lerajzolni. Olyan leképezést célszerű használni, melyben a rajzoló metódusoknak ezeket a világkoordinátákat tudjuk átadni. Mivel a Graphics osztály rajzoló metódusai egész számok (int) és lebegőpontos valós (float) számokat képesek paraméterként fogadni, ezért célszerű a világkoordinátákat is int vagy float típussal tárolni. A 3D-s világ síkba való leképezéséről magunknak kell gondoskodnunk. A lap koordináta-rendszer az, amit a Graphics típusú referencia által modellezett rajzlap használ, legyen az form, képtároló, vagy akár nyomtató. Az eszköz koordináta-rendszere az, amit az adott eszköz használ (például a képernyő bal felső sarka az origó, és a pixelek az egységek). Rajzoláskor tehát két leképezés is végbemegy:

    világ_koordináta → lap_koordináta → eszköz_koordináta

Az első leképezést magunknak kell elkészíteni. Erre több lehetőségünk is van. Térbeli pontokat két alapvető módon vetíthetünk síkba párhuzamos és centrális vetítősugarakkal.

A párhuzamos sugarakkal való vetítést axonometriának hívjuk. A vetítés matematikai modelljét úgy állíthatjuk fel, hogy az x-y-z térbeli koordináta-rendszer párhuzamosan vetített képét berajzoljuk a ξ-η koordináta-rendszerrel bíró síkbeli lapra. Feltételezzük, hogy a térbeli koordináta-rendszer origójának képe a síkbeli koordináta-rendszer origója. Az (IV.33. ábra) ábrán az az x-y-z térbeli koordináta-rendszer képe szaggatott vonallal látszik a folytonos vonallal rajzolt ξ-η koordináta-rendszerben. Jelöljük az IV.33. ábra szerint a ξ-tengely és az x-tengely által bezárt szöget α-val, a ξ-tengely és az y-tengely által bezárt szöget β-val valamint az η-tengely és a z-tengely által bezárt szöget γ-val! Mivel az x-y-z koordináta-tengelyek nem párhuzamosak a ξ-η síkkal, a koordináta-egységek képe a síkon rövidebbnek látszik. Az x-irányú egység q x (≤1)-nek, az y-irányú egység q y (≤1) és a z-irányú egység q z (≤1) hosszúnak látszik.

Az általános axonometria
IV.33. ábra - Az általános axonometria


Ha tehát egy (x,y,z) koordinátájú pontjának keressük a (ξ,η) síkbeli, leképzett koordinátáit, akkor a leképezést megvalósító függvénypár az (f ξ , f η ) (IV.4.1) szerint.

 

(IV.4.1)

A leképezést egyszerűen felírhatjuk, ha arra gondolunk, hogy az origóból indulva a koordináta-tengelyek képeivel párhuzamosan, a rövidüléseknek megfelelően haladunk x, y és z egységnyit, akkor az (x, y, z) pont (ξ, η) képébe jutunk (IV.33. ábra). Ebből következik, hogy a piros nyíllal ξ és az η irányú vetületeit összegezve az alábbi (IV.4.2) módon adódnak a koordináták.

 

(IV.4.2)

Az axonometria speciális esete az izometrikus axonometria, amikor az x-y-z koordináta-tengelyek egymással 120°-os szöget zárnak be, a z-tengely képe az η-tengellyel esik egybe (α=30, β=30, γ=0) és a rövidülések q x =q y =q z =1 (IV.34. ábra)

Az izometrikus axonometria
IV.34. ábra - Az izometrikus axonometria


Másik elterjed axonometria a Cavalier-féle, vagy katonai axonometria, ahol a vízszintes y-tengely, a ξ-tengellyel esik egybe a függőleges z-tengely, az η-tengellyel esik egybe, az x-tengely pedig a másik kettővel 135°-os szöget zár be (α=45, β=0, γ=0). A rövidülések: q x =0,5, q y =q z =1.

A katonai axonometria
IV.35. ábra - A katonai axonometria


Alaphelyzetben a lap 2D-s koordináta-rendszerének origója – form esetében – a form aktív területének bal felső sarkában van, az x-tengely jobbra, az y-tengely pedig lefelé mutat, és mindkét tengelyen pixelek az egységek (IV.36. ábra).

Tehát amíg másképpen nem intézkedünk, a Graphics osztály már megismert DrawLine() metódusának paraméterei ebben a koordináta-rendszerben megadott értékek.

Példaként készítsük el a Cavalier-féle axonometria leképezését elkészítő függvényt! Ennek bemenő paraméterei a térbeli koordináták (x, y, z), a (IV.4.2) képlettel a Cavalier axonometria konstansait behelyettesítve a leképezés PointF típusú lappontot ad vissza.

A 2D-s koordinátarendszer
IV.36. ábra - A 2D-s koordinátarendszer


PointF Cavalier(float x, float y, float z) {
float X = 0;
float Y = 0;
    X=(-x*(float)Math::Sqrt(2)/2/2+y);
    Y=(-x*(float)Math::Sqrt(2)/2/2+z);
return PointF(X, Y);
}

Példaként, a Cavalier függvényt és a Graphics osztály DrawLine() metódusát alkalmazva, rajzoljunk a Paint eseményben egy (250,250,250) középpontú 100 oldalú kockát axonometriában ábrázolva!

private: System::Void Form1_Paint(System::Object^  sender, 
    System::Windows::Forms::PaintEventArgs^  e) {
// a középpont és a fél oldal                
float ox=250;
float oy=250;
float oz=250;
float d=50;
// A piros toll
    Pen ^p=gcnew Pen(Color::Red);
// A felső lap
    e->Graphics->DrawLine(p,Cavalier(ox-d,oy-d,oz-d),
                    Cavalier(ox+d,oy-d,oz-d));
    e->Graphics->DrawLine(p,Cavalier(ox+d,oy-d,oz-d),
                    Cavalier(ox+d,oy+d,oz-d));
    e->Graphics->DrawLine(p,Cavalier(ox+d,oy+d,oz-d),
                    Cavalier(ox-d,oy+d,oz-d));
    e->Graphics->DrawLine(p,Cavalier(ox-d,oy+d,oz-d),
                    Cavalier(ox-d,oy-d,oz-d));
// Az alsó lap
    e->Graphics->DrawLine(p,Cavalier(ox-d,oy-d,oz+d),
                    Cavalier(ox+d,oy-d,oz+d));
    e->Graphics->DrawLine(p,Cavalier(ox+d,oy-d,oz+d),
                    Cavalier(ox+d,oy+d,oz+d));
    e->Graphics->DrawLine(p,Cavalier(ox+d,oy+d,oz+d),
                    Cavalier(ox-d,oy+d,oz+d));
    e->Graphics->DrawLine(p,Cavalier(ox-d,oy+d,oz+d),
                    Cavalier(ox-d,oy-d,oz+d));
// Az oldalélek
    e->Graphics->DrawLine(p,Cavalier(ox-d,oy-d,oz-d),
                    Cavalier(ox-d,oy-d,oz+d));
    e->Graphics->DrawLine(p,Cavalier(ox+d,oy-d,oz-d),
                    Cavalier(ox+d,oy-d,oz+d));
    e->Graphics->DrawLine(p,Cavalier(ox+d,oy+d,oz-d),
                    Cavalier(ox+d,oy+d,oz+d));
    e->Graphics->DrawLine(p,Cavalier(ox-d,oy+d,oz-d),
                    Cavalier(ox-d,oy+d,oz+d));
}

Kocka axonometriában
IV.37. ábra - Kocka axonometriában


Az ábrán (IV.37. ábra) jól látható, hogy az axonometrikus leképzés adott koordinátairányban távolságtartó. Ezért a kocka hátsó éle ugyanolyan hosszúnak látszik, mint az első él. A szemünkkel azonban nem ilyen képet kapunk. A szemünk centrálisan vetít.

Centrális vetítés egy pontba érkező sugarakkal vetíti a képsíkra a térbeli pontokat. Ahhoz, hogy a centrális vetítés egyszerű képleteit megkapjuk, tegyük fel, hogy a térbeli x-y-z koordináta-rendszer pontjait az x-y síkkal párhuzamos S képsíkra vetítjük! Jelöljük a térbeli x-y-z koordináta-rendszer kezdőpontját C-vel! C lesz a vetítés centruma. Az S síkon lévő ξ-η koordináta-rendszer kezdőpontja O, legyen olyan, hogy a térbeli rendszer z-tengelyén d távolságra van C-től! A S sík ξ-η koordináta-rendszerének tengelyei párhuzamosak az x- és y-tengelyekkel (IV.38. ábra). Keressük a centrális vetítés (IV.4.1)-nek megfelelő leképezését.

Centrális vetítés
IV.38. ábra - Centrális vetítés


Az ábrán látható módon legyen az origó centrum távolság d! Legyenek egy térbeli P pont koordinátái x, y, z és ennek legyen P * a képe a síkon ξ, η koordinátákkal. A P pont vetülete az x-z síkra P xz (ennek a z tengelytől mért távolsága éppen x koordináta), az y-z síkra P yz (ennek pedig a z tengelytől mért távolsága éppen y koordináta). P xz képe az S síkon P * ξ, és ennek a síkbeli koordináta-rendszer kezdőpontjától való távolsága éppen a ξ koordináta, Pyz képe az S síkon P * η és ennek a síkbeli koordináta-rendszer kezdőpontjától való távolsága éppen a η koordináta! Mivel COP * ξ háromszög hasonló CTP xz háromszöghöz, így

 

(IV.4.3)

azaz

 

(IV.4.4)

Hasonlóan, az yz síkban a CTP yz háromszög hasonló a COP η háromszöghöz, így

 

(IV.4.5)

azaz

 

(IV.4.6)

Ezzel létrehoztuk a (IV.4.1) leképezést, hiszen átrendezés után

 

(IV.4.7)

adódik. A (IV.4.7) képletekkel az a probléma, hogy a leképezés nem lineáris. Azonban lineárissá tehetjük úgy, hogy megnöveljük a dimenziószámot (kettőről háromra), és bevezetjük a homogén koordinátákat. Tekintsük a [x, y,z] térbeli pont helyett a négydimenziós [x, y, z, 1] pontot! Azaz az [x, y, z, w] négydimenziós térben a w=1 háromdimenziós altérben gondolkodunk. Ebben a térben a (IV.4.7) leképezés

 

(IV.4.8)

ami lineáris, hiszen átírható (IV.4.9) alakban.

 

(IV.4.9)

Példaként készítsük el a centrális vetítés leképezését végző Perspektiv függvényt! A bemenő paraméterek a térbeli koordináták (x, y, z), a (IV.4.4, IV.4.6) képlettel a d fókusztávolságot megadva, a leképezés egy PointF típusú lappontot ad vissza.

PointF Perspektiv(float d, float x, float y, float z) {
float X = 0;
float Y = 0;
    X = d * x / z;
    Y = d * y / z; 
return PointF(X, Y);
}

Példaként, a Perspektiv() függvényt és a Graphics osztály már ismert DrawLine() metódusát alkalmazva, rajzoljunk a Paint eseményben egy (150,150,150) középpontú 100 oldalú kockát f=150, f=450 és f=1050 fókusztávolságú axonometriában ábrázolva (IV.39. ábra)!

private: System::Void Form1_Paint(
        System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
// a középpont és a fél oldal                
float ox=150;
float oy=150;
float oz=150;
float d=50;
float f=350;
// A piros toll
    Pen ^p=gcnew Pen(Color::Red);
// A felső lap
    e->Graphics->DrawLine(p,Perspektiv(f,ox-d,oy-d,oz-d),
                    Perspektiv(f,ox+d,oy-d,oz-d));
    e->Graphics->DrawLine(p,Perspektiv(f,ox+d,oy-d,oz-d),
                    Perspektiv(f,ox+d,oy+d,oz-d));
    e->Graphics->DrawLine(p,Perspektiv(f,ox+d,oy+d,oz-d),
                    Perspektiv(f,ox-d,oy+d,oz-d));
    e->Graphics->DrawLine(p,Perspektiv(f,ox-d,oy+d,oz-d),
                    Perspektiv(f,ox-d,oy-d,oz-d));
    // Az alsó lap
    e->Graphics->DrawLine(p,Perspektiv(f,ox-d,oy-d,oz+d),
                    Perspektiv(f,ox+d,oy-d,oz+d));
    e->Graphics->DrawLine(p,Perspektiv(f,ox+d,oy-d,oz+d),
                    Perspektiv(f,ox+d,oy+d,oz+d));
    e->Graphics->DrawLine(p,Perspektiv(f,ox+d,oy+d,oz+d),
                    Perspektiv(f,ox-d,oy+d,oz+d));
    e->Graphics->DrawLine(p,Perspektiv(f,ox-d,oy+d,oz+d),
                    Perspektiv(f,ox-d,oy-d,oz+d));
// Az oldalélek
    e->Graphics->DrawLine(p,Perspektiv(f,ox-d,oy-d,oz-d),
                    Perspektiv(f,ox-d,oy-d,oz+d));
    e->Graphics->DrawLine(p,Perspektiv(f,ox+d,oy-d,oz-d),
                    Perspektiv(f,ox+d,oy-d,oz+d));
    e->Graphics->DrawLine(p,Perspektiv(f,ox+d,oy+d,oz-d),
                    Perspektiv(f,ox+d,oy+d,oz+d));
    e->Graphics->DrawLine(p,Perspektiv(f,ox-d,oy+d,oz-d),
                    Perspektiv(f,ox-d,oy+d,oz+d));
}

Láthatjuk a fókusztávolság hatását, pontosan úgy viselkedik a fókusztávolság-váltás, mint a kamerák objektívje. Kis fókusztávolság esetén nagy a látószög, erős a perspektíva, nagy fókusztáv esetén kicsi látószög, gyenge perspektíva.

A kocka perspektív nézetei
IV.39. ábra - A kocka perspektív nézetei


A fenti példákban minden esetben gondoskodtunk arról, hogy a térbeli koordinátákat síkba vetítsük, és olyan centrumot valamint méretet választottunk a kockának, hogy az az ábrára kerüljön. A kocka adatai lehetnek akár mm, akár m akár km egységben is megadva. Azzal, hogy kirajzoltuk a formra a középpontok és az élek hosszát egyaránt pixelben adtuk meg, azonban ezzel hallgatólagosan egy nagyítást kicsinyítést alkalmaztunk (mm->pixel, m->pixel, vagy km->pixel). A megjeleníteni kívánt geometria és az ablakméretek alapján ezt minden esetben meg kellene határoznunk (Window-Viewport transzformáció [4.3.] ). A GDI+ a Graphics osztály Transform tulajdonságát koordináta-transzformáció létrehozására használhatjuk, és ezzel könnyen megvalósíthatjuk a Window-Viewport transzformációt is.

IV.4.5. Koordináta-transzformáció

Mint láttuk a lap koordináta-rendszer kezdőpontja az ablak aktív területének bal felső sarka, az x-tengely jobbra, az y-lefelé mutat, és az egységek a képpontok (IV.36. ábra). A Graphics osztály Transform tulajdonsága egy Matrix osztály példányára hívatkozó mutató. A Matrix osztály a System::Drawing::Drawing2D névtéren definiált.

A Mátrix osztály a 2D-s sík 3x3 elemet tartalmazó, homogén koordinátás transzformációs mátrixának modellje, melynek harmadik oszlopa [0,0,1]T, és m 11 , m 12 , m 21 , m 22 a koordináta-transzformáció forgatását és tengelyek menti nagyítását, a d x , d y pedig az eltolást jelenti (IV.4.10).

 

(IV.4.10)

A transzformácós mátrixok geometriai transzformációkat testesítenek meg [4.4] [.] . A geometriai transzformációk egymás után történő elvégzése egyetlen geometriai transzformációt eredményez. Ennek transzformációs mátrixa az első transzformációs mátrix szorozva balról a második transzformációs mátrixszal. A geometriai transzformációk egymás után történő alkalmazása nem felcserélhető művelet ugyanúgy ahogy a mátrixszorzás sem kommutatív. Erre a Transform tulajdonság megadásakor ügyelnünk kel. A transzformáció megadáskor használhatjuk a Matrix osztály metódusait.

A Mátrix osztály konstruktoraival létrehozhatunk transzformációs mátrix példányt. A paraméter nélkül aktivizált konstruktor

Matrix()

a háromdimenziós egységmátrioxot hoz létre. Például, ha készítünk egy E egységmátrixot, és azt megadjuk a Graphics->Trasform tulajdonságnak, az meghagyja a (IV.36. ábra) ábrán látható koordináta-rendszert.

private: System::Void Form1_Paint(System::Object^  sender,
        System::Windows::Forms::PaintEventArgs^  e) {
    Matrix ^ E=Matrix();
    e->Graphics->Transform=E;
    Pen ^ p= gcnew Pen(Color::Red);
    e->Graphics->DrawLine(p,10,10,100,100);
}

Ugyanezt az eredményt érjük el a Matrix osztály void típusú

Reset()

metódusával.

Matrix ^ E=Matrix();
E->Reset();

Tetszőleges – homogén koordinátákban lineáris – transzformációt megadhatunk, ha valamelyik paraméteres konstruktort alkalmazzuk. A transzformáció definíciójaként egy téglalapot torzíthatunk paralelogrammává úgy, hogy közben el is toljuk (IV.40. ábra). Ennek érdekében a transzformációt az alábbi paraméteres konstruktorral hozhatjuk létre:

Matrix(Rectangle rect, array<Point>^ pontok)

A konstruktor olyan geometriai transzformációt hoz létre, amelyik a paraméterként megadott rect téglalapot paralelogrammává torzítja. (IV.40. ábra) A pontok tömb három pontot tartalmaz. A paralelogramma bal felső sarokpontja (bf) az első, a paralelogramma jobb felső sarokpontja (jf) a második és a bal alsó sarok (ba) a harmadik elem (a negyedik sarok helye adódik).

A torzítás megadása
IV.40. ábra - A torzítás megadása


A Graphics osztály DrawRectangle() metódusa a toll, a kezdőpont-koordináták, a szélesség és a magasság megadásával téglalapot rajzol a formra. Az alábbi példában megrajzoljuk a (10,10) kezdőpontú 100 széles, 100 magas téglalapot pirossal. A transzformáció megadásakor úgy módosítottuk a téglalapot, hogy az a (20,20) kezdőpontba kerüljön, 200 magas legyen, és 45 fokkal legyen döntve. Ezután kirajzoltuk ugyanazt a téglalapot kékkel, látható, hogy az eltolás, a nagyítás és a nyírás is érvényesül.

private: System::Void Form1_Paint(System::Object^ sender, 
        System::Windows::Forms::PaintEventArgs^ e) {
    Pen ^ pen= gcnew Pen(Color::Red);
    e->Graphics->DrawRectangle(pen,10,10,100,100);
array< PointF >^ p = {PointF(120,220), PointF(220,220), 
    PointF(20,20)}; 
    Matrix ^ m = gcnew Matrix(RectangleF(10,10,100,100),p);
    e->Graphics->Transform=m;
    pen= gcnew Pen(Color::Blue);
    e->Graphics->DrawRectangle(pen,10,10,100,100);
}

A torzítás
IV.41. ábra - A torzítás


A transzformációs mátrix megadható koordinátánként is a (IV.4.7 képlet) alapján, a másik paraméteres konstruktorral.

Matrix(float m11, float m12, float m21, float m22, float dx, float dy);

Az alábbi példa 10,10 eltolást, x-irányban kétszeres, y-irányban pedig másfészeres nagyítást készít.

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
    Pen ^ pen= gcnew Pen(Color::Red);
    e->Graphics->DrawRectangle(pen,0,0,100,100);
    Matrix ^ m = gcnew Matrix(2,0,0,1.5,50,50);
    e->Graphics->Transform=m;
    pen= gcnew Pen(Color::Blue);
    e->Graphics->DrawRectangle(pen,0,0,100,100);
}

Eltolás nyújtás mátrix-szal
IV.42. ábra - Eltolás nyújtás mátrix-szal


Természetesen le is kérdezhetjük a mátrix elemeit a Matrix osztály Elements, Array<float> típusú, csak olvasható tulajdonságával.

Szerencsére nemcsak a mátrix koordinátáinak megadásával készíthetünk transzformációt, hanem a Matrix osztály transzformációs metódusaival is. A Matrix osztály példányaira úgy alkalmazhatjuk a transzformációs függvényeket, mintha az aktuális példány mátrixát jobbról, vagy balról szoroznánk a transzformációs metódus mátrixával. Ennek szabályozását segíti a MatrixOrder típusú felsorolás, melynek tagjai MatrixOrder::Prepend (0, jobbról)  és MatrixOrder::Append (1, balról).

Közvetlenül szorozhatjuk a matrixot tartalmazó osztály példányát a megadott Matrix transzformációs mátrixszal a

void Multiply(Matrix matrix [,MatrixOrder order])

metódussal. A szögletes zárójel azt jelenti, hogy az order MatrixOrder típusú paramétert nem kötelező megadni. Az alapértelmezett érték a MatrixOrder::Prepend. Ennek ismételt magyarázatát a következő metódusok esetén elhagyjuk.

Eltolást definiálhatunk, és alkalmazhatunk a mátrix példányon a

void Translate(float offsetX, float offsetY 
        [,MatrixOrder order]);

metódussal.

Forgatási transzformációt is végrehajthatunk a koordináta-rendszer origója körül alfa szöggel

void Rotate(float alfa [, MatrixOrder order])

vagy egy megadott pont körül alfa – fokban megadott (!) – szöggel;

void RotateAt(float alfa, PointF  pont [,MatrixOrder order]);

A következő példa a form közepe körül forgat egy egyenest. Ehhez tudnunk kell, hogy a form Size típusú ClientSize tulajdonsága tartalmazza a kliensterület méreteit.

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
    Matrix ^ m = gcnew Matrix();
    Pen ^ p = gcnew Pen(Color::Red);
float x=this->ClientSize.Width/2;
float y=this->ClientSize.Width/2;
for (int i=0; i<360; i+=5) {
        m->Reset();
        m->Translate(x,y);
        m->Rotate(i);
    e->Graphics->Transform=m;
    e->Graphics->DrawLine(p,0.0F,0.0F,
(float)Math::Min(this->ClientSize.Width/2,
this->ClientSize.Height/2),0.0F);
    }
}

Eltolás és forgatás
IV.43. ábra - Eltolás és forgatás


Hasonló függvénnyel vehető igénybe a koordináta-tengelyek irányában a skálázás (scaleX, scaleY)

void Scale(float scaleX, float scaleY[, MatrixOrder order]);

A nyírás egy koordináta-irány mentén azt jelenti, hogy a tengely helyben marad, és ahogy távolodunk a tengelytől a pontok a fix tengellyel párhuzamos eltolása arányos a tengelytől való távolsággal.  

A nyírás homogén koordinátás transzformációs mátrixa:

 

(IV.4.11)

A nyírás megadásához is van függvényünk, ahol a mátrix nyírási koordinátáit lehet megadni:

void Shear(float mX, float mY, [, MatrixOrder order]);

Az alábbi példában eltolunk, és mindkét irányban nyírunk egy téglalapot.

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
    Pen ^ pen= gcnew Pen(Color::Red);
    e->Graphics->DrawRectangle(pen,0,0,100,100);
    Matrix ^m=gcnew Matrix();
        m->Translate(10,10);
        m->Shear(2,1.5);
    e->Graphics->Transform=m;
    pen= gcnew Pen(Color::Blue);
    e->Graphics->DrawRectangle(pen,0,0,100,100);
}

Eltolás és nyírás
IV.44. ábra - Eltolás és nyírás


Kiszámíthatjuk a transzformációs mátrix inverzét is a Matrix osztály void típusú

Invert();

metódusával.

Ha nem akarjuk a Graphics osztály Transform tulajdonságát alkalmazni, akkor használhatjuk közvetlenül a Graphics osztály

void ResetTransform()
void MultiplyTransform(Matrix m, [, MatrixOrder order]);
void RotateTransform(float szog , [, MatrixOrder order])
void ScaleTransform(float sx, float sy[, MatrixOrder order]) 
void TranslateTransform Method(float dx, float dy
    [, MatrixOrder order])

metódusait, melyek közvetlenül állítják a Graphics osztály Transform tulajdonságát.

A lapokat az eszközre leképező laptranszformáció használhatja a Graphics osztály PageUnit és PageScale tulajdonságait a leképezés megadásához. A PageUnit tulajdonság a System::Drawing::GraphicsUnit felsorolás elemeit veheti fel (UnitPixel=2, UnitInch=4, UnitMillimeter=6…).

Példaként a milliméter lapegységet állítjuk be, akkor a 20 mm oldalhosszú négyzet a képernyőn is körülbelül 20 mm oldalhosszú lesz. Ha beállítjuk a Graphics osztály PageScale float típusú tulajdonságát, akkor az skálázza léptékünket. Például, ha a PageScale = 0.5, akkor a 20 mm oldalú négyzet 10 mm oldalú lesz (IV.42. ábra).

private: System::Void Form1_Paint(System::Object^  sender,
        System::Windows::Forms::PaintEventArgs^  e) {
    e->Graphics->PageUnit=GraphicsUnit::Millimeter;
    Pen ^ pen= gcnew Pen(Color::Red);
    e->Graphics->DrawRectangle(pen,10,10,30,30);
    e->Graphics->PageScale=0.5;
    pen= gcnew Pen(Color::Blue);
    e->Graphics->DrawRectangle(pen,10,10,30,30);
}

A mm skála és a PageScale tulajdonság
IV.45. ábra - A mm skála és a PageScale tulajdonság


A GDI+ a meghajtó-programtól kapja a képernyőfelbontás információt, innen tudja a leképezés adatait beállítani. A DpiX és DpiY csak olvasható tulajdonság tartalmazza az adott eszközre vonatkozó dot/inch felbontásokat.

IV.4.6. A GDI+ színkezelése (Color)

A GDI+ színek tárolására a Color struktúrát használjuk. Ez nem más, mint egy 32-bites egész, melynek minden egyes bájtja jelentéssel bír (szemben a hagyományos GDI színekkel, ahol a 32 bitből csak 24 kódolt színt). A 32-bites információt ARGB színnek hívjuk, a felső byte (Alpha) az áttetszőséget a maradék 24 bit pedig a GDI-nek megfelelő Red, Green, és Blue intenzitás-összetevőket kódolja. (Alpha=0 a teljesen átlátszó, 255 a nem átlátszó, a színösszetevőknél az érték az adott összetevő intenzitását jelöli.) Színleíró struktúrát hozhatunk létre a System::Drawing::Color struktúra FromArgb() metódusával. Több túlterhelt lehetőség között egész paraméterekkel megadhatjuk az r, g és b összetevőket, vagy négy egésszel az a, r, g és b összetevőket. Használhatunk több előre definiált színt is Color::Red, Color::Blue stb. A rendszerszíneket is elérhetjük SystemColors::Control, SystemColors::ControlText stb.

Az ablak háttérszínét beállító klasszikus alkalmazás jól mutatja az RGB színek alkalmazhatóságát. Az ablak háttérszíne csak RGB szín lehet. Helyezzünk fel három függőleges csúszkát a formra! A nevek (Name) legyenek rendre piros, zold és kek! Állítsuk be mindegyik maximumát 255-re (nem törődve azzal, hogy ilyenkor a csúszka nem tud 255-ig csúszni)! Mindegyik Scroll eseménye legyen a piros kezelője, ahogy ez a Designer generálta kódból látszik!

// 
// piros
// 
this->piros->Location = System::Drawing::Point(68, 30);
this->piros->Maximum = 255;
this->piros->Name = L"piros";
this->piros->Size = System::Drawing::Size(48, 247);
this->piros->TabIndex = 0;
this->piros->Scroll += gcnew 
    System::Windows::Forms::ScrollEventHandler(
this, &Form1::piros_Scroll);
// 
// zold
// 
this->zold->Location = System::Drawing::Point(187, 30);
this->zold->Maximum = 255;
this->zold->Name = L"zold";
this->zold->Size = System::Drawing::Size(48, 247);
this->zold->TabIndex = 1;
this->zold->Scroll += gcnew 
    System::Windows::Forms::ScrollEventHandler(
this, &Form1::piros_Scroll);
// 
// kek
// 
this->kek->Location = System::Drawing::Point(303, 30);
this->kek->Maximum = 255;
this->kek->Name = L"kek";
this->kek->Size = System::Drawing::Size(48, 247);
this->kek->TabIndex = 2;
this->kek->Scroll += gcnew 
    System::Windows::Forms::ScrollEventHandler(
this, &Form1::piros_Scroll);

A piros Scroll eseménykezelője pedig a háttérszín állításhoz felhasználja mindhárom csúszka pozícióját (Value tulajdonság):

private: System::Void piros_Scroll(System::Object^  sender,
    System::Windows::Forms::ScrollEventArgs^  e) {
this->BackColor = Color::FromArgb(
        piros->Value, zold->Value, kek->Value);
}

Színkeverő
IV.46. ábra - Színkeverő


Az áttetsző színek használatával a toll és az ecset rajzeszközöknél ismerkedünk meg. A Color struktúra csak olvasható R, G és B (Byte típusú) tulajdonságai a színösszetevőket tartalmazzák.

    Byte r = this->BackColor.R;

IV.4.7. Geometriai adatok (Point, Size, Rectangle, GraphicsPath)

Geometriai objektumokat hozhatunk létre a Point, PointF és a Rectangle, RectangleF struktúrákkal, illetve a téglalapok méretét tárolhatjuk a Size, SizeF típusú struktúrákban. A záró F betű a struktúra adattagjainak float típusára utal. Ha nincs F, akkor az adattagok int típusúak.

IV.4.7.1. Méretek tárolása

Size struktúrát létrehozhatunk a

    Size(Int32,Int32)
    Size(Point)
    SizeF(float,float)
    SizeF(PointF)

konstruktorokkal. A Size és a SizeF struktúrák (int, illetve float) tulajdonságai a Height és a Width, melyek a téglalapok magasságát és szélességét tartalmazzák. Az IsEmpty logikai típusú tulajdonság arról informál, hogy mindkét méret értéke 0-e (true), vagy sem (false).

A méretadatokkal műveleteket is végezhetünk a Size(F) struktúrák statikus metódusaival. Az Add() metódus összeadja a Size típusú paraméterek Width és Height értékekeit. A Subtract() kivonja őket egymásból.

    static Size Add(Size sz1, Size sz2);
    static Size Subtract(Size sz1, Size sz2);

Két méret típusú struktúra egyenlőségét vizsgálhatjuk a

    bool Equals(Object^ obj)

A fenti metódusoknak megfelelően definiáltak a Size(F) struktúrára túlterhelt operátorai a +, a és az ==.

SizeF típusú értékeket felfelé kerekít Size típusúra a Ceiling(), míg a szokásos módon kerekít a Round() metódus.

    static Size Ceiling(SizeF value);
    static Size Round(SizeF value);

A ToString() metódus {Width=xxx, Height=yyy} formájú stringre konvertálja a Size(F) struktúra tagjait.

Fontos megjegyezni, hogy mivel a formoknak van Size tulajdonságuk, ezért a Size típusú struktúrát csak a névtér megadásával használjuk. Az alábbi példa a form címsorára írja a létrehozott Size méreteit.

    System::Drawing::Size ^ x= gcnew     System::Drawing::Size(100,100);
this->Text=x->ToString();

IV.4.7.2. Síkbeli pontok tárolása

A Point, PointF struktúrák a Size jellegű struktúrákhoz nagyban hasonlítanak, céljuk azonban nem méretek megadása. Pontokat lehet létrehozni a

    Point(int dw);
    Point(Size sz);
    Point(int x, int y); 
    PointF(float x, float y);

konstruktorokkal. Az első esetben a paraméter alsó 16 bitje definiálja az x-, a felső az y-koordinátát. A második esetben a Size paraméter Width értéke lesz az x-, és a Height pedig az y-koordina. A pont struktúrák alapvető tulajdonságai az X, az Y koordinátaértékek és az IsEmpty vizsgálat.

A pontokkal műveleteket is végezhetünk a Point(F) struktúrák metódusaival. Az Add() metódus  a paraméterként kapott Point koordinátáihoz hozzáadja a szintén paraméterként kapott Size struktúra Width és Height értékeit A Subtract() kivonja őket egymásból.

    static Point Add(Point pt, Size sz);
    static Size Subtract(Point pt, Size sz);

Két pont típusú struktúra egyenlőségét vizsgálhatjuk a

    virtual bool Equals(Object^ obj) override

A fenti metódusoknak megfelelően a Point(F) struktúrák túlterhelt operátorai a +, a és az ==.

PointF típusú értékeket felfelé kerekít Point típusra a Ceiling(), míg a szokásos módon kerekít a Round() metódus.

    static Size Ceiling(SizeF value);
    static Size Round(SizeF value);

A ToString() metódus {X=xxx, Y=yyy} formájú stringre konvertálja a Point(F) struktúra adatait.

IV.4.7.3. Síkbeli téglalapok tárolása

A Rectangle és a RectangleF struktúrák téglalapok adatainak tárolására szolgálnak. Téglalapokat lehet létrehozni a

    Rectangle(Point pt, Size sz);
    Rectangle(Int32 x, Int32 y, Int32 width, Int32 height);
    RectangleF (PointF pt, SizeF sz); 
    RectangleF(Single x, Single y, Single width, Single height);

konstruktorokkal. Az X és Y, a Left és Top, illetve a Location tulajdonságok a Rectangle(F) bal-felső sarkának koordinátáit tartalmazzák. A Height, Width és Size a téglalapok szélességi és magassági méretei, a Right és Bottom tulajdonságok pedig a jobb-alsó sarok koordinátái. Az IsEmpty arról informál valódi téglalappal van-e dolgunk. Az Empty egy nem definiált adatokkal bíró téglalap.

    Rectagle Empty;
    Empty.X=12;

A bal-felső és a jobb-alsó sarok koordinátáival hozhatunk létre téglalapot (Rectangle, RectanleF) az alábbi metódusokkal

    static Rectangle FromLTRB(int left,int top,
                        int right,int bottom);
    static RectangleF FromLTRB(float left, float top, 
                        float right, float bottom)

A Rectangle struktúra rengeteg metódust kínál használatra. A RectanleF struktúrából felfelé kerekítéssel Rectangle struktúrát készít a Ceiling() metódus, szokásos kerekítéssel a Round() metódus, a tizedes rész levágásával pedig a Truncate() metódus.

Eldönthetjük, hogy a téglalap tartalmaz-e egy pontot, illetve téglalapot. a Rectangle struktúra

    bool Contains(Point p); 
    bool Contains(Rectangle r); 
    bool Contains(int x, int y); 

metódusaival, illetve RectangleF struktúra

    bool Contains(PointF p); 
    bool Contains(RectangleF r); 
    bool Contains(single x, single y); 

metódusaival.

Megnövelhetjük az aktuális Rectangle(F) területét a szélesség magasság adatokkal

    void Inflate(Size size);
    void Inflate(SizeF size);
    void Inflate(int width, int height);
    void Inflate(single width, single height);

A

    static Rectangle Inflate(Rectangle rect, int x, int y);
    static RectangleF Inflate(RectangleF rect, single x, single y);

metódusok meglévő téglalapokból készítenek új, megnövelt Rectangle(F) példányokat.

Elmozdíthatjuk a téglalapok bal-felső sarkát az

    void Offset(Point pos);
    void Offset(PointF pos);
    void Offset(int x, int y);
    void Offset(single x, single y);

metódusokkal.

Az

    void Intersect(Rectangle rect);
    void Intersect(RectangleF rect);

metódusok az aktuális példány paraméterrel való metszetét számítják. Az alábbi statikus metódusok a paraméterként megadott Rectangle(F) elemek metszetéből készítenek új példányt.

    static Rectangle Intersect(Rectangle a, Rectangle b);
    static RectangleF Intersect(RectangleF a, RectangleF b);

Meg is vizsgálhatjuk, hogy a téglalapot metszi-e egy másik a

    bool IntersectsWith(Rectangle rect);
    bool IntersectsWith(RectangleF rect);

metódusokkal.

A paraméterként megadott Rectangle(F) elemek úniójaként létrejövő téglalapot készítenek el a

    static Rectangle Union(Rectangle a, Rectangle b);
    static RectangleF Union(RectangleF a, RectangleF b);

metódusok.

Téglalapokra is működnek az

    virtual bool Equals(Object^ obj) override
    virtual String^ ToString() override

A Rectangle(F) típusú elemek között az == és a != operátorok is definiáltak.

IV.4.7.4. Geometriai alakzatok

Vonalak, görbék, szövegek láncolatából álló nyílt vagy zárt geometriai alakzatot (figurát) modelleznek a nem örököltethető System::Drawing::Drawing2D::GraphicsPath osztály példányai. A GraphicsPath objektumok összekapcsolt grafikus elemekből, akár GraphicsPath objektumokból állhatnak. A figuráknak van kezdőpontjuk (az első pont), végpontjuk (az utolsó pont) és jellemző az irányításuk. A figurák nem zárt görbék még akkor sem, ha a kezdő és a végpontok egybeesnek. Azonban készíthetünk zárt alakzatot is a CloseFigure() metódussal. Ki is festhetünk alakzatokat a Graphics osztály FillPath() metódusával. Ilyenkor a kezdő- és végpontokat összekötő egyenes zárja le az esetlegesen nem zárt alakzatokat. A kifestés módja az önmagukat metsző alakzatoknál beállítható. Többféle módon hozhatunk létre alakzatokat a GraphicsPath osztály konstruktoraival. Üres figurát hoz létre a

    GraphicsPath()

konstruktor. Intézkedhetünk a majdani kifestés módjáról a

    GraphicsPath(FillMode fillMode)

konstruktorral. A System::Drawing::Drawing2D::FillMode felsorolt típusú változó értékei Alternate és Winding az ábrának (IV.47. ábra) megfelelően. Az alapértelmezett Alternate a zárt alakzat minden egyes újra átlapolásánál vált, míg a Winding nem.

Alternate és Winding görbelánc
IV.47. ábra - Alternate és Winding görbelánc


Point(F) tömb (pts) által definiált figura is definiálható a

GraphicsPath(array<Point>^ pts, array<unsigned char>^ types 
        [,FillMode fillmode]);
GraphicsPath(array<PointF>^ pts, array<unsigned char>^ types 
         [,FillMode fillmode]);

konstruktorokkal. A types tömb PathPointType típusú elemek tömbje, ami minden egyes pts ponthoz görbetípust definiál. Lehetséges értékei például Start, Line, Bezier, Bezier3, DashMode.

A GraphicsPath osztály tulajdonságai a PathPoints, PathType tömbök, a PointCounts elemszám, a FillMode kitöltéstípus. A PathData osztály a PathPoints, PathType, tömbök egységbezárása, melynek tulajdonságai a Points és a Types.

Vonalszakaszokat, vonalszakaszok tömbjét is integrálhatjuk a figurába az

    void AddLine(Point pt1, Point pt2);
    void AddLine(PointF pt1, PointF pt2);
    void AddLine(int x1, int y1, int x2, int y2);
    void AddLine(float x1, float y1, float x2, float y2);
    void AddLines(array<Point>^ points);
    void AddLines(array<PointF>^ points);

metódusokkal.

Point(F) tömbök által meghatározott poligon csatlakozhat a figurához a

    void AddPolygon(array<Point>^ points);
    void AddPolygon(array<PointF>^ points);

metódusokkal.

Téglalapokat, illetve téglalapok tömbjét adhatjuk a figurához a

    void AddRectangle(Rectangle rect);
    void AddRectangle(RectangleF rect);
    void AddRectangles(array<Rectangle>^ rects);
    void AddRectangles(array<RectangleF>^ rects);

metódusokkal.

Ellipsziseket adhatunk a figurához a befoglaló téglalapjaik adataival.

    void AddEllipse(Rectangle rect);
    void AddEllipse(    RectangleF rect);
    void AddEllipse(int x, int y, int width, int height);
    void AddEllipse(float x, float y, float width, float height);

Ellipszisíveket, illetve ellipszisívek tömbjét adhatjuk a figurákhoz az

    void AddArc(Rectangle rect, float startAngle, float sweepAngle);
    void AddArc(RectangleF rect, 
            float startAngle, float sweepAngle);
    void AddArc(int x, int y, int width, int height, 
            float startAngle, float sweepAngle);
    void AddArc(float x, float y, float width, float height, 
            float startAngle, float sweepAngle);

metódusokkal. Az ellipszisív mindig a befoglaló téglalappal, a kezdőszöggel és a bezárt szöggel definiált.

Ellipszisív
IV.48. ábra - Ellipszisív


A Pie egy ellipszisívvel és két középpontból az ív szélső pontjaiig húzott egyenessel definiált alakzat. Ilyenek is csatlakoztathatók a figurához az

    void AddPie(Rectangle rect, float startAngle, float sweepAngle);
    void AddPie(int x, int y, int width, int height, 
            float startAngle, float sweepAngle);
    void AddPie(float x, float y, float width, float height, 
            float startAngle, float sweepAngle);

metódusokkal.

Bezier görbéket is illeszthetünk a figurába. A harmadfokú Bezier-görbét a síkban négy pont határozza meg. A kezdőpont az első, a végpont pedig a negyedik. A kezdő érintőt az első és második pont határozza meg, a záró érintőt a harmadik és a negyedik pont definiálja úgy, hogy a pontok közti vektor éppen a derivált háromszorosa (IV.49. ábra). A görbe paraméteres leírását a (IV.4.12) paraméteres egyenletek tartalmazzák [4.4.]

A harmadfokú Bezier-görbe
IV.49. ábra - A harmadfokú Bezier-görbe


 

(IV.4.12)

Bezier-görbék beillesztésére szolgálnak az alábbi metódusok:

    void AddBezier(Point pt1, Point pt2, Point pt3, Point pt4);
    void AddBezier(PointF pt1, PointF pt2, PointF pt3, PointF pt4);
    void AddBezier(int x1, int y1, int x2, int y2, 
                int x3, int y3, int x4, int y4);
    void AddBezier(float x1, float y1, float x2, float y2, 
                float x3, float y3, float x4, float y4);

Egymáshoz csatlakozó Bezier görbéket illesztenek a figurába a

    void AddBeziers(array<Point>^ points);
    void AddBeziers(array<PointF>^ points);

metódusok. A points tömbök tartalmazzák a végpontokat és a vezérlő pontokat úgy, hogy az első görbe az első négy ponttal meghatározott (IV.49. ábra, IV.4.12 egyenletek), minden további görbe három további ponttal definiált. Az aktuális görbe előtt elhelyezkedő görbe végpontja a kezdőpont. A két vezérlő pont és egy végpont az aktuális görbéhez tartozó három pont. Ha más görbe volt az előző görbe, akkor annak végpontja is lehet az első pont (0. rendben folytonos illesztés). A Graphics osztály DrawPath() metódusa kirajzolja a figurát (IV.50. ábra).

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
array<Point>^ pontok = {Point(20,100), Point(40,75), 
                Point(60,125), Point(80,100), 
                Point(100,150), Point(120,250), 
                Point(140,200)};
    GraphicsPath^ bPath = gcnew GraphicsPath;
    bPath->AddBeziers( pontok );
    Pen^ bToll = gcnew Pen( Color::Red);
    e->Graphics->DrawPath( bToll, bPath );
}

A harmadfokú Bezier-görbék folytonosan illesztve
IV.50. ábra - A harmadfokú Bezier-görbék folytonosan illesztve


Amennyiben a pontok sorozatára interpolációs görbét fektethetünk úgy, hogy az átmegy a pontokon és, minden pontban a görbe érintője arányos a két szomszédos pont által meghatározott vektorral, akkor azt a görbét kardinális spline-nak nevezzük. Ha a görbe idő szerinti paraméteres leírását tekintjük, akkor azt mondhatjuk, hogy a t k . időpontban a görbe éppen a P k ponton halad keresztül (IV.51. ábra). A görbe érintője a t k . időpontban V k , akkor az érintőt a (IV.4.13) egyenlet határozza meg.

 

(IV.4.13)

ahol f(<1) a görbe feszítése. Ha f=0, akkor az éppen a Catmull-Rom spline [4.4.] .

A kardinális spline
IV.51. ábra - A kardinális spline


A kardinális spline paraméteres leírását a (IV.4.14) egyenletek tartalmazzák.

 

(IV.4.14)

Szokás a széleken az egyoldalas differeciával számítani az érintőt, és ilyenkor a görbe minden ponton átmegy. A kardinális spline-ok használatának előnye az, hogy nincs szükség egyenletrendszer megoldására, és a görbe minden ponton átmegy. Hátránya az, hogy csak egyszer deriválhatók folytonosan.

Az

    void AddCurve(array<Point>^ points, 
            [[int offset, int numberOfSegments], 
            float tension]);
    void AddCurve(array<PointF>^ points, 
            [[int offset, int numberOfSegments], 
            float tension]);

metódusok kardinális spline-t adnak a figurához. A points tömb tartalmazza a tartópontokat. Az elhagyható tension paraméter a feszítést definiálja, míg az elhagyható offset paraméterrel megadhatjuk, hogy hányadik ponttól kezdve vesszük figyelembe a ponts tömb pontjait, ekkor a numberOfSegments paraméterrel megadhatjuk azt is, hogy hány görbeszakaszt szeretnénk (meddig vegyük figyelembe a points tömb elemeit). A tension paraméter elhagyásával vagy 0 értékével a Catmull-Rom görbét definiálhatjuk.

Zárt kardinális spline kerül a figurába az

void AddClosedCurve(array<Point>^ points, float tension);
void AddClosedCurve(array<PointF>^ points, float tension);

metódusokkal.

Az alábbi program az előző példa pontjaira fektet kardinális (Catmull-Rom) spline-t (IV.52. ábra)

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
array<Point>^ pontok = {Point(20,100), Point(40,75), 
                Point(60,125), Point(80,100), 
                Point(100,150), Point(120,250), 
                Point(140,200)};
    GraphicsPath^ cPath = gcnew GraphicsPath;
    cPath->AddCurve( pontok);
    Pen^ cToll = gcnew Pen( Color::Blue );
    e->Graphics->DrawPath( cToll, cPath );
}

Catmull-Rom spline
IV.52. ábra - Catmull-Rom spline


Szöveget illeszthetünk a figurába az

    void AddString(String^ s, FontFamily^ family, int style, 
        float emSize, Point origin, StringFormat^ format);
    void AddString(String^ s, FontFamily^ family, int style, 
        float emSize, PointF origin, StringFormat^ format);
    void AddString(String^ s, FontFamily^ family, int style, 
        float emSize, Rectangle layoutRect, 
        StringFormat^ format);
    void AddString(String^ s, FontFamily^ family, int style, 
        float emSize, RectangleF layoutRect, 
        StringFormat^ format);

metódusokkal. Az s a kiírandó szöveget tároló objektum referenciája. A betűk, betűtípusok lehetőségeivel még foglalkozunk, most csak annyit mondunk el, amennyi az aktuális példához szükséges. A family referencia a kiírás fontcsaládját tartalmazza. Ilyet létrehozhatunk például a egy meglévő fontcsalád nevével (lásd a példát). A style paraméter a FontStyle felsorolás egy eleme (FontStyle::Italic, FontStyle::Bold …), míg az emSize a betű befoglaló téglalapjának függőleges mérete. Meghatározhatjuk a kiírás helyét a Point(F) típusú origin paraméterrel, vagy a Rectangle(F) layoutRect paraméterrel. A format paraméter a StringFormat típusú objektum referencia példánya, használhatjuk a StringFormat::GenericDefault tulajdonságot.

Az alábbi példa egy figurába illeszti és figuraként kirajzolja a GDI+ és a rajzolás szavakat.

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
    GraphicsPath^ sPath = gcnew GraphicsPath;
    FontFamily^ family = gcnew FontFamily( "Arial" );
    sPath->AddString("GDI+", family, (int)FontStyle::Italic, 
                    20, Point(100,100), 
    StringFormat::GenericDefault);
    sPath->AddString("rajzolás", family, (int)FontStyle::Italic, 
                    20, Point(160,100), 
    StringFormat::GenericDefault);
    Pen^ cToll = gcnew Pen( Color::Blue );
    e->Graphics->DrawPath( cToll, sPath );
}

Szövegek a figurában
IV.53. ábra - Szövegek a figurában


Figurát (addingPath) adhatunk az aktuális figura példányhoz az

    void AddPath(GraphicsPath^ addingPath, bool connect);

metódussal. A connect változó arról gondoskodik, hogy összekötött legyen-e a két elem. A alábbi példa két éket köt össze.

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
array<Point>^ tomb1 = {Point(100,100), Point(200,200), 
                Point(300,100)};
    GraphicsPath^ Path1 = gcnew GraphicsPath;
    Path1->AddLines( tomb1 );
array<Point>^ Tomb2 = {Point(400,100), Point(500,200),
                Point(600,100)};
    GraphicsPath^ Path2 = gcnew GraphicsPath;
    Path2->AddLines( Tomb2 );
    Path1->AddPath( Path2, true );  // false
    Pen^ Toll = gcnew Pen( Color::Green);
    e->Graphics->DrawPath( Toll, Path1 );
}

Két összefűzött figura összekötve és nem összekötve
IV.54. ábra - Két összefűzött figura összekötve és nem összekötve


A figurát úgy lehet vastagítani, hogy a figura vonalaival (a sztring karaktereinek vonalaival is) párhuzamosan – esetleg adott távolságra és adott geometriai transzformáció után – rövid vonalszakaszokat húzunk. A

void Widen(Pen^ pen[, Matrix^ matrix[, float flatness]]);

metódussal. A pen paraméter vastagsága adja a párhuzamos vonalak eredeti vonaltól mért távolságát, az opcionális matrix paraméter által definiált transzformációval a párhuzamos húzás előtt eltolhatjuk a figurát. A flatness paraméter azt adja meg, hogy milyen hosszúak legyenek a párhuzamos szakaszok (a körből sokszög lesz). Az alábbi példa két kört eltolva szélesít (IV.55. ábra).

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
    GraphicsPath^ Path = gcnew GraphicsPath;
    Path->AddEllipse( 0, 0, 100, 100 );
    Path->AddEllipse( 100, 0, 100, 100 );
    e->Graphics->DrawPath( gcnew Pen(Color::Black), Path );
    Pen^ widenPen = gcnew Pen( Color::Black,10.0f );
    Matrix^ widenMatrix = gcnew Matrix;
    widenMatrix->Translate( 50, 50 );
    Path->Widen( widenPen, widenMatrix, 10.0f );
    e->Graphics->DrawPath( gcnew Pen( Color::Red ), Path );
}

Szélesített figura
IV.55. ábra - Szélesített figura


Ha nem párhuzamos görbét szeretnénk, hanem csak a figurát egymáshoz csatlakoztatott egyenes szakaszok halmazával kívánjuk helyettesíteni, akkor használhatjuk a

    void Flatten([Matrix^ matrix[, float flatness]]);

metódust, a paraméterek értelmezése megegyezik a Widen() metódusnál elmondottakkal.

Transzformációkat is alkalmazhatunk az aktuális figurára a

    void Transform(Matrix^ matrix);

metódussal.

Torzíthatjuk is a figurát a

void Warp(array<PointF>^ destPoints, RectangleF srcRect[, 
        Matrix^ matrix[, WarpMode warpMode[, 
        float flatness]]]);

metódussal. Ekkor a transzformáció az srcRect téglalapot a destPoints ponttömb által meghatározott négyszögre transzformálja, mintha a síklap gumiból lenne. A matrix opcionális paraméterrel ezen túlmenően még tetszőleges geometriai transzformációt is alkalmazhatunk. A warpMode opcionális paraméter lehet WarpMode::Perspective (ez az alapértelmezett) és WarpMode::Bilinear a IV.4.15 egyenleteknek megfelelően. A szakaszokra bontás pontosságát is definiálhatjuk a flatness opcionális paraméterrel.

 

(IV.4.15)

Az alábbi példa egy szöveget torzít a megfelelő téglalap(fekete) és négyszög (piros) által definiált, perspektív módon és eltolva.

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
    GraphicsPath^ myPath = gcnew GraphicsPath;
    RectangleF srcRect = RectangleF(10,10,100,200);
    myPath->AddRectangle( srcRect );
    FontFamily^ family = gcnew FontFamily( "Arial" );
    myPath->AddString("Torzítás", family, (int)FontStyle::Italic, 
        30,Point(100,100),StringFormat::GenericDefault);
    e->Graphics->DrawPath( Pens::Black, myPath );
    PointF point1 = PointF(200,200);
    PointF point2 = PointF(400,250);
    PointF point3 = PointF(220,400);
    PointF point4 = PointF(500,350);
array<PointF>^ destPoints = {point1,point2,point3,point4};
    Matrix^ translateMatrix = gcnew Matrix;
    translateMatrix->Translate( 20, 0 );
    myPath->Warp(destPoints, srcRect, translateMatrix, 
        WarpMode::Perspective, 0.5f );
    e->Graphics->DrawPath( gcnew Pen( Color::Red ), myPath );
}

Torzított figura
IV.56. ábra - Torzított figura


A GraphicsPathIterator osztály segítségével végigjárhatjuk a figura pontjait, jelölőket helyezhetünk el a SetMarker() metódussal, és a NextMarker() metódussal szeleteket készíthetünk a figurából. A ClearMarker() metódus törli a jelölőket.

Egy figurán belül több alfigura is megadható. Ezeket megnyithatjuk a

    void StartFigure();

metódussal.

Zárttá tehetjük a figurát az utolsó pont és az első pont egyenessel való összekötésével a

    void CloseFigure();

metódussal. Minden aktuálisan megnyitott figurát lezár a

    void CloseAllFigures();

metódus. Az utóbbi két metódus automatikusan új alfigurát nyit.

A

    RectangleF GetBounds([Matrix^ matrix[, Pen^ pen]]);

metódus visszaadja a figura befoglaló téglalapját. Ha használjuk az opcionális mátrix paramétert, akkor annyival eltolva, az elhagyható pen paraméter vastagsága minden oldalon megnöveli a befoglaló téglalap méreteit.

A

    bool IsVisible(Point point[, Graphics ^ graph]);
    bool IsVisible(PointF point[, Graphics ^ graph]);
    bool IsVisible(int x, int y[, Graphics ^ graph]);
    bool IsVisible(float x, float y[, Graphics ^ graph]);

metódusok igaz értékkel térnek vissza, ha az x, y koordinátájú pont, illetve a point paraméter által definiált pont benne van a figurában. Az opcionális graph paraméter egy rajzlapot jelöl, aminek az aktuális körbevágásában (a ClipRegion fogalommal a következő fejezet foglalkozik) vizsgálódunk.

A

    bool IsOutlineVisible(Point point, Pen^ pen);
    bool IsOutlineVisible(PointF point, Pen^ pen);
    bool IsOutlineVisible(int x, int y, Pen^ pen);
    bool IsOutlineVisible(float x, float y, Pen^ pen);
    bool IsOutlineVisible(Point point, Pen^ pen
                    [, Graphics ^ graph]);
    bool IsOutlineVisible(PointF point, Pen^ pen
                    [, Graphics ^ graph]);
    bool IsOutlineVisible(int x, int y, Pen^ pen
                    [, Graphics ^ graph]);
    bool IsOutlineVisible(float x, float y, Pen^ pen
                    [, Graphics ^ graph]);

metódusok igaz értékkel térnek vissza, ha az x, y koordinátájú pont, illetve a point paraméter által definiált pont benne van a figura befoglaló téglalapjában, feltéve, hogy pen paraméternek megfelelő tollal rajzoltuk őket. Az opcionális graph paraméter egy rajzlapot jelöl, aminek az aktuális körbevágásában vizsgálódunk.

A

    void Reverse();

metódus megfordítja a figura pontjainak sorrendjét (a PathPoints tömb tulajdonságban).

A

    void Reset();

metódus a figura minden adatát törli.

IV.4.8. Régiók

A régió osztály (System::Drawing::Region) téglalapokból és zárt körvonalakból létrehozott területet modellez, így a régió területe bonyolult alakú is lehet.

A régió példányát a konstruktorok valamelyikével hozhatjuk létre:

    Region();
    Region(GraphicsPath^ path);
    Region(Rectangle rect);
    Region(RectangleF rect);
    Region(RegionData^ rgnData);

Ha nem adunk meg paramétert, akkor üres régió keletkezik. A path, illetve a rect paraméterek a régió kezdeti alakját definiálják.

A régióval is körbevághatjuk a rajzunkat - a Graphics osztály

    void SetClip(Graphics^ g[, CombineMode combineMode]);
    void SetClip(GraphicsPath^ path[, CombineMode combineMode]);
    void SetClip(Rectangle rect[, CombineMode combineMode]);
    void SetClip(RectangleF rect[, CombineMode combineMode]);
    void SetClip(Region^ region, CombineMode combineMode);

metódusaival, így mint speciális képkeretet használhatjuk. A képkeret geometriáját egy másik rajzlap (g paraméter) képkerete, figura (path), téglalap (rect), vagy régió (region) is definiálhatja. A combineMode paraméter a CombineMode felsorolás értékeit veheti fel.

  • A Replace (a megadott elem a paraméter geometria G),

  • Intersect (az aktuális képkeret – R – metszete a megadott elemmel – G ),

  • Union (unió a megadott elemmel),

  • Xor (szimmetrikus differencia a megadott elemmel ),

  • Exclude (az eredeti és a megadott régió különbsége ) és

  • Complement (a megadott régió és az eredeti különbsége ).

A Windows a rajz frissítésére is használhat régiót.

A RegionData osztály tartalmaz egy régió adatait leíró bájt tömböt a Data tulajdonságában. A

    RegionData^ GetRegionData()

metódussal lekérhetjük a RegionData egy példányát az aktuális regióra vonatkozóan. A

    array<RectangleF>^ GetRegionScans(Matrix^ matrix)

metódus téglalapok egy tömbjével írja le az aktuális régiót. A matrix paraméter egy előzetes transzformáció mátrixát tartalmazza.

A

    void MakeEmpty()

metódus üres régóvá teszi az aktuális régiót, míg a

    void MakeInfinite()

metódus végtelen régiót készít az aktuális régióból.

A régiókkal is műveleteket végezhetünk. A

    void Complement(Region^ region);
    void Complement(RectangleF rect);
    void Complement(Rectangle rect);
    void Complement(GraphicsPath^ path);

metódusok hívásának eredményeként az argumentumként megadott geometria és az aktuális régió komplementerének metszete lesz az aktuális régió. Azaz az aktuális régió a paraméter-geometria és az aktuális régió különbsége lesz (Legyen R az aktuális régió pontjainak halmaza, G a megadott paraméter-geometria pontjainak halmaza, akkor a művelet leírása !).

Az

    void Exclude(Region^ region);
    void Exclude(RectangleF rect);
    void Exclude(Rectangle rect);
    void Exclude(GraphicsPath^ path);

metódusokkal az aktuális régió önmaga és az argumentumként megadott alakzat komplementerének metszete lesz, azaz az eredmény az aktuális régió és a paraméter-geometria különbsége ().

Az

    void Intersect(Region^ region);
    void Intersect(RectangleF rect);
    void Intersect(Rectangle rect);
    void Intersect(GraphicsPath^ path);

metódusokkal az aktuális régió önmaga és az argumentumként megadott geometria metszete lesz ().

Az

    void Union(Region^ region);
    void Union(RectangleF rect);
    void Union(Rectangle rect);
    void Union(GraphicsPath^ path);

metódusokkal az aktuális régió önmaga és az argumentumként megadott geometria uniója lesz ().

A

    void Xor(Region^ region); 
    void Xor(RectangleF rect);
    void Xor(Rectangle rect);
    void Xor(GraphicsPath^ path);

metódusokkal az aktuális régió csak azokat a pontokat fogja tartalmazni, amelyek csak az egyik geometria elemei voltak előzőleg. Az a geometria lesz, ami az aktuális régió és a megadott geometria uniójából levonjuk az aktuális régió és a megadott geometria metszetét (szimmetrikus differencia ).

Transzformációs mátrixszal vagy megadott eltolás-koordinátákkal transzformálhatjuk a régiót:

    void Transform(Matrix^ matrix);
    void Translate(int dx, int dy);
    void Translate(float dx, float dy);

Megvizsgálhatjuk, hogy a pont benne van-e az aktuális régióban:

    bool IsVisible(Point point[, Graphics gr]);
    bool IsVisible(PointF point[, Graphics gr]);
    bool IsVisible(float x, float y);
    bool IsVisible(int x, int y[, Graphics gr]);

Téglalapokról eldönthetjük van-e közös metszetük az aktuális régióval:

    bool IsVisible(Rectangle rect[, Graphics gr]);
    bool IsVisible(RectangleF rect[, Graphics gr]);
    bool IsVisible(int x, int y, int width, int height
                [, Graphics gr]);
    bool IsVisible(float x, float y, float width, float height
                [, Graphics gr]);

Mindkét esetben - a Graphics típusú elhagyható paraméterrel - a rajzlapot is kijelölhetjük

Az

    bool IsEmpty(Graphics^ g);

metódus eldönti, hogy az aktuális régió üres-e. az

    bool IsInfinite(Graphics^ g);

pedig azt hogy végtelen-e.

Az alábbi példa szöveggel figurát készít, és mielőtt kirajzolná, egy vágórégiót definiál

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
    GraphicsPath^ sPath = gcnew GraphicsPath;
    FontFamily^ family = gcnew FontFamily( "Arial" );
    sPath->AddString("Szöveg körbevágva", family, (int)FontStyle::Italic, 
        40,Point(0,0),StringFormat::GenericDefault);
    System::Drawing::Region ^ vag = gcnew     System::Drawing::Region( 
            Rectangle(20,20,340,15));
    e->Graphics->SetClip(vag, CombineMode::Replace);
    e->Graphics->DrawPath( gcnew Pen(Color::Red), sPath );
}

Körbevágott figura
IV.57. ábra - Körbevágott figura


IV.4.9. Képek kezelése (Image, Bitmap, MetaFile, Icon)

Képek tárolására két alapvető stratégia létezik. Az egyik eset, amikor a kép olyan mintha ceruzával rajzolnánk. Ilyenkor a kép adatainak tárolása úgy történik, hogy tároljuk a kép rajzolásához szükséges vonaldarabok (vektorok) adatait, és ennek alapján megrajzolhatjuk a képet. Az ilyen tárolási módot vektoros képtárolásnak hívjuk. A IV.58. ábra egy nagy A rajzolatát mutatja vektorosan.

Vektoros A
IV.58. ábra - Vektoros A


Lehetőségünk van arra is, hogy a képet képpontokra (raszterpontokra) osszuk és minden egyes pontról tároljuk annak színét. Ezt a raszteres képet a térképhez való hasonlatossága miatt bitképnek hívjuk. A IV.59. ábra egy ilyen fekete-fehér bitképét mutatja egy A betűnek.

Raszteres A
IV.59. ábra - Raszteres A


Az Image egy absztrakt képtároló osztály mely, őse Windows rajzokat tartalmazó raszteres System::Drawing::Bitmap és vektoros System::Drawing::Imaging::MetaFile osztályoknak. Image objektumok létrehozására az Image osztály statikus metódusait használjuk, melyekkel a megadott (filename) állományból tölthetünk be képet.

    static Image^ FromFile(String^ filename
                [, bool useEmbeddedColorManagement]);
    static Image^ FromStream(Stream^ stream 
                [, bool useEmbeddedColorManagement
                [, bool validateImageData]]);

Ha a useEmbeddedColorManagement logikai változó igaz, akkor az állományban lévő színkezelési információkat használja a metódus, egyébként nem (az alapértelmezett az igaz érték). Adatfolyam esetén a validateImageData logikai változó a kép adatellenőrzéséről intézkedik. Alaphelyzetben van ellenőrzés. Használhatjuk a régi GDI bitképeket is a FromHbitmap() metódussal.

A betöltött bitképeket kirajzolhatjuk a Graphics::DrawImage(Image,Point) metódussal. Az alábbi példa a Visual Stúdió fejlesztői környezetének képét tölti a formra (IV.60. ábra).

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
    Image^ kep = Image::FromFile( "C:\\VC++.png" );
    e->Graphics->DrawImage( kep, Point(10,10) );
}

Image a formon
IV.60. ábra - Image a formon


Az Image osztály tulajdonságai tárolják a kép adatait. Az int típusú Width és Height csak olvasható tulajdonságok a kép szélességét és magasságát adják meg pixelben. A Size struktúra típusú Size csak olvasható tulajdonság is a méreteket tárolja. A float típusú HorizontalResolution és VerticalResolution tulajdonságok a kép felbontását adják meg pixel/inch dimenzióban. A SizeF struktúra típusú PhisicalDimension csak olvasható tulajdonság a kép valódi méreteit adja bitkép esetén pixelben, metafile kép esetén 0,01 mm egységekben.

Az ImageFormat osztály típusú RawFormat tulajdonsággal kép fájlformátumot specifikálhatunk, amit aztán a kép tárolásánál felhasználhatunk. Az ImageFormat Bmp tulajdonsága bitképet, az Emf továbbfejlesztett metafilet, az Exif pedig cserélhető képet (Exchangeable Image File) specifikál. A Gif tulajdonságot a szabványos képformátumhoz használhatjuk (Graphics Interchange Format). A Guid tulajdonság a Microsoft által alkalmazott, globális objektumazonosítót választja ki. Az Icon tulajdonságot a Windows Iconhoz használhatjuk. A Jpeg a szabványos (Joint Photographic Experts Group) formátumhoz, a MemoryBmp memória bitképhez, a Png a W3C (World Wide Web Consortium [4.5.] ) grafikus elemek hálózati átviteléhez (Portable Network Graphics) való tulajdonság. A Tiff a szabványos TIFF formátumhoz (Tagged Image File Format), a Wmf a Windows metafilehoz tartozó formátumot specifikálja.

A Flags int típusú tulajdonság bitjei a kép jellegzetességeit (színkezelés, átlátszóság, nagyítás stb.) szabályozzák. Néhány jellegzetes bitállásnak megfelelő konstans az ImageFlagsNone (0), az ImageFlagsScalable (1), az ImageFlagsHasAlpha (2) ,valamint az ImageFlagsColorSpaceRGB (16).

A képek tárolhatják színeiket a képhez csatlakozó színpalettában. A ColorPaletteClass típusú Palette tulajdonság nem más mint egy színtömb (array<Color>^ Entries) ami a színeket tartalmazza. A ColorPaletteClass osztálynak is van Flags tulajdonsága, melynek bitenkénti értelmezése: a szín tartalmaz alpha információt(1), a szín szürkeskálát rögzít (2), a szín ún. halftone [4.6.] információt ad meg, amikor az emberi szemnek szürkeskálának tűnő színeket fekete-fehér elemekből építjük fel (IV.61. ábra).

Halftone ábrázolás
IV.61. ábra - Halftone ábrázolás


A kép méreteit a megadott grafikus egységben szolgáltatja a

    RectangleF GetBounds(GraphicsUnit pageUnit);

metódus.

A

    static int GetPixelFormatSize(PixelFormat pixfmt);

megadja, milyen módon tárolt a kép – ez lehet egy paletta index, vagy maga a szín értéke. Néhány lehetséges érték: Gdi – a pixel GDI színkódot tartalmaz (rgb). Alpha - a pixel tartalmaz attetszőséget is, Format8bppIndexed – index, 8 bit per pixel (256) szín.

Lekérdezhetjük, hogy a kép áttetsző-e:

    static bool IsAlphaPixelFormat(PixelFormat pixfmt);

hogy a kép pixelformátuma 32 bites-e:

    static bool IsCanonicalPixelFormat(PixelFormat pixfmt);

hogy a kép pixelformátuma 64 bites-e:

    static bool IsExtendedPixelFormat(PixelFormat pixfmt);

A

    void RotateFlip(RotateFlipType rotateFlipType);

metódussal forgatni és tükrözni lehet a képeket a RotateFlipType felsorolás elemeinek megfelelően (pl. Rotate90FlipNone 90 fokkal forgat nem tükröz, RotateNoneFlipX y-tengelyre tükröz, Rotate90FlipXY 90 fokkal forgat és középpontosan tükröz.

Az alábbi példa az y-tengelyre tükrözve teszi ki IV.60. ábra form képét.

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^ e) {
    Image^ kep = Image::FromFile( "C:\\VC++.png" );
    kep->RotateFlip(RotateFlipType::RotateNoneFlipX);
    e->Graphics->DrawImage( kep, Point(10,10) );
}

Elforgatott kép
IV.62. ábra - Elforgatott kép


A képeket fájlba menthetjük a

    void Save(String^ filename[,ImageFormat^ format]);

metódussal. A filename paraméter a fájl nevét tartalmazza, a kiterjesztés a filename kiterjesztésétől függ. Az opcionális format paraméterrel definiálhatjuk a mentés formátumát. Ha nincs kódoló információ a fájlformátumokhoz (mint pl a Wmf), akkor a metódus Png formátumba ment. Adatfolyamba is menthetjük a kép adatait a

    void Save(Stream^ stream, ImageFormat^ format);

metódussal.

A Bitmap az Image leszármazottja, így az Image összes tulajdonságát és funkcióját örökli, azonban a Bitmap adatstruktúrája megfelel a memóriában való tárolásnak, így egy sor speciális metódus használatára is lehetőség nyílik. A Bitmap osztálynak vannak konstruktorai, melyek segítségével bitképet hozhatunk létre Image-ből Stream-ból, fájlból, sőt akármilyen grafikából, illetve memóriában tárolt pixel-információkból.

Üres – adott méretű (width, height paraméterek) – bitképet készít a

    Bitmap(int width, int height[,, PixelFormat format]);

konstruktor. Amennyiben használjuk a PixelFormat típusú format paramétert, akkor a szímmélységet is szabályozhatjuk, mint azt láttuk az Image::GepPixelFormatSize() metódusnál . A

    Bitmap(Image^ original);
    Bitmap(Image^ original, Size newSize);
    Bitmap(Image^ original, int width, int height);

konstruktorok a paraméterként megadott Image-ből készítenek bitképet. Ha megadjuk a newSize paramétert, vagy a width és height paramétereket, akkor a bitkép az Image képének az új méretekre való átskálázásával jön létre. A

    Bitmap(Stream^ stream[, bool useIcm]);

túlterhelt konstruktorral stream-ből készíthetünk bitképet, a

    Bitmap(String^ filename[, bool useIcm]);

túlterhelt konstruktorral fájlból. A useIcm logikai paraméterrel azt szabályozhatjuk, hogy használjunk-e színkorrekciót vagy sem. Bármilyen Graphics példányról, pl. a képernyőről készíthetünk bitképet a

    Bitmap(int width, int height, Graphics^ g);

konstruktorral. A width és height egészek a méreteket definiálják, a g paraméter a rajzlappéldány, ennek DpiX és DpiY tulajdonsága, ami a bitmap felbontását adja. A scan0 egészre mutató pointer által kijelölt memóriából is gyárthatunk bitképet, ha megadjuk a width szélességét, a height magasságát és a stride értéket, vagyis két bitképsor közti különbséget bájtban

    Bitmap(int width, int height, int stride, 
        PixelFormat format, IntPtr scan0);

Egy újonnan létrahozott bitkép felbontását beállíthatjuk a

    void SetResolution(float xDpi, float yDpi);

metódussal. A paramétereket dot per inch mértékegységben kell megadni.

Néhány, nem az Image osztálytól örökölt metódus egészíti ki a bitképek lehetőségeit. Közvetlen módon hozzáférünk a bitkép egy pontjához. A megadott képpont (x, y) színét adja vissza a

    Color GetPixel(int x, int y);

metódus. Átszínezhetjük a bitkép pontjait a

    void SetPixel(int x, int y, Color color);

metódussal a color paraméter segítségével.

A

    void MakeTransparent([Color transparentColor]);

függvény átlátszóvá teszi a bitképen beállított átlátszó színt. Ha a transparenColor paramétert is megadjuk, akkor az válik átlátszóvá.

Az alábbi példa beolvas egy bitképet, és kirajzolja azt, majd a fehér színhez közeli színű pontjait zöldre festi, és az így módosított bitképet kirajzolja. A következő lépésben a zöld színt átlátszóvá teszi és félig eltolva újra kirajzolja a bitképet (IV.63. ábra).

private: System::Void Form1_Click(System::Object^  sender, 
        System::EventArgs^  e) {
    Bitmap ^ bm = gcnew Bitmap("C:\\D\\X.png");
    Graphics ^ g = this->CreateGraphics();
    g->DrawImage( bm, Point(0,0) );
    Color c;
for (int i=0; i<bm->Width; i++) {
for (int j=0; j<bm->Height; j++) { 
        Color c=bm->GetPixel(i,j);
 if ((c.R>200)&&(c.G>200)&&(c.B>200))
        bm->SetPixel(i,j,Color::Green);
}
}
    g->DrawImage( bm, Point(bm->Width,bm->Height) );
}

Bitkép színezés
IV.63. ábra - Bitkép színezés


A GetPixel() és SetPixel () metódusokkal való bitkép kezelés nem elég hatékony. Lehetőségünk van arra, hogy a bitkép bitjeit memóriába töltsük, és azokat módosítsuk, majd visszapumpáljuk a biteket a bitképbe. Ilyenkor gondoskodnunk kell arról, hogy az operációs rendszer ne zavarja műveleteinket. A bitképek memóriában való rögzítésére használhatjuk a

    BitmapData^ LockBits(Rectangle rect, ImageLockMode flags, 
        PixelFormat format[, BitmapData^ bitmapData]);
    void UnlockBits(BitmapData^ bitmapdata);

függvényt. A rect paraméter a bitkép rögzíteni kívánt részét jelöli ki, a felsorolás típusú ImageLockMode flags (ReadOnly, WriteOnly, ReadWrite, UserInputBuffer) az adatok kezelésének módját. A PixelFormat felsorolással már többször találkoztunk, a bitmapdata elhagyható paraméter BitmapData osztály típusú, melynek tulajdonságai a Height és Width a bitkép méretei, a Stride a scanline szélessége, a scan0 az első pixel címe. A visszatérési érték is BitmapData referencia osztály példányában tartalmazza a bitkép adatait.

A függvény hívása után a bitmapdata rögzítve van, így azt kezelhetjük a Marshal osztály metódusaival. A System::Runtime::InteropServices ::Marshal osztály metódusaival nem menedzselt memóriát foglalhatunk, másolhatjuk a nem menedzselt memóriadarabokat, illetve menedzselt memóriaobjektumokat másolhatunk nem menedzselt memóriaterületre. Például, a Marshal osztály

    static void Copy(IntPtr source, 
            array<unsigned char>^ destination, 
            int startIndex, int length);

metódusa a source pointer által jelölt területről a destination menedzselt tömbbe másol a kezdő indextől length darab bájtot

    static void Copy(array<unsigned char>^ source, 
            int startIndex, 
            IntPtr destination, int length);

metódus visszafelé, a source menedzselt tömbből a startindex indextől másol a destination nem menedzselt területre length db byte-ot.

Ha kimásoltuk a bitkébből a változtatni kívánt részleteket, majd megváltoztattuk és visszamásoltuk, akkor a memóriarögzítés feloldását az

v    oid UnlockBits(BitmapData^ bitmapdata);

metódussal végezhetjük el.

Az alábbi példa a formon kattintásra egy memóriába olvasott menedzselt bitképet kirajzol, majd rögzít, kiemeli kép bájtjait, a kép középső harmadát törli, visszaolvassa az adatokat a bitképbe, és elengedi a rögzítést és kirajzolja.

private: System::Void Form1_Click(System::Object^  sender, 
        System::EventArgs^  e) {
    Bitmap ^ bm = gcnew Bitmap("C:\\D\\X.png");
    Graphics ^ g = this->CreateGraphics();
    g->DrawImage( bm, Point(0,0) );
    Rectangle rect = Rectangle(0,0,bm->Width,bm->Height);
    System::Drawing::Imaging::BitmapData^ bmpData = bm-    >LockBits( 
        rect,     System::Drawing::Imaging::ImageLockMode::ReadWrite,
        bm->PixelFormat );
    IntPtr ptr = bmpData->Scan0;
int bytes = Math::Abs(bmpData->Stride) * bm->Height;
array<Byte>^Values = gcnew array<Byte>(bytes);
    System::Runtime::InteropServices::Marshal::Copy( ptr, Values, 
    0, bytes );
for ( int counter = Values->Length/3; 
        counter < 2*Values->Length/3; counter ++ )    {
        Values[ counter ] = 255;
}
    System::Runtime::InteropServices::Marshal::Copy( Values, 0, 
        ptr, bytes );
    bm->UnlockBits( bmpData );
    g->DrawImage( bm, bm->Width, 0 );
}

Nem menedzselt bitkép kezelés
IV.64. ábra - Nem menedzselt bitkép kezelés


Metafájlokkal is dolgozhatunk, hiszen használhatjuk az Image osztály metódusait. Az alábbi példa beolvas egy Png állományt egy bitképbe és azt Wmf metafileként tárolja:

private: System::Void Form1_Click(System::Object^  sender, 
        System::EventArgs^  e) {
    Bitmap ^ bm = gcnew Bitmap("C:\\D\\X.png");
    Graphics ^ g = this->CreateGraphics();
    g->DrawImage( bm, bm->Width, 0 );
    bm->Save("C:\\D\\X.wmf");
}

IV.4.10. Ecsetek

Területek kifestésére ecseteket használhatunk a GDI+-ban [4.1.] [4.2.] . Az összes ecsettípus a System::Drawing::Brush osztály leszármazottja (IV.30. ábra). A legegyszerűbb ecset az egyszínű (SolidBrush). Ennek konstruktora egy megadott színből hoz létre ecsetet. Ennek megfelelően az osztály Color tulajdonsága felveszi az ecset színét.

    SolidBrush(Color color);

Az alábbi példa egy piros téglalapot (a Graphics osztály FillRectangle() metódusával) fest ki a formon (IV.65. ábra).

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
    SolidBrush ^ sb = gcnew SolidBrush(Color::Red); 
    e->Graphics->FillRectangle(sb,200,20,150,50);
}

Az mintázott ecset HatchBrush szintén a Brush osztály leszármazottja és a System::Drawing::Drawing2D névtérben definiált, ezért szükséges azt beépíteni.

    using namespace System::Drawing::Drawing2D; 

A mintázott ecset konstruktora definiálja az ecset mintázatát, a mintázat színét és a háttérszínt. Ha az utolsó paramétert elhagyjuk (a szögletes zárójel jelzi az elhagyható paramétert), akkor a háttérszín fekete lesz.

HatchBrush(HatchStyle hatchstyle, Color foreColor, 
                [Color backColor])

A Drawing2D névtér HatchStyle felsorolása rengeteg előre definiált mintázatot tartalmaz. HatchStyle::Horizontal a vízszintes, Vertical a függőleges, ForwardDiagonal átlós (balra dől) stb. Az alábbi példa piros sraffozott ecsettel fest ki sárga téglalapot (IV.65. ábra).

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
    HatchBrush ^ hb = gcnew HatchBrush(HatchStyle::ForwardDiagonal,
            Color::Red,Color::Yellow);
    e->Graphics->FillRectangle(hb,200,80,150,50);
}

A mintázott ecsetek jellemző tulajdonságai a BackGroundColor a hátérszín, ForegroundColor az előtér szín és a HatchStyle a mintázat.

A TextureBrush osztály példányaival képekkel festő ecseteket hozhatunk létre. A konstruktorok

    TextureBrush(Image^ image);
    TextureBrush(Image^ image, WrapMode wrapMode);
    TextureBrush(Image^ image, Rectangle dstRect);
    TextureBrush(Image^ image, RectangleF dstRect);
    TextureBrush(Image^ image, WrapMode wrapMode, 
                Rectangle dstRect);
    TextureBrush(Image^ image, WrapMode wrapMode, 
                RectangleF dstRect);
    TextureBrush(Image^ image, Rectangle dstRect, 
                ImageAttributes^ imageAttr);
    TextureBrush(Image^ image, RectangleF dstRect, 
                ImageAttributes^ imageAttr);

Az image a kifestéshez használt képet, a dstRect a kép kifestéskori torzítását adja meg, a wrapMode a képeknél megismert módon a kifestő képek elrendezésére utal (WrapMode felsorolás elemei Tile – mintha téglákból lenne kirakva, TileFlipX – y tengelyre tükrözött téglákból kirakva, TileFlipY - x tengelyre tükrözött téglákból kirakva, TileFlipXY középpontosan tükrözött téglákból kirakva, Clamped nem téglákból kirakott a textúra). Az imageAttr paraméter a kép manipulációjáról intézkedik (színek, színkorrekciók, kifestési módok stb.).

A TextureBrush osztály Image^ típusú Image tulajdonsága a kifestő grafikát adja meg a Transform ^ típusú Transform tulajdonsága és annak metódusai a transzformációknál megismert módon használható. A WrapMode tulajdonság az elrendezést tartalmazza.

Az alábbi példa képekkel fest ki egy téglalapot (IV.65. ábra).

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
    Image ^ im = Image::FromFile("c:\\M\\mogi.png");
    TextureBrush ^ tb = gcnew TextureBrush(im);
    Matrix ^ m= gcnew Matrix();
    m->Translate(5,5);
    tb->Transform->Reset();
    tb->Transform=m;
    Graphics ^ g = this->CreateGraphics();
    g->FillRectangle(tb, 200, 140, 150, 50);
}

A Drawing2D névtér tartalmazza a LinearGradientBrush osztályt, melynek konstruktoraival színjátszó ecseteket készíthetünk. A különböző konstruktorokkal más és más színátmeneteket állíthatunk be. System::Windows::Media::BrushMappingMode felsorolás a színjátszáshoz határoz meg koordináta-rendszert. E felsorolás értékei Absolute a pontok az aktuális koordináta-rendszerben értelmezettek. A RelativeToBoudingBox beállítás esetén az adott kifestés befoglaló téglalapjának bal felső sarka=(0,0), jobb alsó sarka (1,1).

A

    LinearGradientBrush(Rectangle rect, 
            Color startColor, Color endColor,
            double angle, [isAgleScaleable]);
    LinearGradientBrush(RectangleF rect, 
            Color startColor, Color endColor,
            double angle, [isAgleScaleable]);
    LinearGradientBrush(Point startPoint, Point endPoint, 
            Color startColor, Color endColor);
    LinearGradientBrush(PointF startPoint, PointF endPoint, 
            Color startColor, Color endColor);
    LinearGradientBrush(Rectangle rect, 
            Color startColor, Color endColor
            LinearGradientMode lgm);
    LinearGradientBrush(RectangleF rect, 
            Color startColor, Color endColor,
            LinearGradientMode lgm);

konstruktorok lineáris gradiens ecsetet hoznak létre, a kezdő szín (startColor)és a végső szín (endColor) felhasználásával. A RelativeToBoudingBox ecsetkoordináta-rendszerben a gradiens irányszögét az angle paraméterrel adjuk meg fokban (0 – vízszintes gradiens, 90 – függőleges) vagy startPoint és az endPoint pontokkal, illetve a rect sarkaival, a % mértékegységben. A LinearGradientMode felsorolás elemei Horizontal, Vertical, ForwardDiagonal, BackwardDiagonal a színváltozás irányt adják meg.

A

    PathGradientBrush(GraphicsPath path);
    PathGradientBrush(Point[] points);
    PathGradientBrush(PointF[] points);
    PathGradientBrush(Point[] points, WrapMode wrapMode);

konstruktorokkal létrehozhatunk egy vonal mentén változó színű ecsetet. A vonalat a path, illetve points paraméterekkel definiálhatjuk. A wrapMode WrapMode típusú paraméterrel már találkoztunk a textúra ecseteknél.

Mindkét gradiens ecset rendelkezik a kifestés ismétlésének módját definiáló WrapMode tulajdonsággal, a lokális transzformációt tartalmazó Transform tulajdonsággal. Mindkét gradiens ecset tartalmaz Blend osztály típusú Blend tulajdonságot. A Blend osztály Factors és Positions tulajdonságai float tömbök 0 és 1 közötti értékkel, melyek megadják a százalékos színintenzitást és a százalékos hosszpozíciót adják meg. Mindkét gradiens ecsetnek van InterpolationColors (ColorBlend osztály típusú) tulajdonsága is, ahol a százalékos pozíciók mellett az intenzítás helyett a színeket adhatjuk meg. Mindkét gradiens ecsetnek van határoló Rectangle tulajdonsága.

A PathGradientBrush SurroundColors színtömb tulajdonsága tartalmazza a felhasznált színeket.

Lehet az ecset áttetsző is az ARGB színhasználattal. Az alábbi példa a különbözően kifestett téglalapok fölé áttetsző téglalapot rajzol.

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
    SolidBrush ^ sb = gcnew SolidBrush(Color::Red); 
    e->Graphics->FillRectangle(sb,200,20,150,50);
    HatchBrush ^ hb = gcnew HatchBrush(
    HatchStyle::ForwardDiagonal,
            Color::Red,Color::Yellow);
    e->Graphics->FillRectangle(hb,200,80,150,50);
    Image ^ im = Image::FromFile("c:\\M\\mogi.png");
    TextureBrush ^ tb = gcnew TextureBrush(im);
    Matrix ^ m= gcnew Matrix();
        m->Translate(5,5);
        tb->Transform->Reset();
        tb->Transform=m;
    e->Graphics->FillRectangle(tb, 200, 140, 150, 50);
    LinearGradientBrush ^ lgb = gcnew LinearGradientBrush(
        PointF(0,0), PointF(100,100), Color::Blue, Color::Red); 
    e->Graphics->FillRectangle(lgb, 200, 200, 150, 50);
    SolidBrush ^ at = gcnew SolidBrush(
        Color::FromArgb(127,Color::Red)); 
    e->Graphics->FillRectangle(at, 250, 10, 200, 270);
}

Ecsetek
IV.65. ábra - Ecsetek


IV.4.11. Tollak

Vonalas ábrákat tollal rajzolhatunk. A tollaknak van színük, és vastagságuk.

    Pen(Brush brush [, float width]);
    Pen(Color color [, float width]);

A Pen osztály konstruktoraival color színnel vagy brush ecsettel definiált tollat hozhatunk létre. A width paraméterrel a toll vastagságát is definiálhatjuk.

A Brush a Color és a Width tulajdonságok tárolhatják a toll ecsetét, színét és vastagságát.

A DashStyle tulajdonság a DashStyle felsorolás ( System::Drawing::Drawing2D névtér) egy értékét veheti fel, beállítva ezzel a toll mintázatát (Solid – folytonos, Dash – szaggatott, Dot – pontokból rajzolt vonal, DashDot – pontvonal, DashDotDot pontvolnal dupla ponttal, illetve Custom). Az utolsó vonalstílus esetén magunk definiálhatjuk a mintázatot a DashPattern float tömb egymást követő számai a vonal- és a szünet hosszakat definiálják.

A float típusú DashOffset tulajdonsággal a vonal kezdőpontja és a szaggatás kezdete közti távolságot adhatjuk meg. Szaggatott vonalak esetén a DashCap ( System::Drawing::Drawing2D ) azt is megadhatjuk milyen végei legyenek a vonalaknak. A DashCap felsorolás értékei Flat – sima, Round lekerekített, Triangle – háromszögű.

Az Alignment tulajdonság a PenAligment típusú felsorolás egyik értéke lehet ( System::Drawing::Drawing2D névtér), és arról intézkedik az alakzat éléhez képest hova kerül a vonal (Center – felező, Inset – zárt alakzat belseje, OutSet – zárt alakzaton kívül, Left – a vonal „bal partjára”, Right – a vonal „jobb partjára”).

Figurák vonalainak csatlakozási geometriájára használhatjuk a LineJoin felsorolás típusú LineJoin tulajdonságot (Bevel – szögben letört sarok, Round – lekerekített, a Miter és a MiterClipped szintén letört csatlakozás, ha a letörés hossza nagyobb, mint az float típusú MiterLimit határérték).

A vonalvégek rajzolatának szabályozására a StartCap és az EndCap System::Drawing::Drawing2D::LineCap felsorolás típusú tulajdonság szolgál (Flat – sima, Square - négyzet, Round – lekerekített – ArrowAnchor – nyíl vég, RoundAnchor – körlap-vég stb.). Érdekes lehet a CustomStartCap és a CustomEndCap tulajdonság, melyek a CustomLineCap osztály példányai. A példány a

    CustomLineCap(GraphicsPath fillPath, GraphicsPath strokePath 
            [, LineCap baseCap[, float baseInset]]);

konstruktorral hozható létre. Figurával definiálhatjuk a kitöltést (fillPath) és a körvonalat (strokePath). Kiindulhatunk egy meglévő vonalvégből (baseCap), illetve beállíthatjuk a rést a vonal és a vége alakzat között (baseInset). A CustomLineCap BaseCap tulajdonsága az ős vonalvéget azonosítja a BaseInset a rést. Definiálhatjuk a vonalak csatlakozási módját (StrokeJoin), illetve beállíthatunk egy vastagsági skálázást (WidthScale tulajdonság). Alaphelyzetben ez a vonal vastagsága.

A Pen osztály PenType csak olvasható PenType felsorolás típusú tulajdonságából megtudhatjuk, milyen típusú a toll (SolidColor, HatchFill, TextureFill, LinearGradient, PathGradient)

Természetesen a tollak is rendelkeznek Transform – lokális transzformációt definiáló – tulajdonságuk.

Az alábbi példa piros, folytonos vonallal, majd lineáris gradiens-ecsettel definiált tollal húz vonalat. Ezt követően vonalat húz az egyik oldalon nyíllal, a másik oldalon kerek véggel. Ezek után két szaggatott vonal következik, az egyik előre definiált mintával, a másik mintatömbbel. Ezt két téglalap figura követi az egyik letört és az alapértelmezett OutSet Alignment tulajdonsággal, a másik InSet tulajdonsággal, letörés nélkül (IV.66. ábra).

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
    Pen ^ ps = gcnew Pen(Color::Red,5);
 
    e->Graphics->DrawLine(ps,10,25,100,25); 
    LinearGradientBrush ^ gb = gcnew LinearGradientBrush(
                PointF(10, 10), PointF(110, 10),
    Color::Red, Color::Blue);
    Pen ^ pg = gcnew Pen(gb,5);
    e->Graphics->DrawLine(pg,10,50,100,50); 
 
    ps->StartCap=LineCap::ArrowAnchor;
    pg->EndCap=LineCap::Round;
    e->Graphics->DrawLine(pg,10,75,100,75); 
 
    pg->DashStyle=DashStyle::DashDot;
    e->Graphics->DrawLine(pg,10,100,100,100); 
 
array <float> ^ dp= {1.0f,0.1f,2.0f,0.2f,3.0f,0.3f};  
    pg->DashPattern=dp; 
    e->Graphics->DrawLine(pg, 10, 125, 100, 125);
    GraphicsPath^ mp = gcnew GraphicsPath;
    RectangleF srcRect = RectangleF(10,150,100,20);
    mp->AddRectangle( srcRect );
    ps->LineJoin=LineJoin::Bevel;
    e->Graphics->DrawPath( ps , mp );
 
    Matrix ^ em = gcnew Matrix();
    em->Reset();
    em->Translate(0,25);
    mp->Transform(em);
    pg->Alignment=PenAlignment::Inset;
    e->Graphics->DrawPath( pg , mp );
}

Tollak
IV.66. ábra - Tollak


IV.4.12. Font, FontFamily

A Windows-ban a karakterkészletek három alapvető típusát különböztethetjük meg. Az egyik esetben a karakter megjelenítéséhez szükséges információt bitkép tárolja. A második esetben a vektoros tárolású fontok azt rögzítik, hogy milyen vonalakat kell meghúzni egy karakter megrajzolásához. A harmadik eset az ún. TrueType karakterkészlet esete, amikor a karakterek definíciója pontok és speciális algoritmusok halmaza, amelyek mindenféle felbontáshoz képesek definiálni a karaktereket. Amikor TrueType karakterkészletet használunk, akkor az ún. TrueType raszterizáló a pontokból és az algoritmusokból elkészíti az adott igényeknek megfelelő bittérképes karakterkészletet. Ennek eredménye, ha egy kiadványszerkesztőben megtervezzük a kinyomtatandó lapot, akkor az nyomtatáskor ugyanolyan lesz, mint azt a képernyőn láttuk (WYSIWYG - What You See Is What You Get). A TrueType fontok használatának jogát a Microsoft az Apple Computer-től [4.] [8] [.] vette meg. Nyilvánvaló az első két karakterábrázolásnak van előnye és hátránya. A vektoros fontok tetszőleges mértékben kicsinyíthetők, nagyíthatók, azonban megjelenítésük sok esetben nem elég esztétikus és nem olvasható. A bitképes fontok általában jól olvashatók, azonban nem deformálhatók szabadon. A TrueType típusú karakterkészlet használata biztosítja a fenti előnyöket, és egyúttal kiküszöböli a hátrányokat.

Ismerkedjünk meg néhány fogalommal, melyek segítenek eligazodni a fontok világában! Alapvető kérdés a karakterek megjelenítésekor a betű vonalának módosíthatósága. Lehet-e vékonyabb, illetve vastagabb betűrajzolatot használni? A betű vonalát az angol irodalom "stroke" névvel illeti.

Több karakterkészlet létezik, amelyben a betű megrajzolásakor használt vonalakat kis keresztvonalakkal zárják. A betűknek kis talpa, illetve kalapja lesz. Ezt a kis keresztvonalat az angol "serif"-nek hívja. Így az a karakterkészlet, melyben nincsenek ilyen kis záró "serif"-ek a "sanserif" azaz serif néküli.

Karaktercellának hívjuk azt a téglalap alakú területet, mely a karaktert befoglalja. Ennek méretét jellemezhetjük az emSize paraméterrel, amely a karakter méretét pontokban (1/72 inch) tárolja. Az em előtag a M betű kiejtéséből adódik, mert ezt használják a betűtípus méretezésére. A karakterek nem teljesen töltik ki a cellát. A cellára jellemző a szélessége, magassága és a kezdőpontja, amely általában a bal felső sarok. A karaktercellát egy vízszintes vonal osztja két részre. Erre a vonalra - a bázis vonalra - vannak ráültetve a karakterek. A karaktercella bázisvonal feletti részét "ascent" névvel, a bázisvonal alatti részét "descent" névvel illeti az angol. Két egymás alá helyezett karaktercella bázisvonalainak távolsága a "leading". A bázisvonalra írt nagybetűk sem érik el a karaktercella tetejét.

A karaktercella felső széle és a nagybetűk felső széle közti rész a belső "leading". Ha a két egymás alá helyezett karaktercella felső és alsó éle nem érintkezik, akkor a kettő közti távolság a külső leading.

Karakterjellemzők
IV.67. ábra - Karakterjellemzők


Egy karaktertípus adott méretét nevezik betűtípusnak (typeface). A Windows az adott méretben torzítással képes betűtípusokat létrehozni. A dőlt betűket (italic) egyszerűen úgy hozza létre, hogy a karakter pontjai a bázisvonalon változatlanok maradnak, míg a karakter többi része a bázisvonaltól való távolság függvényében elcsúszik (a karakter megdől). A kövér betűk (bold) egyszerűen a stroke megvastagításából származtathatók. Nyilvánvaló, hogy egyszerűen létrehozhatók aláhúzott (underlined) és áthúzott (strikeout) karakterek, a megfelelő helyre húzott vízszintes vonallal. Használatos még a "pitch" fogalma, amely azt specifikálja, hogy egy adott fontból hány karaktert kell egymás mellé írni, hogy a kiírt szöveg szélessége egy inch (~2.54 cm) legyen. Érdemes megjegyezni, hogy a képernyőn való megjelenítéshez a logikai inch-et használjuk. A logikai inch nagysága úgy van meghatározva, hogy a képernyőn is ugyanolyan jól olvasható legyen a szöveg, mint a papíron. Két alapvető font típus létezik. Az egyik a nem proporcionális - fix méretű -, amelyben minden karakter azonos szélességű cellában van elhelyezve, legyen az akár "i" akár "m". Újabban az ilyen betűtípusokat monospace fontoknak hívják. A másik a proporcionális (azaz arányos) - ebben az esetben a cella szélessége függ az adott betűtől. Az utóbbi esetben csak az átlagos karakterszélességek, az átlagos és a maximális pitch méretek használhatók.

Hagyományos karakter szélességek
IV.68. ábra - Hagyományos karakter szélességek


Érdemes foglalkozni a karakterek oldalarányaival is, hiszen nem minden font lenne megjeleníthető mindenféle output eszközön (pl. mátrixnyomtató). A Windows képes oldalarány-konverziót végrehajtani, a megjelenítés érdekében. A Windows karakterkészlet családokat használ attól függően, hogy milyen a karakterek megjelenése.

A hagyományos karakterkészletek a karakterek méreteinek jellemzésére az ábrán (IV.68. ábra) megjelenő adatokat használják. A TrueType karakterkészletek a fenti hagyományos értelmezés helyett az ún. ABC adatokat használják. Mint ahogy az az ábrán (IV.69. ábra) látszik, az A és a C értékek lehetnek negatívok is.

ABC karakterszélességek
IV.69. ábra - ABC karakterszélességek


A fontcsaládok hasonló jellegzetességgel bíró betűtípusokat tartalmaznak. A fontcsaládok modellezésére a .Net a nem örököltethető FontFamily osztályt definiálja. A FontFamily példányt a

    FontFamily(GenericFontFamilies genericFamily);
    FontFamily(String^ name[, FontCollection^ fontCollection]);

konstruktorokkal hozhatunk létre. A genericFamily paraméter értékét a GenericFontFamilies felsorolás elemei közül választhatjuk ki a család létrehozásához. A felsorolás lehetséges értékei (Serif, SansSerif és Monospace) már ismert fogalmakat takarnak. A name paraméter az új fontcsalád nevét tartalmazza. Lehet üres sztring, vagy a gépre nem telepített fontcsalád neve, vagy egy nem TrueType font neve. A fontcsaládokból gyűjteményeket is készíthetünk. A fontCollection paraméterrel megadhatjuk a gyűjteményt, amibe behelyezhetjük az új fontcsaládot.

A FontFamily osztály tulajdonságaival tájékozódhatunk lehetőségeinkről. A statikus Families tulajdonsággal lekérdezhetjük az aktuális rajzlap fontcsaládjait egy FontFamily típusú tömbbe. A FontFamily típusú statikus GenericMonospace, GenericSansSerif és GenericSerif tulajdonsgok egy nem proporcionális, egy sansserif, illetve egy serif fontcsaláddal térnek vissza. A Name string tulajdonság a fontcsalád nevét tartalmazza.

A statikus

    static FontFamily[] GetFamilies(Graphics graphics);

metódus visszatér az adott rajzlapon található összes fontcsalád nevével.

Az alábbi példa a formon való kattintásra egy listaablakba olvassa a rajzlap összes fontcsaládjának nevét.

private: System::Void Form1_Click(System::Object^  sender, 
        System::EventArgs^  e) {
    Graphics ^ g = this->CreateGraphics();
array <FontFamily ^> ^ f = gcnew array<FontFamily^>(100);
    f=FontFamily::GetFamilies(g);
for (int i=0; i<f->GetLength(0); i++) {
    listBox1->Items->Add(f[i]->Name);
    }
}

Fontcsaládok
IV.70. ábra - Fontcsaládok


A

    int GetCellAscent(FontStyle style);
    int GetCellDescent(FontStyle style);
    int GetEmHeight(FontStyle style);
    int GetLineSpacing(FontStyle style);
    String^ GetName(int language);

metódusok az adott fontcsalád style paraméterrel megadott adatait kérdezik le. A

    bool IsStyleAvailable(FontStyle style);

metódus azt vizsgálja, hogy az adott fontcsaládnak van-e style stílusa.

FontStyle felsorolás értékei: Bold, Italic, Underline és Strikeout.

A GDI+ a betűtípusok modellezésére a Font osztályt használja. Az osztály példányait a

    Font(FontFamily^ family, float emSize[, FontStyle style
        [, GraphicsUnit unit [, unsigned char gdiCharSet
        [, bool gdiVerticalFont]]]]);
    Font(FontFamily^ family, float emSize, GraphicsUnit unit);
    Font(String^ familyName, float emSize[, FontStyle style
        [, GraphicsUnit unit[, unsigned char gdiCharSet
        [, bool gdiVerticalFont]]]]);
    Font(String^ familyName, float emSize, GraphicsUnit unit);
    Font(Font^ prototype, FontStyle newStyle);

konstruktorokkal hozhatjuk létre. A family paraméter az új font családját, a familyName a család nevét adja meg. Az emSize a font mérete pontban, a style a betűstílusa (dőlt, kövér stb.). A unit paraméter a font által használt egységeket (Pixel, Inch, Millimeter stb.) adja meg. A gdiCharset a GDI+ által biztosított karakterkészleteket azonosítja. A gdiVerticalFont a függőleges írást szabályozza. Új fontot is definiálhatunk a prototype prototípussal, és az új newStyle stílussal.

A Font osztály tulajdonságai csak olvashatók. Ha valamit meg szeretnénk változtatni, akkor új fontot kell létrehoznunk. FontFamily^ típusú FontFamily tulajdonság a fontcsaládot azonosítja. A Name sztring a font nevét, az OriginalFontName sztring pedig a font eredeti nevét adja. Ha az IsSystemFont logikai tulajdonság igaz, rendszerfonttal van dolgunk – ilyenkor a SystemFontName sztring tulajdonság a rendszerfont nevét adja. A GdiVerticalFont logikai tulajdonság megadja, hogy a font függőleges-e. A GdiCharSet bájt típusú tulajdonság a GDI karakterkészlet azonosítója. A Size (emSize) és SizeInPonts a fontméretet szolgáltatják pontban kifejezve. A Style tulajdonság a FontStyle felsorolás elemeivel (Italic, Bold, Underline és Strikeout) logikai tulajdonságként azonosítják a font stílusát. A Unit megadja a GraphicsUnit mértékegységét.

A

    float GetHeight();

metódus a sorok közt mérhető távolságot adja pixelben. A

    float GetHeight(Graphics^ graphics);

metódus a sorok közt mérhető távolságot adja GraphicsUnit-ban. A

    float GetHeight(float dpi);

metódus a sorok közt mérhető font távolságot adja pixelben az adott dpi (dot per inch – pont per inch) felbontású eszközön.

Az alábbi példa a Tipus gomb megnyomására választott font torzítását szemlélteti a Bold, Italic és Underline opciógombokkal (IV.71. ábra).

Font torzítások
IV.71. ábra - Font torzítások


IV.4.13. Rajzrutinok

A Graphics osztály egy sor rajzrutint kínál. Letörölhetjük a teljes rajzlapot az adott színnel (color)

    void Clear(Color color);

A következő rajzrutinokban a pen a rajzolás tollát azonosítja. Egyenes vonalszakaszokat húzhatunk a

    void DrawLine(Pen^ pen, Point pt1, Point pt2);
    void DrawLine(Pen^ pen, PointF pt1, PointF pt2);
    void DrawLine(Pen^ pen, int x1, int y1, int x2, int y2);
    void DrawLine(Pen^ pen, float x1, float y1, float x2, float y2);

metódusokkal. A pt1, pt2, (x1,y1) és (x2,y2) a végpontok .

Törtvonalakat rajzolnak a

    void DrawLines(Pen^ pen, array<Point>^ points);
    void DrawLines(Pen^ pen, array<PointF>^ points);

metódusok. A points a sarokpontokat határozza meg.

Téglalapokat rajzolhatunk a

    void DrawRectangle(Pen^ pen, Rectangle rect);
    void DrawRectangle(Pen^ pen, int x, int y, 
                int width, int height);
    void DrawRectangle(Pen^ pen, float x, float y, 
                float width, float height);

metódusok segítségével. A téglalap adatait a rect struktúra, vagy az (x,y) bal felső sarok, width szélesség és height magasság határozzák meg.

Egy sor, a rects tömbbel meghatározott téglalapot rajzolhatunk egyszerre a

    void DrawRectangles(Pen^ pen, array<Rectangle>^ rects);
    void DrawRectangles(Pen^ pen, array<RectangleF>^ rects);

metódussal.

A points struktúratömbbel meghatározott sokszöget rajzolnak az alábbi metódusok:

    void DrawPolygon(Pen^ pen, array<Point>^ points);
    void DrawPolygon(Pen^ pen, array<PointF>^ points);

A rect struktúra, vagy az (x,y) és (height, width) adatok által meghatározott befoglaló téglalappal rendelkező ellipszist rajzolnak a

    void DrawEllipse(Pen^ pen, Rectangle rect);
    void DrawEllipse(Pen^ pen, RectangleF rect);
    void DrawEllipse(Pen^ pen, int x, int y, int width, int height);
    void DrawEllipse(Pen^ pen, float x, float y, 
            float width, float height);

metódusok.

Az (IV.48. ábra) ábrának megfelelő ellipszisívet rajzolhatunk a

    void DrawArc(Pen^ pen, Rectangle rect, 
            float startAngle, float sweepAngle);
    void DrawArc(Pen^ pen, RectangleF rect, 
            float startAngle, float sweepAngle);
    void DrawArc(Pen^ pen, int x, int y, int width, int height, 
            int startAngle,int sweepAngle);
    void DrawArc(Pen^ pen, float x, float y, 
            float width, float height, 
            float startAngle, float sweepAngle);

metódusokkal. A

    void DrawPie(Pen^ pen, Rectangle rect, 
            float startAngle, float sweepAngle);
    void DrawPie(Pen^ pen, RectangleF rect, 
            float startAngle, float sweepAngle);
    void DrawPie(Pen^ pen, int x, int y, int width, int height, 
            int startAngle,int sweepAngle);
    void DrawPie(Pen^ pen, float x, float y, 
            float width, float height, 
            float startAngle, float sweepAngle);

metódusok tortaszeletet rajzolnak az ívből (egyenesek a középponthoz).

A7 (IV.49. ábra) ábrának megfelelő Bezier-görbét rajzolnak a

    void DrawBezier(Pen^ pen, Point pt1, Point pt2, 
            Point pt3, Point pt4);
void DrawBezier(Pen^ pen, PointF pt1, PointF pt2, 
            PointF pt3, PointF pt4);
void DrawBezier(Pen^ pen, float x1, float y1, 
            float x2, float y2, 
            float x3, float y3, 
            float x4, float y4);

metódusok. A tartópontokat a pt1, pt2, pt3, pt4 struktúrákkal, illetve az x i , y i koordinátákkal definiálhatjuk.

Megadott Bezier görbeláncot rajzolnak a

    void DrawBeziers(Pen^ pen, array<Point>^ points);
    void DrawBeziers(Pen^ pen, array<PointF>^ points);

metódusok. Az első 4 pont után minden következő három definiál újabb görbeszakaszt.

A

    void DrawCurve(Pen^ pen, array<Point>^ points
                [, float tension]);
    void DrawCurve(Pen^ pen, array<PointF>^ points
                [, float tension]);
    void DrawCurve(Pen^ pen, array<PointF>^ points, 
            int offset, int numberOfSegments
                [, float tension]);

metódusok kardinális spline-t (IV.52. ábra) rajzolnak a megadott tollal (pen) a ponts tömb pontjain keresztül. Megadhatjuk a feszítést (tension) és a figyelembevett görbeszakaszok számát (numberOfSegments).

Zárt spline-okat is rajzolhatunk a

    void DrawClosedCurve(Pen^ pen, array<Point>^ points[, 
                FillMode fillmode]);
void DrawClosedCurve(Pen^ pen, array<PointF>^ points[, 
                FillMode fillmode]);

Metódusokkal a points tömb pontjain keresztül a fillmode – a már ismert - FillMode felsorolás típus Alternate és Winding értékekkel (IV.47. ábra).

Figurát (path) rajzol a

    void DrawPath(Pen^ pen, GraphicsPath^ path);

metódus a pen tollal.

A

    void DrawString(String^ s, Font^ font, Brush^ brush, 
            PointF point[, StringFormat^ format]);
    void DrawString(String^ s, Font^ font, Brush^ brush, 
            float x, float y[, StringFormat^ format]);
    void DrawString(String^ s, Font^ font, Brush^ brush, 
            RectangleF layoutRectangle[, 
            StringFormat^ format]);

metódusok kiírják a rajzlapra  a megadott s sztringet az adott font betűtípussal és brush ecsettel. A kiírás helyét megadhatjuk a point , x és y paraméterekkel, valamint a layoutRectangle befoglaló téglalappal. A StringFormat osztály példánya definiálja a megjelenítés formátumát (például a FormatFlags tulajdonság StringFormatFlags felsorolás DirectionRightToLeft, DirectionVertical, NoWrap stb. értékeivel, vagy az Alignment tulajdonságban az Alignment felsorolás Near, Center és Far elemeivel).

Az Icon ikont rajzolhatjuk ki a

    void DrawIcon(Icon^ icon, int x, int y);

a metódussal az x, y pontba a rajzlapra. Torzíthatjuk az ikont a targetRectangle téglalapra.

    void DrawIcon(Icon^ icon, Rectangle targetRect);

Az ikon tozítatlanul kerül ki a téglalapra a

    void DrawIconUnstretched(Icon^ icon, Rectangle targetRect);

a metódussal.

Az image által definiált képet, illetve egy választható részletét (srcRect a megadott srcUnit egységekben) jeleníthetjük meg a point, vagy az (x,y) pontba a

    void DrawImage(Image^ image, Point point);
    void DrawImage(Image^ image, PointF point);
    void DrawImage(Image^ image, int x, int y [, Rectangle srcRect, 
        GraphicsUnit srcUnit]);
    void DrawImage(Image^ image, float x, float y
        [, RectangleF srcRect, GraphicsUnit srcUnit]);

a metódusokkal.

A

    void DrawImage(Image^ image, Rectangle rect);
    void DrawImage(Image^ image, RectangleF rect);
    void DrawImage(Image^ image, int x, int y, 
            int width, int height);
    void DrawImage(Image^ image, float x, float y, 
            float width, float height);

metódusok a rect, illetve az (x, y, width, height) téglalapra torzítják az image képet.

A kép (image) egy téglalapját (srcRect) kirajzolhatjuk a rajz egy téglalapjára (destRect) a

    void DrawImage(Image^ image, Rectangle destRect, 
            Rectangle srcRect, GraphicsUnit srcUnit);
    void DrawImage(Image^ image, RectangleF destRect, 
            RectangleF srcRect, GraphicsUnit srcUnit);

metódusokkal. Meg kell adnunk a forrás téglán a grafikus egységeket (srcUnit).

A fentiekhez hasonló céllal, ha a forrástéglalapot int vagy float sarokpont adatokkal (srcX, srcY) és int vagy float szélesség és magasságadatokkal (srcWidth, srcHeight), adjuk meg, a forrás grafikus egységeit akkor is meg kell adnunk (srcUnit). Ekkor azonban használhatjuk a ImageAttributes osztály egy példányát az átszínezésre (imageAttr), sőt hibakezelő CallBack függvényt is definiálhatunk, és adatokat is küldhetünk a callback függvénynek (callbackData):

    void DrawImage(Image^ image, Rectangle destRect, 
        int srcX, int srcY, int srcWidth, int srcHeight, 
        GraphicsUnit srcUnit 
        [, ImageAttributes^ imageAttr
        [,Graphics::DrawImageAbort^ callback
        [,IntPtr callbackData]]]);
 
    void DrawImage(Image^ image, Rectangle destRect, 
        float srcX, float srcY, 
        float srcWidth, float srcHeight, 
        GraphicsUnit srcUnit[, 
        ImageAttributes^ imageAttrs
         [,Graphics::DrawImageAbort^ callback
         [,IntPtr callbackData]]]);

Paralelogrammára torzítottan is kitehetjük a képet a

    void DrawImage(Image^ image, array<Point>^ destPoints 
            [, Rectangle srcRect, GraphicsUnit srcUnit
            [, ImageAttributes^ imageAttr[,
            Graphics::DrawImageAbort^ callback
            [,int callbackData]]]]);

metódussal. Az image a kirajzolni kívánt kép, a destPoints tömb a kép kirajzolásakor a torzított kép paralelogrammájának három pontja. A további paraméterek megegyeznek a legutóbb említett függvényeknél megismertekkel. Az alábbi példa egy képet egyenesen és torzítva is kirajzol.

private: System::Void Form1_Paint(System::Object^  sender, 
        System::Windows::Forms::PaintEventArgs^  e) {
    Image^ mogi = Image::FromFile( "C:\\M\\Mogi.png" );
        int x = 100;
        int y = 100;
        int width = 250;
        int height = 150;
    e->Graphics->DrawImage( mogi, x, y, width, height );
    Point b_f_sarok = Point(100,100);
    Point j_f_sarok = Point(550,100);
    Point b_a_sarok = Point(150,250);
    array<Point>^ cel = {b_f_sarok,j_f_sarok,b_a_sarok};
    e->Graphics->DrawImage( mogi, cel);
}

Nagyított és nagyított torzított kép
IV.72. ábra - Nagyított és nagyított torzított kép


Kifestett alakzatokat is rajzolhatunk a Graphics osztály Fillxxx() metódusaival. A

    void FillRectangle(Brush^ brush, Rectangle rect);
    void FillRectangle(Brush^ brush, RectangleF rect);
    void FillRectangle(Brush^ brush, int x, int y, 
                int width, int height);
    void FillRectangle(Brush^ brush, float x, float y, 
                float width, float height);

metódusok rect struktúrával, vagy a x,y, height width adatokkal megadott téglalapot festenek a brush ecsettel.

A rects tömb téglalapjait festik ki a

    void FillRectangles(Brush^ brush, array<Rectangle>^ rects);
    void FillRectangles(Brush^ brush, array<RectangleF>^ rects);

metódusok.

A points paraméter által megadott zárt poligonokat festik ki a brush ecsettel a

    void FillPolygon(Brush^ brush, array<Point>^ points 
            [, FillMode fillMode]);
    void FillPolygon(Brush^ brush, array<PointF>^ points, 
            [FillMode fillMode]);

metódusok. Az opcionális fillMode paraméterrel a kifestés is megadható (Alternate, WindingGraphics2D).

A

    void FillEllipse(Brush^ brush, Rectangle rect);
    void FillEllipse(Brush^ brush, RectangleF rect);
    void FillEllipse(Brush^ brush, int x, int y, 
                int width, int height);
    void FillEllipse(Brush^ brush, float x, float y, 
                float width, float height);

metódusok rect struktúrával, vagy x,y, height width adatokkal megadott ellipszist festenek a brush ecsettel.

A rect struktúrával vagy x,y, height width adatokkal megadott ellipszis egy cikkét festik ki a brush ecsettel az alábbi metódusok:.

    void FillPie(Brush^ brush, Rectangle rect, 
            float startAngle, float sweepAngle);
    void FillPie(Brush^ brush, int x, int y, 
            int width, int height, 
            int startAngle, int sweepAngle);
    void FillPie(Brush^ brush, float x, float y, 
            float width, float height, 
            float startAngle, float sweepAngle);

A points tömb tartópontjai által meghatározott, zárt kardinális görbe által határolt területet festik ki a brush ecsettel az alábbi metódusok. Esetleg megadhatjuk a kifestés módját (fillmode) és a görbe feszítését (tension) is.

    void FillClosedCurve(Brush^ brush, array<Point>^ points, [
                FillMode fillmode, [float tension]]);
    void FillClosedCurve(Brush^ brush, array<PointF>^ points, [
                FillMode fillmode, [float tension]]);

A path figurát festhetjük ki a brush ecsettel a

    void FillPath(Brush^ brush, GraphicsPath^ path);

metódussal.

A region régiót festhetjük ki a brush ecsettel a

    void FillRegion(Brush^ brush, Region^ region);

metódussal.

A képernyő egy pontjával (upperLeftSource vagy sourceX, sourceY) a blocskRegionSize méretű téglalapot másolhatunk a rajzlap adott pontjától (upperLeftDestination) elhelyezkedő téglalapba. A copypixelOperation paraméterben megadhatjuk, hogy mi történjen a cél helyén már ottlévő pontokkal (a CopyPixelOperation felsorolás logikai műveletet definiáló elemeivel: SourceCopy, SourceAnd, SourceInvert, MergeCopy stb.).

    void CopyFromScreen(Point upperLeftSource, 
                Point upperLeftDestination, 
            Size blockRegionSize, [
            CopyPixelOperation copyPixelOperation]);
    void CopyFromScreen(int sourceX, int sourceY, 
                int destinationX, int destinationY, 
            Size blockRegionSize, [
            CopyPixelOperation copyPixelOperation]);

A grafikus beállítások állapotát transzformációk, vágások beállítása stb. elmenthetjük a

    GraphicsState^ Save()

metódussal, és később visszaolvashatjuk a Save() függvény visszatérési értékének felhasználásával (gstate)

    void Restore(GraphicsState^ gstate);

metódussal.

Hasonlóan a

    GraphicsContainer^ BeginContainer();

metódussal menthetjük a Graphics pillanatnyi állapotát a verembe.

Az

    void EndContainer(GraphicsContainer^ container);

hívás visszaállítja azt a grafika állapotot, amit a BeginContainer() metódus hívásakor mentettünk, és a konténernyitáskor megkapott GraphicsContainer osztály példányával azonosítunk.

IV.4.14. Nyomtatás

A nyomtatási lehetőségek a System::Drawing::Printing névtéren definiáltak.

A nyomtatást a PrintDocument osztály segítségével végezhetjük. Akár a Toolbox-ról felhelyezhetjük a formra az osztály egy példányát, de létrehozhatjuk azt a

    PrintDocument()

konstruktorral.

A PrintDocument osztály példányával dokumentumokat nyomtathatunk. A dokumentum nevét is beállíthatjuk a String típusú

    DocumentName 

írható és olvasható tulajdonsággal.

A PrinterSettings osztály egy példányára mutat a PrintDocument objektum

    PrinterSettings

tulajdonsága. Ennek statikus InstalledPrinters tulajdonsága egy sztring-gyűjtemény, amely a rendszerbe telepített nyomtatók nevét tartalmazza. A PrinterName sztring tulajdonságot használhatjuk arra, hogy beállítsuk hova szeretnénk nyomtatni. Ha nem teszünk semmit, akkor az alapértelmezett nyomtatót használjuk. Az IsValid tulajdonságból megtudhatjuk, hogy jó nyomtatót állítottunk-e be.

A PrinterSettings osztály egy sor tulajdonságot tartalmaz a nyomtató lehetőségeinek és beállításainak lekérdezésére. Például, a CanDuplex logikai tulajdonság a kétoldalas nyomtatási lehetőségről informál, a szintén logikai Duplex ennek használatát szabályozza. A Collate a leválogatási lehetőségről tudósít, a Copies a nyomtatott példányszámot állítja be és mutatja a MaximumCopies tulajdonságig. A logikai IsDefaultPrinter megmutatja, hogy alapértelmezett nyomtatóval van e dolgunk, a szintén logikai IsPlotter a rajzgépet, a SupportColor tulajdonság a színes nyomtatót jelzi. A nyomtató tulajdonságaitól függ a PaperSizes gyűjtemény, amely a használható papírméreteket, PaperSources pedig a használható papír-forrásokat tartalmazza. A PrinterResolution az elérhető felbontásokat tárolja. A LandScapeAngle a portré és a tájkép beállítások közti szöget definiálja.

A DefaultPageSettings tulajdonság a PageSetting osztály egy példánya, mely tartalmazza az összes lapbeállítást (színek, margóméretek, felbontások).

Kiválaszthatjuk a dokumentum egy részét a PrintRange tulajdonsággal (AllPages, SomePages, Selection, CurrentPage stb. értékek). A nyomtatás kezdő oldala (FromPage) és utolsó oldala (ToPage) is beállítható. Gondoskodhatunk fájlba való nyomtatásról a PrintToFile logikai tulajdonság beállításával. Ilyenkor a fájl neve megadható a PrintFileName sztring tulajdonságban.

A nyomtatást kezdeményezhetjük a PrintDocument osztály

    void Print()

metódusával. A nyomtatás során az osztály eseményei aktiválódnak: a BeginPrint (nyomtatás kezdete), az EndPrint (nyomtatás vége) és PrintPage (oldalnyomtatás). Mindegyiknek paramétere a PrintEventArgs osztály egy-egy példánya, melynek tulajdonságai a PageSettings osztály (a Bounds - laphatárok, Color – színes nyomtatás, LandsCape - tájkép, Margins - margók, PaperSize- papírméret, PrinterResolution – nyomtató felbontás tulajdonságokkal), a margó és oldalbeállítások mellett a már ismert Graphics típusú nyomtató rajzlap is. A HasMorePages írható, olvasható logikai tulajdonság azt mutatja, hogy van-e további nyomtatandó oldal.

Az alábbi példa a formon való kattintás hatására az alapértelmezett nyomtatón (esetünkben a Microsoft Office OnNote program) vonalat rajzol.

    private: System::Void printDocument1_PrintPage(
            System::Object^  sender, 
            System::Drawing::Printing::PrintPageEventArgs^  e) {
    e->Graphics->DrawLine(gcnew     Pen(Color::Red,2),10,10,100,100);
}
    private: System::Void Form1_Click(System::Object^  sender, 
            System::EventArgs^  e) {
    printDocument1->Print();
}

Az OnNote az alapértelmezett nyomtató
IV.73. ábra - Az OnNote az alapértelmezett nyomtató


Irodalmak:

[4.1.] MicroSoft Developer Network http://msdn.microsoft.com/. 2012.07.

[4.2.] Benkő , Tiborné, Kiss , Zoltán, Kuzmina , Jekatyerina, Tamás , Péter , és Tóth , Bertalan. Könnyű a Windowst programozni!? . ComputerBooks . Budapest . 1998.

[4.3.] Rózsa , Pál. Lineáris algebra. Műszaki könyvkiadó . Budapest . 1974.

[4.4.] Szirmay-Kalos , László, Antal , György , és Csonka , Ferenc. Háromdimenziós Grafika, animáció és játékfejlesztés. ComputerBooks . Budapest . 2003.

[4.5.] World Wide Web Consortium - www.w3c.org. 2012.

[4.6.] Funkhouser, Thomas . Image Quantization,Halftoning, and Dithering. Előadásanyag Princeton University www.cs.princeton.edu/courses/archive/.../dither.pdf.