Az első ablakos alkalmazás elkészítése

 Névtelen 1
  1. A Windows ablakkezelése
    1. Az ablak felépítése
    2. Az ablak tulajdonságai
    3. Az üzenetkezelési lánc
    4. Üzenetek küldése az ablakok között
    5. Az üzenetek tartalma
  2. Az első ablakos Windows alkalmazás
    1. A WinMain függvény
    2. Az ablakeljárás felépítése
    3. Az ablakosztály előállítása
    4. Az ablak létrehozása
    5. Az emlegetett program kódja
    6. Az emlegetett program futás közben
  3. A WinSpy++ használata

1, A Windows ablakkezelése

Ebben a részben elkészítjük az első valódi Windows alkalmazást, ami természetesen egyelőre egy csak üres ablak lesz. Ahhoz hogy ezt ki tudjuk vitelezni újabb ismeretekre van szükség pl. az ablakokról vagy az üzenetkezelési rendszerről.

1.1, Az ablak felépítése

Az ablak (window) alapvetően több részből épül fel. Igazából az ábrán nagyon sok minden látható, de egy dolog lenne itt igazán fontos - hogy néhány alapfogalmat tisztázzunk.

A kliensterület (client area) az a része az ablaknak, amit közvetlenül programozhatunk, megadhatjuk hogy melyik részén mi legyen, vezérlőket pakolhatunk ki rá, rajzolhatunk rá, illetve válaszolhatunk az itt történt billentyűleütésekre, kattintásokra. A kliensterületnek nem része a címsor, a menü, stb. mint ez az ábrán is látható.

A vezérlő (control) olyan előkészített ablakelem (pl. gomb, lista, jelölőnégyzet, ...), amik egy adott funkciót látnak el az ablakon, saját tulajdonságokkal, eseményekkel bírnak.

A tulajdonság (property) olyan az adott objektumhoz/vezérlőhöz tartozó változó, aminek módosítása valamilyen változást fog okozni annak megjelenésében, működésében. Egy gomb tulajdonsága pl. a gombon lévő szöveg, vagy a gomb mérete, és helyzete.

Az esemény (event) akkor váltódik ki, mikor pl. rákattintunk egy gombra, vagy egy adott vezérlő felett leütünk egy billentyűt, vagy csak az egeret mozgatjuk felette. Létezik teljesen szoftveres alapú esemény is, pl. mikor átméretezzük az ablakot, vagy amikor valami oknál fogva a Windowsnak újra kell rajzolnia az ablak tartalmát, és erről eseményt küld nekünk - hogyha egyedi rajzolása van az ablaknak azt most rajzoljuk rá fel.

Egy ablakot párbeszédablaknak (dialog) nevezünk akkor, ha

  • felhasználói értelemben véve: nincs ikonja, menüsora, csak bezárás gomb van rajta és rendelkezik valamilyen gombsorral (pl. OK, Mégse, Alkalmaz)

  • programozási értelemben véve: előre legyártott ablak, melyen a vezérlők nem futásidőben lesznek felpakolva hanem alkalmazás-erőforrásként lesznek tárolva

1.2, Az ablak tulajdonságai

Minden ablak legfontosabb tulajdonsága az ún. ablakeljárás (window procedure). Az ablakeljárás nem más, mint egy olyan eljárás, amiben megfelelően reagálunk az egyes eseményekre. Ilyen reakció pl. hogyha rányomunk a gombra, akkor rajzolja át alaphelyezetről, arra a helyzetre, mikor be van nyomva:

A Windows rendszerben minden egyes vezérlő egy-egy ablak, így a lenyomás műveletét pl. a gomb saját ablakeljárása végzi. Ebből adódik az is, hogy néha nem árt spórolni a vezérlőkkel, pl. egy ablakra 3000 jelölőnégyzet helyett, érdemesebb egy kézzel kirajzoltatott listát kitenni, 3000 elemmel - így már csak egy vezérlőt tettünk ki.

