V minulém díle jsme vytvořili nejjednodušší aplikaci ve Windows pomocí Win32 API, dnes si vytvoříme hlavní okno aplikace a podrobně si ho popíšeme...
Vítám vás opět u dalšího dílu. I dnešní díl bude trochu podrobnější vzhledem k teorii. Proč? Myslím, že je důležité, abyste co nejvíce pochopili základy API a proč, co a jak je tak, jak je. Pokud nebudete znát potřebné detaily, nevytěžíte z API to maximum, co jde. „A základ je základ“ :-).
V našem programu, který jsme splodili minule, jsme vytvořili zprávu pomocí funkce MesageBox. MessageBox vytváří vlastně „okno“. Windows chápe téměř většinu objektů jako okno (window). A co je okno? Okno je obdelníková oblast na obrazovce, která přebírá vstup od uživatele a zobrazuje výstup v podobě textu a grafiky.
Okno v podobě, které se vytvoří funkci MessageBox, má speciální určení a omezené možnosti. Má malé záhlaví s tlačítkem pro uzavření, volitelně ikonu, jeden nebo více řádků textu a až čtyři tlačítka. Ikony a tlačítka jsou z definovaných konstant. MessageBox je velice užitečná f-ce, ale nám moc nevystačí. Nemůžeme v ní vykreslit grafiku, používat systém nabídek atd. Potřebujeme vytvořit vlastní okno, se kterým můžeme kouzlit podle svých představ. Teď je ta pravá chvíle, jdeme na to.
Blokově se hlavní okno skládá ze těchto několika částí:
vstupní bod (WinMain)
třída okna
definice okna
smyčka zpráv
procedura okna
V podstatě funkce WinMain definuje a registruje třídu okna, nastavuje vzhled a chování okna a volá funkci CreateWindow, obstarává smyčku zpráv a na jejich základě provádí Windows kód v proceduře okna.
A teď se podíváme, jak to vypadá v praxi:
#include <windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
TCHAR szAppName[] = TEXT("Nazdárek celičkej světe:-)");
HWND hWnd;
MSG msg;
WNDCLASSEX wc;
wc.cbSize = sizeof(wc);
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 = NULL;
wc.hCursor = (HCURSOR)LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wc.lpszMenuName = NULL;
wc.lpszClassName = szAppName;
RegisterClassEx(&wc);
hWnd = CreateWindowEx(0,szAppName,
szAppName,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,NULL);
ShowWindow(hWnd, iCmdShow);
UpdateWindow(hWnd);
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
A to je vlastně všechno :-). Tento program vytvoří normální okno aplikace. Jeho kód najdete téměř v každé aplikaci Windows. I u her, když chcete ovládat grafiku a jiné části a doslova s nimy kouzlit, musíte mít tento kód, který předá řízení např. DirectX nebo OpenGL a nebo může jít pouze o 2D hru, kterou lze úplně s přehledem napsat jen pomoci API, když bude čas, můžeme si to někdy ukázat. API k tomu má definovano spoustu funkci :-).
Snad nikdo si nepamatuje z hlavy tuto syntaxi, a tak si nedělejte hlavu z toho, že si to taky nepamatujete a už vůbec se neděste. V praxi si většinou programátoři uchovávají základní projekt a pak jej kopírují a upravují dle potřeb aktuálního programu, který zrovna vytváří.
A teď už zbývá něco si o tomto kódu říct.
Tak hezky od začátku:
Funkce WinMain
Jako v minulém programu začíná kód vkládáním hlaviček. Zde jde o standardní hlavičku Windows. Program má funkci WinMain, stejně jako ten z minulého dílu, takže jí nebudeme dále rozebírat, ale má navíc i funkci WndProc.
Funkce WndProc je procedura okna. Zajímavostí je, že se nikde funkce WndProc přímo nevolá! Ale ve funkci WinMain je odkaz na WndProc a z toho důvodu je funkce deklarována na začátku programu. WndProc obsahuje obsluhu zpráv, o tom za chvíli.
V tomto kódu je už krásně vidět, že API používá velkou spoustu identifikátorů, jejichž názvy jsou psány velkými písmeny. Jak už jsme si řekli, tyto identifikátory jsou definované v hlavičkových souborech Windows.
Většina těchto identifikátorů obsahuje 2-3 písmenou předponu, za kterou následuje podtržítko. Jsou to prostě číselné konstanty. Předpona označuje obecnou kategorii, do které konstanta patří. Jejich význam vysvětluje tato tabulka:
CS - volba stylu třídy
CW - možnosti pro vytváření okna
DT - možnosti pro psaní textu
IDI - identifikační číslo (ID) ikony
IDC - identifikační číslo (ID) kurzoru myši
MB - možnosti dialogu se zprávou
SND - možnosti zvuku
WM - zpráva Windows
WS - styl okna
V našem programu jsou používané i některé nové datové typy. Někdy jsou to staré známé typy, které mají jen jiné jméno z důvodu zkratky. Jsou definované taktéž v hlavičkových souborech, a to příkazy typedef nebo #define.
První parametr funkce WndProc je takzvaný manipulátor okna (handle window). Handle se ve Windows používá dost často. Třeba i u funkce MessageBox. Vzpomínáte? Ten první parametr (HWND hWnd), ale jelikož jsme žádné okno vytvořené neměli, tak jsme použili hodnotu NULL, což je jen konstanta 0 a kdybychom ji tak napsali, výsledek bude úplně stejný. Pokud vás zajímá, co by se stalo, kdybychom v minulém dílle použili identifikátor hWnd (za předpokladu, že bychom definovali proměnnou HWND hWnd před voláním funkce), tak by se nám zpráva nezobrazila, poněvadž by byla platná jen pro dané okno, ale to by bylo neexistujicí a v hWnd by byla nenaplněná struktura.
Handle je jednoduše řečeno číslo (obvykle 32bitové), které odkazuje na nějaký objekt. Handle Windows je podobný souborovému handle z textového režimu. Takže když budeme volat nějakou funkci, většinou ji předáváme handle ojektu, kterého se týká.
Druhý parametr, typ UINT, je zkratkou pro unsigned int. Ve třetím a čtvrtém parametru jsou použity typy WPARAM a LPARAM.
K nim trošku teorie:
Když byly Windows 16bitové, byl třetí a čtvrtý parametr definován jako WORD, což je 16bitové celé číslo – unsigned short a čtvrtý parametr jako LONG, což je 32bitové celé číslo typu long se znaménkem. To je důvod předpony W a L před slovem PARAM.
Ve 32bitových Windows je samozřejmě WPARAM definován jako UINT a LPARAM jako LONG, což je stále long). Takže oba tyto parametry procedury okna mají 32bitové hodnoty.
Ale zajímavost je, že např. Ve Windows 98 byl WORD definován jako 16bitové číslo typu unsigned short, takže předpona W před PARAM je vlastně svým způsobem chybná, ale je to tak. Funkce WndProc vrací hodnotu typu LRESULT. Ta je definována jako LONG.
Funkce WinMain má přidělený typ WINAPI a funkce WndProc zase typ CALLBACK, což jsou oba dva typy definovýny jako typ __stdcall, který odkazuje na speciální posloupnost pro volání funkcí, jak jsme si říkali minule, které se uplatňuje mezi Windows a našemi programy. WinMain jsme si popisovali minule, tak se jí zde nebudeme již moc podrobně věnovat a půjdeme dál.
Máme definované pole typu TCHAR, ve kterém máme uložený řetězec názvu aplikace, který používáme hned na několika místech, ale samozřejmě pole definovat nemusíte a zadáváte řetězce přímo. Následuje identifikátor hWnd typu HWND, což je handle okna, jak jsme si už říkali. Na dalším řádku je definice proměnné msg typu MSG, což je struktura zprávy, o těch si ještě povíme. Následuje další struktura a to struktura třídy okna wc typu WNDCLASSEX. Následně hned nastavíme parametry struktury okna (wc). Nově vytvářené okno je vždy založené na nějaké třídě okna.
A co ta třída okna?
Třída okna určuje proceduru okna, která zpracovává zprávy určené oknu a některé další chrakteristiky okna, které jsou pro toto okno jedinečné. Na jedné třídě okna lze vytvořit samozřejmě více oken. Např. tlačítka, zaškrtávací políčka, a přepínací tlačítka jsou založena na stejné třídě okna.
Předtím, než se vytvoří okno aplikace, musí se tato třída zaregistrovat voláním funkce RegisterClassEx. Tato funkce si přebírá jediný parametr, a to ukazatel na strukturu WNDCLASSEX. A co se v této struktuře nastavuje a proč?
Jedná se o 12 položek, které určují základní vzhled a chování okna. Jsou to tyto:
cbSize = velikost objektu
style = styl okna
lpfnWndProc = ukazatel na proceduru okna
cbClsExtra = rezervovaný prostor pro třídu
cbWndExtra = ---------------//-----------------
hInstance = manipulátor instance
hIcon = ikona okna
hIconSm = ikona systémové nabídky
hCursor = kurzor okna
hbrBackground = barva pozadí
lpszMenuName = název menu okna
lpszClassName = název třídy okna (bývá stejný jako název aplikace)
V hlavičkovém souboru WINUSER.H je tato struktura definována takto:
typedef struct _WNDCLASSEX {
UINT cbsize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HICON hIconSm
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCSTR lpszMenuName;
LPCSTR lpszClassName;
} WNDCLASSEX;
Předpona: cb = count of bytes (počet bajtů) se často používá k vyjádření velikosti v bajtech.h = handle.
hbr = handle štětce (handle to brush) (o štětcích v dalších dílech).
lpsz = ukazatel long na řetězec ukončený nulou (long pointer to a string terminated with a zero).
K nejdůležitějším parametrům:
Do položky lpfnWndProc předáváme adresu procedury okna, která se pak používa pro okna založená na této třídě. V style se definuje styl okna, zde se říká, že bude okno překresleno při změně šířky nebo výšky okna. Jsou to dva parametry spojené bitovým operátorem OR (|). Nahlédněte do hlavičkového souboru WINUSER.H.
hIcon – zde se nastavuje ikona aplikace, resp. její handle pomocí funkce LoadIcon. Zde se nahrává standardní ikona. V příštím díle si ukážeme, jak nahrát vlastní ikonu, a tudíž se tímto budem zabývat podrobněji. hIconSm prozatím nepoužíváme, jinak to samé co hIcon.
hCursor – to samé, zatím nahráváme standardní kurzor funkcí LoadCursor.
hbrBackgound je také handle, handle štětce. Znamená, že bude pozadí okna celé bílé. Štětec je grafický termín, který odkazuje na některý barevný vzorek bodů použitých pro vyplnění oblasti. Windows používají jen několik standardních štětců.
Toto volání vrací handle bílého štětce. Zkuste experimentovat s barvou pozadí, buď zadávejte různá čísla nebo se koukněte do hlavičkového souboru, jak sou konstanty definovány.
Pozn.: Vy, kdo používáte VC++, tak můžete použít funkci GetStockObject(WHITE_BRUSH);. V Dev-C++ mi nechtěla fungovat.
hMenu zde máme prázdné, protože žádné menu zatím nemáme, proto hodnota NULL, příště to napravíme.
lpszClassName – jedná se o název třídy okna, může být jakýkoliv název, ale většinou se používá název aplikace, proto jsme použili proměnnou szAppName.
Když máme strukturu naplněnou, musíme zavolat funkci, která nám třídu zaregistruje, abychom ji mohli používat.
O to se stará funkce RegisterClassEx, které předáváme jako parametr adresu třídy okna. Po úspěšném zaregistrování třídy okna již můžeme okno samotné začít vytvářet.
Třída okna definuje obecné charakteristiky okna. K zadání podrobnějších vlastností okna musíme zavolat funkci, která nám to umožní a okno vytvoří. To dělá funkce CretaeWindowEx, která vrací manipulátor okna, podle kterého se na toto okno můžeme snadno odkazovat a ten se uloží do proměnné hWnd.
Pokud si říkáte, proč se všechny položky nevytvářejí přímo ve třídě, tak vám jednoduše odpovím.
Představte si, že byste vytvořili aplikaci a ta by byla MDI a chtěli byste vytvořit další okno, jenže aby mělo jinou velikost, otevřelo se na jiné pozici, mělo jiná systémová tlačítka. Kdyby úplně všechny položky byly přímo ve třídě okna, museli byjste pro kazdé další okno vytvářet další třídu a to je blbost. Takhle máme jen jednu třídu a upravíme si jen těch pár dodatečných položek. Viz. například definice tlačítek atd. Každé má jiný popis, jinou velikost, jsou na jiném místě, ale všechny vycházejí ze stejné třídy okna.
Tak tedy k parametrům:
První je NULL, definuje rozšířený styl okna, my budeme používat vždy NULL. Další parametr přebírá název třídy okna, takže zase szAppName nebo skutečný název třídy okna. Parametr třetí je také řetězec, ale který se objeví v záhlaví, předali jsme opět szAppName, ale můžete tam vepsat cokoliv potřebujete. Následuje styl okna, to znamená, jak bude okno zobrazeno – tento styl definuje, že bude okno zobrazeno s tlačítkem systémové nabídky, pruhem záhlaví, tenkým proměnným okrajem a tlačítka pro minimalizaci, maximalizaci a uzavření okna a také, že jde o normální překryvné okno – toto je standardní styl okna.
Když se kouknete do WINUSER.H, zjistíte, že tento styl je definován jako kombimace několika bitových příznaků.
#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED |
WS_CAPTION |
WS_SYSMENU |
WS_THICKFRAME |
WS_MINIMIZEBOX |
WS_MAXIMIZEBOX)
Na základě toho můžete experimentovat a přizpůsobit si vzhled okna podle svých představ, jen si to zkuste :-), ale bacha, pokud nezadáte identifikátor WS_SYSMENU, nebudete mít okno jak zavřít!
Následuje čtveřice parametrů, které definují pozici a velikost okna. První dva určují pozici okna. Umístění levého horního rohu okna k levému hornímu rohu obrazovky, a tedy vzdálenost rohu okna od rohu obrazovky v souřadnicích x-y. Pokud byste místo těchto parametrů použili 0,0, tak by bylo okno uplně nahoře vlevo. Parametr CW_USEDEFAULT říká, že bude použito standardní umístění okna.
Zbývajicí dva parametry určují jeho velokost. Taktéž nám hodnoty říkají, že bude použita standardní velikost, ale samozřejmě místo nich můžete použít přímo hodnotu rozlišení. Takže by to mohlo vypadat třeba takto:
0,
0,
640,
480,
…
Další parametr je handle rodičovského okna. Použili jsme NULL, protože vytváříme okno na nejvyšší úrovni. Kdybychom vytvářeli dceřiné okno, byla by hodnota handle rodičovského okna rovna právě handle tohoto okna. Takže, pokud existuje mezi dvěma okny vztah rodičovské-dceřiné okno, objeví se dceřiné okno na ploše okna rodičovského. Hlavní okno, které tvoříme, se zase objeví na ploše okna pracovní plochy. My však nemusíme zjišťovat handle okna pracovní plochy.
Další parametr je handle nabídky okna, tu zatím nemáme vytvořenou, tak bude také NULL. Parametr, co následuje, je handle instance programu a je nastaven na hInstance, který je předáván programu jako parametr funkce WinMain. A nakonec máme ukazatel, parametr pro vytvoření je nastaven na NULL, nepoužíváme ho, jinak ho můžete použít jako ukazatel na nějaká data, na která byste se mohli v programu odkazovat.
Nyní, když máme v proměnné hWnd handle okna, zde okna aplikace, můžeme toto okno konečně zobrazit.
Připomenutí: každé okno ve Windows má handle.
Náš program používá handle pro odkazování se na okno, kterého se daná operace týká a to znamená, že většina funkcí API požadují handle okna, aby Windows věděly, kterého okna se funkce týká. Pokud v programu vytvoříme více oken, každé má jiý handle. Handle okna je nejdůležitější handle, který program pro Windows spravuje.
V tuto chvíli, se nám vytvoří nové okno. To v podstatě znamená, že Windows alokovaly část paměti pro uložení informací o okně určené funkcí CreateWindowEx. A toto okno potřebujeme zorazit.
O to se postarají funkce ShowWindow a UpdateWindow.
Funkce ShowWindow přebírá 2 parametry. První je handle již vytvořeného okna a druhým je hodnota iCmdShow, což je parametr funkce WinMain. Tato hodnota určuje, jak má být okno poprvé zobrazeno na obrazovce (normálně, maximalizovaně, minimalizovaně).
Jedná se o číselné konstanty, které jsou ale definovány takto:
SW_SHOWNORMAL
SW_SHOWMAXIMIZED
SW_SHOWMINNOACTIVE
Zobrazí se nám okno a pokud je tedy z WinMain v iCmdShow předána hodnota SW_SHOWNORMAL, vyplní se jeho klientská oblast štětcem pozadí, v našem případě bílé barvy.
Následuje volání funkce UpdateWindow, která zařídí, že se klientská oblast vykreslí. Vlastně odešle zprávu WM_PAINT proceduře okna.
Teď už je okno vidět. Windows udržují pro každý program, který právě běží, takzvanou frontu zpráv. Když dojde k tomu, že uživatel něco udělá,Windows převede tuto událost na zprávu a uloží ji do fronty zpráv programu.
Program vybírá tyto zprávy pomocí kódu, kterému se říká smyčka zpráv. A to je vlastně ta poslední část kódu ve funkci WinMain.
Ač se to asi ještě nezdá, zpracování zpráv je v programu tou nejdůležitější činností a závisí na tom funkce celého programu. Pochopení zpráv Windows je asi to nejtěžší ve WinAPI. A na tom, jak zprávy pochopíte, závisí i to, co z programování ve Windows vytěžíte.
Proměnná msg je struktura MSG, která je definována v WINUSER.H. Funkce GetMessage na začátku smyčky vyzvedne zprávu z fronty zpráv. V prvním, parametru předá Windows ukazatel na strukturu msg typu MSG. Druhý, třetí a čtvrtý parametr je NULL nebo 0, protože program chce všechny zprávy pro všechna okna. Windows vyplní položky struktury podle dané zprávy. Položky této struktury jsou tyto:
hwnd – handle okna, kterému je zpráva směrována. V našem programu, jelikož má jen jedno okno, je to proměnná hWnd.
message – identifikátor zprávy. Je to číslo, které označuje zprávu. Pro každou zprávu je definován v hlavičkových souborech identifikátor (většinou WINUSER.H). Začíná písmeny WM (Windows Message).
wParam – 32bitový parametr zprávy, jehož význam a hodnota jsou závislé na konkrétní zprávě.
lParam – další 32bitový parametr zprávy také závislý na konkrétní zprávě.
time – čas, kdy byla zpráva umístěna do fronty.
pt – souřadnice myši v okamžiku, kdy byla zpráva umístěna do fronty.
Když je položka message vyzvednuta z fronty zpráv, jakákoliv, kromě WM_QUIT, funkce GetMessage vrátí nenulovou hodnotu. Zpráva WM_QUIT způsobí, že GetMessage vrátí 0.
Následující příkaz TranslateMessage přebírá ukazatel na strukturu zpráv a tu opět předá Windows. Kvůli dalším klávesnicovým převodům.
Příkaz DispatchMessage opět předá strukturu zpátky Windows. Windows potom zašlou zprávu ke zpracování příslušné proceduře okna.
Znamená to tedy, že Windows volají proceduru okna a ne náš program a to je základ zpráv Windows. To si musíte zapamatovat. Spoustu lidí to mate.
Poté, co procedura okna zpracuje zprávu, předá řízení zpět Windows, která stále zpracovávají volání DispatchMessage. Když potom Windows vrátí řízení hned za volání DispatchMessage, smyčka pokračuje dalším cyklem až do doby, kdy se vrátí 0 zpracováním WM_QUIT.
Uplně na konci vrátí WinMain parametr zprávy ze struktury msg.
A jéje, procedura okna a zprávy
Následuje definice procedury okna, o které jsme už několikrát mluvili. Jak jsme si řekli, tak právě v proceduře okna je definována funkčnost programu.
Tak se na to koukneme. Zatím toho náš program moc neumí, takže máme definovánu jen jednu zprávu, a to zprávu, která se postará o ukončení programu.
Všimněte si, že parametry procedury okna jsou shodné s položkami struktury zprávy. Obsahem procedury okna je přepínač switch, který porovnává parametr message a volá funkce podle příslušné hodnoty v case.
Takže když chceme ukončit program, např. klepneme na křížek systémové nabídky, program reaguje na zprávu WM_DESTROY voláním funkce PostQuitMessage. Tato funkce vloží zprávu WM_QUIT do fronty zpráv a GetMessage vrátí nulu, což způsobí, že WinMain opustí smyčku zpráv. Následuje proto tento příkaz: return msg.wParam, což v podstatě znamená, že return vrátí 0 a tím se ukončí program.
Na konci se vrací nezpracované zprávy (ty, které nechceme zpracovávat) zpět Windows funkcí DefWindowProc.
Se zprávami se budeme setkávat stále a tak si na ně časem zvyknete a hlavně si o nich ještě něco povíme později.
V příštím díle si povíme něco o zdrojích (resources) a tím si také vložíme vlastní ikonu a kurzor aplikace a zkusíme si vytvořit i vlastní místní nabídku (menu). Na základě těchto zkušeností už vytvoříme trošku inteligentnější a použitelný program a blíž si ukážeme práci se zprávami a jako perličku si ukážeme, jak ukončit program jen v případě odklepnutí výzvy.
Těšim se na příště :-).