játékfejlesztés.hu
FórumGarázsprojectekCikkekSegédletekJf.hu versenyekKapcsolatokEgyebek
Legaktívabb fórumozók:
Asylum:    5448
FZoli:    4892
Kuz:    4455
gaborlabor:    4449
kicsy:    4304
TPG:    3402
monostoria:    3284
DMG:    3172
HomeGnome:    2919
Matzi:    2520

Pretender:    2498
szeki:    2440
Seeting:    2306
Geri:    2188
Orphy:    1893
Joga:    1791
Bacce:    1783
MaNiAc:    1735
ddbwo:    1625
syam:    1491
OpenGL + WinAPI 2008.04.17 06:50


Ha egyszer már belevetettük magunkat a 3D-s programozás, és az OpenGL világába, valószínűleg szembesültünk azzal a ténnyel, hogy sok „felesleges” dolgot is le kell kódolnunk mire a tényleges 3D megjelenítés kódolásáig eljuthatnánk. Magát az ablakot is létre kell hoznunk, amibe rajzolni fogunk. Habár erre van már kész megoldás GLUT képében, mégis sokan vannak, akik szeretik saját kezükbe venni az „irányítást”, ez a kis írás pont őket célozza meg.
Ha a kényelmes GLUT után vágunk bele a WinAPI világába, akkor bizony kényelmetlennek fog tűnni az egész, de ez ne vegye kedvünket, mert így sokkal inkább mi tudjuk irányítani az egész folyamatot.
Első nekifutásra nem szeretném bonyolítani a dolgot, ezért kerüljük az OO megvalósítást.
Az első fontos lépés, hogy nem konzolos alkalmazást hozunk létre, hanem ablakosat. Ez nagyon fontos lépés, mivel minden Windows program, a WinMain ponton lép be, ellenben a konzolos alkalmazások a main függvénnyel indulnak. A WinMain függvény boncolgatása előtt még egy fontos dologra kitérünk, mégpedig az include állományokra. Mivel már a WinAPI-t használjuk, azért be kell hívnunk a windows.h headert:

Kód:
#include <windows.h>


Szörnyen bonyolult. :D
Ez volt a neheze, nézzük a könnyebb részt, a WinMain belépő függvényt. Teljes pompájában valahogy így néz ki:

Kód:
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)


Az első két paramérter HINSTANCE típusú, ez eslőt leszámítva kb. sohasem fogjuk használni őket. A harmadik paraméter neve már árulkodik, sztring típusú, és igen, a parancssori paramétereket érhetjük el innen. Legvégül pedig egy paraméter, mely azt jelzi, hogy az ablakot milyen állapotban kellene indítani: normál, rejtett, minimalizált, maximalizált, stb.

A jelenlegi teljes kódunk valahogy így fest:

Kód:
#include <windows.h>


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{

return 0;
}


Ez már így hiba nélkül fordul, de ha elindítjuk nem fogunk látni semmit. J Nem baj, mi elhisszük, hogy futott a programunk.

A következő lépésként meg kell ismernünk a WinAPI egyik legfontosabb típusát, a HWND-t. Mikor létrehozunk egy ablakot, akkor ezt valahogy később is el kell érnünk, ha hivatkozni szeretnénk rá, ilyenkor jó ha van egy azonosítója, ez lesz a HWND típusú változó.
Egyelőre deklaráljuk csak globális változóként (ergo, nem a WinMain függvényen belül).

Kód:
HWND hWnd = NULL;


Mint minden változót, ezt is lássuk el kezdőértékkel, esetünkben legyen NULL.

Valószínű, már hallottunk róla, hogy a Windows eseményalapú munkát végez. Ezt értsük úgy, hogy ha fut az ablakunk és pl. kattintunk rajta valahol, akkor egy üzenet érkezik az ablakunknak (attól függően, hogy melyik gombbal kattintottunk), hogy kattintás történt. Ezeket az eseményeket le kell kezelnünk, ha mindet nem is (nem fontos minden rendszerüzenetet lekezelni!), de muszáj lesz valahogy őket elkapdosni.
Erre a megoldás egy másik függvény, ami kb. így néz ki:

Kód:
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)