A remek kérdés az lehet, hogy ezek szerint még egy gomb lenyomását is nekünk kell leprogramozni? A válasz természetesen nem, a Windows tartalmaz beépített ablakeljárásokat is, ahol az eseményeknek a kezelését rábízhatjuk a rendszerre. Ez teszi lehetővé azt, hogy csak azokat az eseményeket írjuk át/bővítsük amik a saját programunk szempontjából fontosak.

Egy ablakeljárás újra és újra felhasználható az osztálynév (class name) által. Ez lehetővé teszi, hogy definiáljunk osztályokat - mint pl. a "Button" osztályt - ahol az egyes ablakpéldányok, ugyanazt az ablakeljárást fogják használni. A Windows rendszerben rengeteg ilyen előre definíált osztály van - ez teszi lehetővé azt, hogy a programok ugyanazokat a vezérlőket használják.

Az ablak neve (window name) az a szöveg, ami a legértelemszerűbb helyen fog megjelenni az adott ablakon - egy ablaknak a címsorában, egy gombnak a közepén, egy jelölőnégyzetnek pedig a négyzet mellett.

Az ablak stílusa (window style) az a halmaz, ami megadja hogy milyen kinézete legyen az adott ablaknak a kliensterületén kívül - pl. legyen-e kerete, ha igen milyen gombok legyenek a bal felső sarokban, stb...

Az ablak kiterjesztett stílusa (extended window style) lehetővé teszi néhány további kinézet, és viselkedésbeli finomság megadását - szintén halmaz formájában.

Az ablak helyzete (position) megadja hogy a képernyő bal felső sarkához képest, hány képpontra helyezkedik el az ablakunk bal felső sarka. Hogyha egy vezérlőt tekintünk akkor a referencia, a befoglaló ablak kliensterületének bal felső sarka lesz.

Az ablak mérete (size) megadja hogy hány képpont széles, és magas az adott ablak befoglaló mérete. Ez általában nagyobb, vagy egyenlő a kliensterület méretével.

A szülő ablak (parent window) az az ablak, amelyikben elhelyezkedik az adott ablak. Ez vezérlők esetén ugyebár majd az általunk létrehozott ablak lesz. Ilyenkor a belső ablakra azt mondjuk hogy gyermekablak (child window).

A tulajdonos ablak (owner window) az az ablak, amelyikkel birtokosi kapcsolatban áll egy adott ablak. Ilyenkor a birtokolt ablak (owned window), a tulajdonos ablak előtt jelenik meg, ha a tulajdonost elrejtik, vagy a tálcára csukják a birtokolt is megy vele. A tulajdonos ablak megsemmisítésekor, a birtokolt is megsemmisül.

Az ablak azonosítója (window handle) lesz az a számérték, amelynek segítségével a Windows utasításokon keresztül módosítgathatjuk az ablakunkat. A közvetlen hozzáférés elkerülhető ezzel a megoldással. Habár erre külön típus van bevezetve (HWND), ez nem más mint egy unsigned long int - jelen esetben mégis használjuk a HWND típust, hogy látszódjon hogy ez nem csak egy szám változó.

Az ablak ezen felül rendelkezik még:

  • menüvel (menu)

  • alkalmazáspéldány azonosítóval (application instance handle)

  • létrehozási adatokkal (creation data)

1.3, Az üzenetkezelési lánc

Most már nagyjából valami fogalmunk van arról hogy programozási szempontból mi az az ablak. De még mindig nyitva van a kérdés - honnan fog lefutni az ablakeljárás, és értesülést kapni arról hogy billentyűleütés történt hogy feldolgozhassa? Ezt a feladatot végzi az üzenetkezelési lánc.

A Windows a különféle eseményekről üzeneteken (message) keresztül tájékoztat. Ez nem más mint egy struktúra négy darab duplaszó értékkel (DWORD = unsigned long int), és néhány kiegészítő adattal:

typedef struct tagMSG {
	HWND   hwnd;
	UINT   message;
	WPARAM wParam;
	LPARAM lParam;
	DWORD  time;
	POINT  pt;
} MSG, *PMSG, *LPMSG;
  • hwnd: az üzenet címzettje, az címzett ablak azonosítója

  • message: az üzenet tárgya

  • wParam: az üzenet alsó része (word parameter)

  • lParam: az üzenet felső része (long parameter)

  • time: az elküldés időpontja

  • pt: a kurzor koordinátája, az elküldés időpontjában

A rendszer az ablakok számára fenntart egy üzenetlistát (message queue), amelyben folyamatosan gyűlhetnek az egyes üzenetek, amíg a program nem tud azokra válaszolni - ilyen várólistás üzenet pl. a kilépés, az újrarajzolás, vagy az időzitő üzenetei. Léteznek természetesen olyan üzenetek is, amik nem várhatnak, ilyen lesz pl. az ami jelzi hogy előtérbe kerül, vagy hogy a pozíciója változott.

Mint erről már korábban szó esett, az üzenetek kezelését valamely ablakeljárásban végezzük, és ahhoz hogy a program folyamatosan kivegye ezen listából az üzeneteket, és eljuttassa az ablakeljárásig működtetnie kell egy üzenetkezelési láncot.

Íme egy egyszerű üzenetkezelési lánc:

MSG uzenet;

while (GetMessage(&uzenet, 0, 0, 0))
{
	TranslateMessage(&uzenet);
	DispatchMessage(&uzenet);
}

A GetMessage utasítás végzi el a listából való kivételt, majd ez után a TranslateMessage utasítás átalakítja a billentyűleütésekkel kapcsolatos üzeneteket, karakterekkel kapcsolatossá - végül pedig a DispatchMessage utasítás eljuttatja az adott ablakhoz az üzenetet. Ha gondoljuk a PeekMessage utasítással, esetleg megszűrhetjük az üzeneteket.

Ez a lánc akkor ér véget, ha a program egy kilépési üzenetet kap - ilyenkor majd a program futása is véget ér.

1.4, Üzenetek küldése az ablakok között

Az üzenetküldés során, az MSG struktúrának csak az említett négy duplaszó értékére van szükségünk, és két utasításra:

LRESULT WINAPI SendMessage(
_In_ HWND   hWnd,
_In_ UINT   Msg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);

BOOL WINAPI PostMessage(
_In_ HWND   hWnd,
_In_ UINT   Msg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);

A kettő ugyanúgy néz ki, és ugyanazt is csinálja, mindössze a működésükben van különbség.

A SendMessage megvárja amíg az üzenet eljut a címzetthez, értelemszerűen addig a mi programunkat akasztja meg (szinkron működésű), de megkapjuk az üzenetre adott választ is.

A PostMessage használata során csak annyit tudunk, hogy elindult-e a küldés, vagy nem - az eredményességéről, illetve az eredményéről sem tudunk semmit, viszont nem akasztja meg a programunkat ezzel (aszinkron működésű).

Mielőtt azt hinnénk, hogy innentől kezdve a két program ablakai között bármilyen adatot át lehet passzolni ezzel a két utasítással, sajnos ez tévedés. A két program címtere miatt, az egyikben érvényes mutató, a másikban érvénytelen lesz (pl. char* esetén) - így legfeljebb duplaszókat küldözgethetünk át egyik progiból a másikba (egyébként fix méretű struktúrák is átküldhetőek a WM_COPYDATA üzenet segítségével, de az is kissé komplikált).

1.5, Az üzenetek tartalma

Itt is van egy jelölésrendszer, mi ebből a WM (window message), illetve CM (control message) üzeneteket fogjuk legtöbbször alkalmazni.

Íme néhány nevezetes üzenet tárgya:

Hogy milyen legyen az wParam, és lParam értéke, azt minden egyes üzenetre leírja az MSDN, általában eltérnek. Lehet hogy egyik alkalommal csak egy szám, egy másik üzenetben egy halmaz, a harmadikban egy átmásolt struktúra címe.

Pl.: az "ablak" ablak azonosítójú ablakot szeretnénk bezárni:

HWND ablak = /*...*/;

PostMessage(ablak, WM_QUIT, 0, 0);

Egyébként mi magunk is definiálhatunk üzeneteket a WM_USER és a 0x7FFF értékek között, pl.:

#define WM_SAJAT_UZENET WM_USER+1