WndProc a függvényünk neve, négy paraméterrel dolgozik:
A HWND-t már letárgyaltuk, tudjuk hogy ez egy ablak azonosító lesz,
UINT msg, ez lesz maga az üzenet, pl. bal egérgombos kattintás történt (WM_LBUTTONDOWN)
WPARAM és LPARAM-ban pedig a különböző paraméterek tárolódnak, ha a kattintásos példánál maradunk, akkor a kattintás pillanatában az egér x és y pozíciója.
(Itt még fontos megemlíteni, hogy mind a WPARAM, mind az LPARAM egy 32 bites egész, melynek általában az alsó és a felső 16 bitjén is szoktak külön adatot küldeni, tehát pl. az egérpozíciója az LPARAM típusú paraméteren érkezik, az x az alsó 16 biten, míg az y a felső 16-on. Erről részletesebben majd máskor.)
Szóval, maga az eseménykezelés úgy történik, hogy switch-be bevágjuk az üzenetet, s lekezeljük, ami nekünk kell:

Kód:
switch(msg)
{
case WM_CLOSE:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd, msg, wParam, lParam);
}


WM_CLOSE üzenet akkor érkezik hozzánk, ha a felhasználó az ablak kis x-én kattintott, vagy pedig egy Alt+F4-et nyomott. Ez esetben küldünk egy kis üzenetet, hogy ki szeretnénk lépni, hagsúlyozva a „ki szeretnénk” részt, ez csak egy kérés. Ennek a kérésnek a PostQuitMessage hívással teszünk eleget (PostQuitMessage egyébként bármikor, bárhonnan kiadható a programunkon belül).
FONTOS, hogy az eseményválasztás default ágában (ez ugye akkor hajtódik végre, ha egyetlen egy megadott esemény (case:) sem „talál gazdára”), ha mi nem, akkor majd ő lekezeli az eseményt, erre szolgál a DefWindowProc, mely paraméterei megegyeznek a mi eseménykezelő függvényünk paramétereivel, mily meglepő kb. ugyanazt is csinálja mint a miénk.
Most már az eseményeket, lekezelésüket is ismerjük, de még egy ablakot sem tudunk létrehozni. Kicsit gáz, de tegyünk ellene.
Irány vissza a WinMain függvényünk törzsébe.
Első lépésünk az ablak felé, egy WNDCLASSEX struktúra kitöltése, ez fog pár adatot tárolni az ablakunkhoz, mint pl. az ikonok, ablakon belüli egérkurzor, eseménykezelő függvény…

Kód:
WNDCLASSEX wc;
const char wclass[] = "WinClassOne";


wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = wclass;


Pontosan 12 tagja van a WNDCLASSEX struktúrának, többé-kevésbé beszédes nevekkel ellátva:
cbSize– maga a WNDCLASSEX struktúra mérete
style – az ablakunk stílusa, esetünkben horizontális és vertikális újrarajzolást megadva
lpfnWndProc – itt adjuk meg az eseménykezelő függvényünket, melyet már kitárgyaltunk előbb
cbClsExtra, cbWndExtra – kb. mindig 0 értékkel operálnak, esetünkben is.
hInstance – igen, igen, ez pont az a HINSTANCE, a WinMain függvény első paramétere
hIcon, hIconSm, hCursor – az ablakunk ikon, valamint az egérkurzor adatai, esetünkben (és ezen kívül is gyakran) csak az előre deklarált értékekkel (IDI_APPLICATION, IDC_ARROW)
hbrBackground – az ablakot kitöltő szín, most legyen csak fekete, az menő
lpszMenuName – semmi érdekes, kb. mindig NULL az értéke
lpszClassName – ez egy fontos pont lesz, ugyanis ezzel a sztringgel azonosítjuk a regisztrált ablakleíró osztályt (ezt a kitöltött WNDCLASSEX-t :)), itt biztos, ami biztos egy változót használunk, na meg lusták is vagyunk beirogatni mindenhova ugyan azt…
Megvan a 12 tag, mindegyiket szépen meghatároztuk, akkor jöhet a struktúra regisztrálása:

Kód:
if(!RegisterClassEx(&wc))
{
return 0;
}


Azt hiszem, ehhez nem kell semmit fűzni, ha nem jó a kitöltött leíró osztály, akkor ki is lépünk hiszen nem folytathatjuk a munkánkat hiányzó adatokkal.

Mivel már megvan, majdnem, minden adatunk egy ablakhoz, hozzuk is azt létre:

Kód:
hWnd = CreateWindow(wclass,
"Window title",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
640, 480,
NULL, NULL,
hInstance,
NULL);