Ilyenkor a wParam és lParam céljáról teljesen mi magunk dönthetünk - ugyebár a cél az lenne hogy a másik oldal is tudja értelmezni az üzenetet, azzal hogy ott is leprogramozzuk annak értelmezését, illetve az IPC korlátait se hanyagoljuk el, mert hibákba fogunk futni..

2, Az első ablakos Windows alkalmazás

Ennyi rizsa után már érdemes lenne megnézni, milyen is egy Windows program kódja. Ehhez azonban még egy új dolgot el kell sajátítani, a korábban már említésre került WinMain függvény működését.

2.1, A WinMain függvény

Sajnos ára van annak hogyha áttérünk a Windows programozásra, méghozzá az hogy nem használhatjuk tovább az eddig jól bevált main függvényt, mivelhogy olyan adatokra is szükségünk van egy ablakos alkalmazáshoz, amit a main nem szolgáltat számunkra.

Íme hát az új "main" függvényünk, mostantól őt fogjuk használni:

int CALLBACK WinMain(
_In_ HINSTANCE hInstance,
_In_ HINSTANCE hPrevInstance,
_In_ LPSTR     lpCmdLine,
_In_ int       nCmdShow
);

A programunkból elindított minden egyes példánynak lesz egy alkalmazáspéldány azonosítója (application instance handle) amire szükségünk lesz. - ez lesz a hInstance.

Az lpCmdLine az nem újdonság, lényegében az összes paraméter amivel meghívták a programot egy szövegbe felfűzve.

Az nCmdShow azt adja meg, hogy milyen indítási móddal lett megnyitva a program, pl. alapból legyen a tálcára lecsukva, vagy alapból legyen kinagyítva, stb. - ez már a programok indításakor is használt konstansokat jelenti pl. SW_SHOWNORMAL

A visszatérési érték sem változott, ugyanaz a szerepe mint korábban a main esetében - de tilos a programot a továbbiakban a return, exit, halt, ... parancsok használatával megszakítani, mivel az összes megnyitott leíró (handle) fennmarad, és nem kerülnek felszabadításra a rendszerből - ez jelenthet pl. ablakokat, stb... Csak akkor használjuk ezeket a parancsokat, ha már minden leírót lezártunk/felszabadítottunk!

Megjegyzés: Az nCmdShow paraméter értékeinél az SW a show window rövidítése.

2.2, Az ablakeljárás felépítése

Íme egy sablon ablakeljárás:

LRESULT CALLBACK Ablakeljaras(HWND ablak, UINT msg, WPARAM wParam, LPARAM lParam) 
{
	switch(msg) 
	{
		case WM_DESTROY: 
			PostMessage(ablak, WM_QUIT, 0, 0);
			break;
		case ...:
 			/* tovabbi uzenetek kezelése */
			break;
		default:
			return DefWindowProc(ablak, msg, wParam, lParam);
	}
	return 0;
}

Az LRESULT típus szintén csak az unsigned long int egy újabb csúfneve, míg a CALLBACK azt jelenti hogy ez egy olyan függvény meg lehet hívni .dll-en keresztül is. Minthogy a Windows .dll-eken keresztül működik együtt a programjainkkal, ezért ez nagyon fontos.

A paraméterek az üzenetkezelésből már ismerősek lehetnek, az egyes üzenetek lekezelését pedig egy switch-case szerkezetben célszerű megoldani. Ez azért is jó, mert a default részben, azokat az üzeneteket amik nem érdekelnek minket átadjuk a Windows-nak, hogy csinálja meg vele amit amúgy is szokott - ez lesz a DefWindowProc utasítás, aminek a visszatérési értékét továbbítani kell, mintha csak mi állítottuk volna elő azt.

A WM_DESTROY üzenet az ablak megsemmisülésekor lép fel (bezárás után), ilyenkor a programunkat is be kell zárni, tehát megszakítjuk az üzenetkezelési láncot egy WM_QUIT üzenettel.

2.3, Az ablakosztály előállítása

Lényegében a korábban elmondottakat kell átültetni kódba, mivel már rendelkezünk ablakeljárással elkészíthetjük azt az osztályt, ami alapján létrehozhatjuk az ablakainkat (ha több is van):

WNDCLASS osztaly = {0};