Egyből szemet szúr, hogy ez a bizonyos CreateWindow visszaad –jobb esetben – egy HWND típusú azonosítót, hát kapjuk el.
Szóval CreateWindow :
- jelen esetben a már deklarált osztálynevet adjuk meg elsőként
- a “Window title” pont az a szöveg, ami megjelenik majd az ablak címsorában
- harmadik paraméterként a CreateWindow egy „ablak kinézet” meghatározást vár, jelen esetben a WS_OVERLAPPEDWINDOW egy olyan ablak melynek van kerete, fejléce, minimize, maximize, na meg egy kis x gombja is. Ha nekünk egy különleges ablak kell, mondjuk, ne legyen minimize, maximize gombja, az is előállítható több stílus összefűzésével (igen, bitenként vaggyal): WS_CAPTION | WS_SYSMENU.
- negyedik és ötödik parameter az ablak kezdőpozícióját határozza meg, x és y koordinátával, mi most ezt a Windows-ra bízzuk, határozza meg ő.
- következik az ablak dimenzója, szélessége és magassága, 640x480
- nyolcadik paraméter tipusa már ismert, HWND, itt pedig arra utal, hogy ki a mi ablakunk szülője. Ha NULL-t adunk meg, akkor maga a desktop lesz ez, vagyis egy önálló ablakunk lesz.
- kilencedik paraméterként megadhatjuk a menü „kezelőjét”, vagyis, ha szeretnénk menüt az ablakunkba (tudjátok, File, Edit, View, stb, stb), akkor itt jelezhetjük
- utolsó előtti HINSTANCE típusú paraméter pedig az ablak példány azonosító, amit a WNDCLASSEX-nek is megadtunk, WinMain függvény első paramétere
- legvégül pedig egy mutató arra az értékre, amit a WM_CREATE (ez az üzenet akkor küldődik el az eseménykezelőnek (WndProc), mikor létrejött az ablak) LPARAM-ként kap az eseménykezelőben, mivel ez nekünk nem fontos, ezért csak NULL-t írunk be.

Érdemes ellenőrizni a hWnd értékét mielőtt tovább lépnénk:

Kód:
if(hWnd == NULL)
{
return 0;
}


Itt meg kellene említeni egy furcsa hibát: a leíró osztály regisztrálása sikeres, a CreateWindow által visszaadott HWND mégis nulla, annak ellenére, hogy hibátlanul adtunk meg minden paramétert. Ez akkor fordul elő, ha az eseménykezelő nem kezeli le a WM_CREATE üzenetet. Mint láthatjuk mi sem kezeljük le, LÁTSZÓLAG! Ugyanis mégis lekezelődik a DefWindowProc által. Ezt jegyezzük meg. :)
Nagyjából meg is vagyunk a létrehozással, de még két aprócska függvényhívást elvégzünk:

Kód:
ShowWindow(hWnd, SW_SHOW);
UpdateWindow(hWnd);


Első neve teljesen elmond mindent, tegyük láthatóvá az ablakot. Jelen esetben az SW_SHOW értéket adjuk meg, mely azt jelzi, hogy látható lesz az ablak. Itt azt az értéket is adhatnánk, amit a WinMain utolsó paramétere jelez (előbb már kiveséztük).
Még egy kis zárójeles megjegyzés, hogy ha a CreateWindow ablak stílus meghatározásához hozzáadjuk a WS_VISIBLE értéket, akkor nem fontos meghívni a ShowWindow függvényt, mivel a WS_VISIBLE jelzi, hogy a létrehozott ablak egyből legyen látható, a rejtett alaptól elérően. Hogy melyiket használjuk rajtunk áll, teljesen mindegy.
UpdateWindow-ról sem hiszem, hogy sok magyarázatot kell írni, frissítjük az ablakot, bár ez láthatóan semmit nem csinál. :)

Ha ebben az állapotban lefordítjuk a kódunkat, indítás után látjuk, hogy felvillan az ablak, de el is tűnik, ugyanis egyből ki is lép, csúnyán takarítás nélkül, habár az ablak bezáródik, s a program el is tűnik a feladatkezelőből is, azért illene eltüntetni a nyomokat. De ez a kisebb gond. Ami most nekünk kell, hogy ne lépjen ki egyből a program indulás után. Erre a legjobb ez olyan ciklus lenne, ami akkor lép ki, ha egy WM_QUIT üzenet érkezik (jé, a PostQuitMessage is pont ilyent küldözget).

Az üzenetek tárolásához is van ám előre megadott struktúra:

Kód:
MSG msg;


Nem sokat érdemes róla beszélni, van pár tagja, köztük egy “message” is, ezt fogjuk ellenőrizni a ciklus feltételeként. De használat előtt inicializáljuk a biztonság kevéért (s, hogy elhallgattassuk a fordító figyelmeztetését):

Kód:
ZeroMemory(&msg, sizeof(MSG));


Hol is tartunk?:

Kód:
while(msg.message != WM_QUIT)
{

}


Alakul, de még használhatatlan. A célunk, hogy ha egy üzenetet kap az ablakunk, akkor azt küldjük el az eseménykezelőnek.

Kód:
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}


Szóval PeekMessage fogja nekünk elkapni a kapott üzeneteket, paraméter listája nem valami izgalmas téma: elsőként az MSG stuktúra címét adjuk meg, mely tárolni fogja az üzeneteket, utolsó paraméter pedig azt jelzi, hogy a kapott üzenetet eltávolítjuk az ablak felé küldött üzenetek listájáról, ergo nem dolgozzuk fel többször UGYANAZT az üzenetet. Persze ha kétszer kattintunk az ablakon, akkor kettő KÜLÖNBÖZŐ üzenet érkezik, mégis mindkettő ugyanazt az utasítást tartalmazza.
Szóval ha a PeekMessage igazzal tér vissza, akkor van üzenet, kezdjünk vele valamit.
TranslateMessage fordítja le az üzeneteket, míg az üzenet küldését az eseménykezelő felé a DispatchMessage függvény végzi. Tehát:

Kód:
while(msg.message != WM_QUIT)
{
if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
//render
}
}


Szemfülesebbek már biztos kiszúrták, hogy mi van akkor, ha nem jön üzenet? Akkor bizony tétlenkedik a programunk. Ezt azért nem hagyhatjuk, ilyenkor mit is csináljunk? Hát mondjuk renderelünk OpenGL segítségével. De még most nem. Majd legközelebb. :)

Volt már szó a takarításról. Nézzük meg most kód szinten is. Ha ki akarunk lépni, akkor kattintunk a kis x-en, jön a WM_CLOSE üzenet, elkapjuk, PostQuitMessage küld egy WM_QUIT-et, ciklus feltétel igaz, megszakad, haladunk tovább a vég, a return felé.
Logikus, hogy a takarítást valahol itt kellene elvégezni, a return előtt.

Kód:
UnregisterClass(wclass, hInstance);
DestroyWindow(hWnd);


Két lépésben takarítunk: először is a regisztrált osztályleírónkat távolítjuk el, ablakunk osztálynevét és a program példány azonosítóját felhasználva (6 millió + egyszer elmondva a WinMain első paramétere az :)).
Ezzel megvagyunk bontsuk le az ablakunkat is DestroyWindow, az ablak azonosítóját használva az azonosításhoz. Plusz infó, hogy a DestroyWindow egy WM_DESTROY üzenetet küld az eseménykezelő felé, kb. ugyanaz mint a WM_CLOSE, de mint tudjuk WM_CLOSE x-re kattintva, valamint Alt+F4-re keletkezik, WM_DESTROY pedig csakis DestroyWindow hívással, tehát ez nem csak kérés, ez már inkább tény.
WinMain függvény visszatérési értéke legyen az utolsó kapott üzenet, a WM_QUIT. Ez egyébként nem szentírás, lehet 0 is. Szabad a döntés.
Ha ide eljutottunk, s minden jól ment, akkor most már létre tudunk, hozni egy ablakot, amibe majd renderelhetünk OpenGL segítségével.
Felmerülhet a kérdés, miben jobb ez a módszer, mint amit a GLUT biztosít? Igaz, hogy a GLUT tud más platformokon is működni (Linux például?), de ha Windows alá szeretnénk elsősorban dolgozni, akkor egyértelműen WinAPI a legéletképesebb megoldás, ami kb. végtelen szabadságot is ad.
Fájl : OpenGL + WinAPI
Méret: 4kb Mb - Letöltve : 472

Értékelés: 0

Új hozzászólás
beast          2008.04.30 03:39
DevC-ben Project Options-ben, General fül alatt ott van egy Icon panel.
Burwor          2008.04.30 01:48
Valaki leírhatná hogy kell saját ikont az ablakhoz hozzárendelni (IDI_MYICON). De nem ilyen Visual Studio Wizarddal. Én Dev-C-ben nem bírom összehozni.
Asylum          2008.04.19 17:08
egy igazi hacker ezt is megoldja
beast          2008.04.17 15:41
Pedig minden windows progi winmain-nel indul, else: unresolved external symbol _WinMain@16.
Asylum          2008.04.17 12:54
szerintem is jó de az nem teljesen igaz, hogy minden windows progi winmain el indul
sima main()-el is ugyanugy müködik csak megjelenik a konzol is (szerintem hasznos).
beast          2008.04.17 09:24
Folytatás eggyel feljebb.
MaximumViolence          2008.04.17 09:21
naggyon jó,kicsit szájbarágós,de kell is az folytatás?