osztaly.lpfnWndProc = &Ablakeljaras;
osztaly.hInstance = hInstance;
osztaly.hCursor = LoadCursor(NULL, IDC_ARROW);
osztaly.hbrBackground = (HBRUSH)(COLOR_WINDOW);
osztaly.lpszClassName = "Teszt_Ablakosztaly";

RegisterClass(&osztaly);

Itt van szükségünk az alkalmazáspéldány azonosítójára (hInstance), amiért main() függvényt kellett váltani. A struktúra kinullázása után, megadjuk:

  • az ablakeljárás címét

  • az alkalmazáspéldány azonosítóját

  • a hagyományos egérkurzor azonosítóját (a betöltés után adja vissza a LoadCursor utasítás)

  • az ablak hátterének színét (a HBRUSH típusról majd később lesz szó)

  • és az osztály nevét, itt "Teszt_Ablakosztaly" - később erre hivatkozva hozhatunk létre ilyen típusú ablakokat

A regisztrálás után (RegisterClass) már létre is hozhatjuk az első példányt az ablakunkból.

2.4, Az ablak létrehozása

Az ablakot magát nem fogjuk megkapni, de egy leírót igen aminek segítségével a későbbiekben hozzáférhetünk. A létrehozás a CreateWindow függvény segítségével történik meg:

HWND WINAPI CreateWindow(
_In_opt_ LPCTSTR   lpClassName,
_In_opt_ LPCTSTR   lpWindowName,
_In_     DWORD     dwStyle,
_In_     int       x,
_In_     int       y,
_In_     int       nWidth,
_In_     int       nHeight,
_In_opt_ HWND      hWndParent,
_In_opt_ HMENU     hMenu,
_In_opt_ HINSTANCE hInstance,
_In_opt_ LPVOID    lpParam
);

A paraméterek az alábbiak:

  • lpClassName: az imént regisztrált osztály neve
  • lpWindowName: az ablak fejlécének a címe, pl. "Teszt ablak"
  • dwStyle: az ablak stílusát jelölő halmaz, kezdésként megfelelő lesz pl. WS_OVERLAPPEDWINDOW | WS_VISIBLE
  • x, y: az ablak elhelyezése a képernyőn, megadható számérték is, vagy hogy kérjük az alapbeállítást - én most így teszek, mindkettő értéke CW_USEDEFAULT lesz
  • nWidth, nHeight: az ablak mérete, a szélessége, és a magassága - én ide 640x480 méretű ablakot választok
  • hWndParent: nincs szülőablak, tehát az értéke nulla (ilyenkor az Asztal (Desktop) lesz a szülőablak)
  • hMenu: nem készítettünk hozzá menüt, tehát az értéke nulla
  • hInstance: az alkalmazáspéldány azonosítója, megint csak szükséges adat - de a WinMain ezt szolgáltatja számunkra
  • lpParam: esetlegesen átadott további információk az ablaknak, mivel pillanatnyilag nincs ilyenre szükségem nálam az értéke NULL lesz

A visszatérési érték értelemszerűen az ablak azonosítója, ha sikerült megcsinálnia - a hibakezelést most hanyagolom az átláthatóság érdekében.

2.5, Az emlegetett program kódja

Lényegében csak minden eddigi ismeretet kell összegyúrni benne, és kész is vagyunk:

#include <windows.h>

LRESULT CALLBACK Ablakeljaras(HWND ablak, UINT msg, WPARAM wParam, LPARAM lParam) 
{
	switch(msg) 
	{
		case WM_DESTROY: 
			PostMessage(ablak, WM_QUIT, 0, 0);
			break;
		default:
			return DefWindowProc(ablak, msg, wParam, lParam);
	}
	return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) 
{ 
	//1. az ablakosztály definiálása, és regisztrálása
	WNDCLASS osztaly = {0};
	osztaly.lpfnWndProc = &Ablakeljaras;
	osztaly.hInstance = hInstance;
	osztaly.hCursor = LoadCursor(NULL, IDC_ARROW);
	osztaly.hbrBackground = (HBRUSH)(COLOR_WINDOW);
	osztaly.lpszClassName = "Teszt_Ablakosztaly";
	RegisterClass(&osztaly);

	//2. az ablak létrehozása
	HWND ablak = CreateWindow("Teszt_Ablakosztaly", "Teszt ablak", WS_VISIBLE | WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT, 640, 480, 0, 0, hInstance, NULL);

	//3. az üzenetkezelési lánc
	MSG uzenet;
	while(GetMessage(&uzenet, NULL, 0, 0) > 0) 
	{
		TranslateMessage(&uzenet);
		DispatchMessage(&uzenet);
	}

	//4, minden lezárult, kiléphetünk
	return 0;
}

2.6, Az emlegetett program futás közben

Talán nem is tűnik akkora nagy számnak, ahhoz képest amennyi meló volt vele - de a jó hír hogy a nehezén túlvagyunk, innen már látványosabb és kevésbé komplikált dolgok fognak következni, ráadásul erre a sémára fogunk majd építkezni amit itt elsajátítottunk.

Az ablak képes arra amit elvárunk tőle, le lehet csukni a tálcára (minimize), ki lehet nagyítani (maximize), vissza lehet állítani a méretét (restore), be lehet zárni (close), át lehet méretezni, és mozgatni is lehet. Egyelőre ez minden amit tud - de a konzol alkalmazásokhoz képest ez is komoly előrelépésnek számít.

3.7, A WinSpy++ használata

Segédprogramok által megtekinthető az is, amit az ablak mögé programoztunk - az általam használt program a WinSpy++. A program főoldalán, az ablakunk kiválasztását követően, rengeteg adat látható:

Látható, hogy az ablak címe Teszt ablak, az azonosítója (hWnd) 0x000402CE, az osztályneve Teszt_Ablakosztaly, illetve az alkalmazáspéldány azonosítója (hInstance) 0x00400000. A program az adatok megtekintésén kívül, rengeteg módosítási lehetőséget is biztosít - ideiglenesen módosítható pl. az ablak címe, stílusa, helyzete...

Itt módosítható az ablak helyzete, és mérete.

Itt módosítható néhány fontos jellemző, pl. ha az Always On Top lehetőséget aktiváljuk, akkor az ablak mindig el fogja takarni az összes többit, még akkor is ha éppen nem az a kijelölt, aktív ablak. A Bring To Front, illetve Send To Back parancsokkal előtérbe hozható az ablak, vagy a háttérba küldhető.

A Visible jellemző azt állítja hogy az ablak látható legyen-e. Az Enabled jellemző eltávolításával a programunk többé nem reagál pl. a billentyűleütésekre, egérkattintásokra - ilyenkor a Windows egy hangjelzéssel fogja tudatni velünk hogy az ablak pillanatnyilag "le van tiltva" (disabled). A Close Window parancs bezárja az ablakot, mintha csak a bezárás gombot megnyomtuk volna a sarokban, vagy az Alt+F4 billentyűkombinációt leütöttük volna (ilyenkor WM_CLOSE üzenetet küld neki).

Ha fület váltunk, a Styles oldalon anélkül kipróbálhatjuk az egyes stílusok kombinációját, hogy a programot 167x lefordítanánk. Például, ha kíváncsiak vagyunk arra, hogy milyen lenne az ablak egy párbeszédablak kerettel válasszuk ki az alábbi kombinációt:

Mostantól kezdve, az ablakunknak már egészen hasonlít a kerete a korábbi párbeszédablakokhoz:

Ezen új stílus mellett már csak odébb helyezhető, nem lehet átméretezni, mint korábban. Természetesen a program újraindítását követően megint egy új ablak jön létre, a kódban megadott paraméterek mellett, így visszaáll minden alaphelyzetben.

Az ablak osztályáról is sok információt tudhatunk meg a Class oldalon:

 

Itt például látható a választott egérkurzor, háttérszín, menüleíró, és ikonleíró is. Mivel túlzásokba nem estünk az osztály adatainak kitöltésénél, ez a rész elég hiányos pillanatnyilag.

A többi oldalon a folyamatazonosítókat, stb. találjuk meg, illetve a gyermek, és szülőablakok azonosítóit. Mivel ez a program egyablakos, erről már nem teszek fel képet, de megnézni mindenféleképpen érdemes.