Radek Chalupa Software na zakázkuŠkolení a konzultaceFreewareČlánkyMapa stránekKontakt
Software na zakázku Školení a konzultace Freeware Články Mapa stránek Kontakt

Učíme se Win API

Tento seriál jsem psal pro server builder.cz, kde vycházel od začátku roku 2002.Je tedy již staršího data, nicméně základní principy programováni pro Windows platí stále a budou platit i v budoucnu, dokud bude podporována zpětná kompatibilita.

Motivační úvod

Úvod do programování ve Win API

Základní stavební kameny Win32 programu

Seznámení s Unicode. Začínáne s GDI - výpis textu do okna.

Více o výpisu textu do okna - nastavení parametrů.

Jednorázové kreslení do okna. Kontext klientské oblasti.

Prostředky (resources) - úvod.

Hlavní nabídka okna.

Ovládací prvky Windows - úvod.

Použití časovače. Vzhled prvků v novém stylu Windows.

Dialogy

Ovládací prvky na dialogovém okně - úvod.

ListBox a ComboBox. Program založený na dialogovém okně.

Rozšiřujeme znalost GDI.

Kreslení čárových objektů a další typy per.

Kreslení plošných objektů. Štětce.

Nastavení písma. Fonty.

Bitmapa a její vykreslení.

Další funkce pro práci s grafikou.

Subclassing ovládacích prvků.

Výběr a načtení souboru.

Práce s registry

Ikona aplikace v oznamovací oblasti.

Zvuky a animace.

Přehrávání multimédií pomocí MCI.

Začínáme oživovat vzhled aplikace.

Uživatelsky kreslená tlačítka.

Uživatelsky kreslený ListBox.

Uživatelsky kreslený ListBox - II.

Výběr složky a naplnění ListBoxu soubory.

Vyhledávání souborů - zjištění obsahu složky.

Pracujeme s ComboBoxem.

Pracujeme s ComboBoxem - II.

Rozšířený prvek ComboBoxEx.

Ovládací prvek TrackBar.

Ovládací prvek ProgressBar.

Motivační úvod

Kdo si myslí, že naučit se (alespoň základy) Win API bude pro něj užitečné a zajímavé (a samozřejmě ještě Win API nezná) může mu tento seriál být zdrojem trochu opožděného novoročního předsevzetí, které poprvé v životě splní - "naučit se Win API". Takový jedinec může pár následujících řádek tohoto úvodu formálně jen formálně přelétnout a navázat na místě pro něj již zajímavějším. Pro ty ostatní, kteří váhají, zda má smysl "investovat" úsilí a čas do něčeho takového, jako je Win API, "v kterém dneska přece nikdo nepíše...". Dalo by se říci, API je na 2 věci... Na které? 1. V API lze napsat malý, relativně jednoduchý prográmek bez výrazné ztráty produktivity a na druhou stranu s trvalým ziskem jeho efektivnosti, především z hlediska minimalizace nároků na paměť a další systémové zdroje. Týká se to především programů běžících (většinou na pozadí) po celou dobu běhu operačního systému. A každý nemá doma počítač jako v NORADu a může pro něj být zajímavé, jestli pár takových "utilitek" na pozadí zabere každá řekněme o 1 MB více paměti plus další rozdíl ve využití ostatních systémových zdrojů proto, že k vytvoření prázdného okna je použita třeba VCL nebo MFC. 2. Druhý důvod, dnes pro většinu asi mnohem významější vás může napadnout při pročítání mnohých dotazů na diskusním fóru tohoto serveru. "Jak mám udělat tohle? ....žádná komponenta na to není. V property ani eventech jsem nic podobného nenašel?... A mnohdy jde o nějakou (pro API znalého) drobnost, jejíž znalost by tazateli ušetřila mnoho času hledání na Internetu, čekání na radu v diskusi a jeho aplikaci ušetřila pár dalších (kilo)bajtů kódu navíc, zataženého tam s nějakou nalezenou komponentou, které umí dalších 50 věcí navíc, které však v tomto okamžiku jsou v programu zcela zbytečné. A věřte tomu, že věcí které vizuálně nenaklikáte, je dost a dost. A mít nainstalovaných stovky komponent či ActiveX prvků jen kvůli tomu...to se raději zkusme naučit API, pro začátek alespoň základy. Než začneme nejdříve si řekněme něco o vhodných nástrojích. Odhlédněme od toho, že by nám stačil notepad, nějaký command-line win32 kompilátor a linker, a samozřejmě Windows SDK, tedy příslušné knihovny .lib, hlavičky .h atd. Z těch rozšířenějších vývojových prostředí je jednoznačně nejvhodnější Microsoft Visual C++ (zatím verze 6, brzy bude nová jako součást nového .Net Studia). Název  "Visual" totiž není od toho, že by se jednalo o RAD nástroj jako C++ Builder, Delhpi, Visual Basic apod. ale že má vizuální editor zdrojů (resources). Co jsou to zdroje (resources - nikoli zdrojový kód) si samozřejmě v tomto seriálu  řekneme. Zatím tolik, že použití tohoto vizuálního editoru neznamená sebemenší újmu na optimalizaci programu, což je samozřejmě velký rozdíl například oproti použití formulářů v Delhi. Tento editor vám prostě na základě vašeho grafického snažení vygeneruje textový soubor (tzv. resource script), který nijak úsporněji napsat nelze. Proto také neumí nic víc, než podporuje řekněme "jazyk resource skriptu". Tedy žádné nastavení barev tlačítka, fontu a pod. Ale udělá za vás tu práci s textových skládáním třeba dialogů "bez ztráty kytičky". Pro ty co používají VCL přirovnání - formulář můžete navrhnout také textově, napíšete prostě vlastní dfm soubor. Můžete samozřejmě celkem s úspěchem použít i C++ Builder. Zde však doporučuji pořídit si nějaký šikovný editor zdrojů. Výstupem může být jak běžný rc soubor (resource skript), tak tzv. kompilovaný soubor zdrojů (.res), který pak lze přidat snadno do projektu C++ Builderu. Rozdíl je v tom, že .res soubor je v podstatě binární soubor obsahující "vše v jednom", zatímco resource skript je textový soubor obsahující odkazy na soubory obsahující data některých typů zdrojů, jako např. ikony, bitmapy, která jsou binární a do textového souboru je samozřejmě nelze "zabalit". Tento soubor také musí projít kompilátorem, zatímco soubor .res většinou zpracovává až linker. Jinak C++ Builder obsahuje command-line kompilátor resource skriptů, takže .rc soubory vygenerované "něčím jiným" nebo napsané ručně lze pak snadno překládat. K editoru nyní již jen tolik, že C++ Builder do verze 4 (tuším od varianty Professional výše) obsahoval "Resource Workshop" od Borlandu. V době Windows 3.x výborný nástroj, dnes již zastaralý, ale stále snad použitelný. Ve verzi 5 ale již není. Bohužel, buď je to výsledek viditelné snahy Borlandu co nejvíce skrýt to základní co je pod povrchem, nebo Borland usoudil, že Resource Workshop opravdu již patří do muzea. Určitě je to škoda pro ty, kteří nechtějí krást a Visual C++ si nemohou dovolit. Ale opravdu i v C++ Builderu lze v API psát, tím spíše, že pokud se jedná o důsledek výše uvedeného důvodu číslo 2, není většinou problém s resource skripty. Další vývojové nástroje lze jistě použít ale jejich rozšíření je oproti dvěma zmíněným okrajové a ani je neznám z vlastní zkušenosti, proto se o nich nebudu zmiňovat. V posledním odstavci tohoto suchého teoretického úvodu si ještě řekněme pár "organizačních záležitostí". Pro tvorbu ukázkových příkladů budu používat Visual C++ verze 6. Všechny ukázkové aplikace budou psány v "čistém API" s použitím jazyka C/C++. Budu se snažit nepoužívat žádná C++ specifika Microsoftu. Ale nikdo není dokonalý a zvyk je zvyk, takže mě upozorněte pokud zdrojový kód nepůjde přeložit v C++ Builderu z uvedeného důvodu. Dále se budu všemožně snažit držet kompatibility s Windows SDK z doby vydání Visual C++ 6, tedy nepoužívat (alespoň na začátku, v případě zájmu samozřejmě dojde i na ně) funkce specifické pro Windows 2000 ("nedej bože" XP). I když jako motivaci předesílám, v tom novém, co je v SDK pro XP je mnoho zajímavého. Kdo může (tedy má pevnou linku nebo kabelový modem), doporučuji stáhnout poslední Windows SDK (listopad 2001) zde: http://www.microsoft.com/msdownload/platformsdk/sdkupdate/. V několika úvodních dílech (s výjimkou tohoto, příště si založíme nový projekt, a řekneme si při té příležitosti o nastaveních projektu) bude jako ukázková aplikace tentýž projekt postupně sofistikovaný tak jak se budeme učit jednotlivá základní témata. Aby nezůstalo jen u teorie. Slyšel jsem (možná zrovna v diskusi na Builderu :-)) názor, že lidé se nechtějí API učit také proto, že nemohou ihned (tj. během prvních cca 5 minut "výuky") napsat oblíbený program "Hello world". To je samozřejmě nesmysl. zde je kompletní zdrojový kód takového programu:
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
  MessageBox(NULL, "Ahoj Win API !!!","První program", MB_OK);
  return 0;
}
Výsledek po spuštění vidíte zde:

win_api_1

Tedy jako skutečný program k něčemu to zrovna nevypadá, že? Ale o tom až příště!

Úvod do programování ve Win API

V minulém, úvodním díle jsme se kromě teoretického úvodu dostali pouze k vytvoření té asi nejjednodušší Win32 aplikace, která pro zobrazení textu použila API funkci MessageBox. Dnes již se začneme (pomalu ale jistě) zabývat skutečnou aplikací. Skutečnou v tom smyslu, že bude mít alespoň jedno běžné okno, tzv. hlavní okno aplikace. A toto okno na rozdíl od uvedeného dialogu MessageBox, i přesto, že nebude modální, bude moci zůstat v systému tak dlouho, jak si budeme přát, tedy i poté co ztratí fokus. Což je pro každého samozřejmě jasná záležitost, ale leckdo neví proč tomu tak je a jak to funguje uvnitř. Nejdříve bychom si měli říci trochu teorie o tom, jak jsou vlastně programy ve Windows řízeny a ovládány. Pod pojem programy budu mít dále na mysli pouze běžné Win32 aplikace, tedy nikoli konzolové aplikace běžící v okně, nebo nějaké speciální služby. Je to samozřejmě ne zcela přesné zjednodušení, ale pro výklad ho použijme. Takže: pod Windows jde o tzv. "událostmi řízené programování". Jde o to, že o jakékoli události v systému jsou posílány jednotlivým oknům (nebo více oknům "současně") jedna nebo i více zpráv, které tuto událost nějak popisují. Takovou událostí je jakákoli "myší akce", stisk klávesy, uplynutí nějakého aplikací definovaného timeru a mnoho dalších typů zpráv. A tyto zprávy jsou pak doručovány oknu (nebo oknům), kterých se nějak dotýkají, to znamená, že toto okna na ně musí, nebo "může pokud chce", nějak reagovat. A reakce na tyto zprávy (události) je vlastně ono událostmi řízené programování. Významným rysem takového programu je, že je schopen čekat na podněty z nejrůznějších směrů. V DOSovém programu většina aplikací po většinu doby svého "života" prostě čekala na klávesnicový vstup uživatele. Na něj nějak zareagovala a opět se čekalo. Pokud jsme chtěli současně "být na příjmu" i z jiných směrů, museli jsem nějak periodicky "osahávat" ostatní porty, ze kterých by mohlo "něco přijít". To byla pro většinu programátorů poměrně složitá záležitost. Dále neexistoval multitasking, takže čekající program stále zabíral prakticky 100% výkonu procesoru a počítač byl jinak "k nevyužití". Nyní máme Windows s multitaskingem a událostmi řízeným programováním. Brzy se sami naučíte velice jednoduše to, jak zařídit abychom byli připraveni přijmou uživatelský vstup "kdykoli a kdekoli". Uživatel může tutéž akci vyvolat myší, horkou klávesou, z nabídky, když budeme chtít tak třeba i joystickem apod. A kromě toho bude náš program současně připraven reagovat na různé systémové události či změny, které se ho mohou nějak týkat. Je to například změna rozlišeni obrazovky, změna barevné palety, požadavek na ukončení Windows nebo odhlášení uživatele apod. Pro představu o množství zpráv si můžete spustit program Spy++, který je součástí instalace Visual C++, popř. WinSight, který je v C++ Builderu (nebo samozřejmě jiný podobný). Nastavte si monitorování všech zpráv všem oknům v systému. A stačí pohnout myší (půjde to hůře ten monitor samozřejmě bere velké procento výkonu procesoru) a pomaleji odbíhající seznam zpráv se rázem rozeběhne jak o závod. Během několika sekund budete mít tisíce, spíše desetitisíce zpráv. A i když dáte "ruce pryč od počítače", stále tam budou, i když podstatně pomaleji, běžet zprávy různých timerů a dalších systémových zpráv. K čemu nám taková zpráva je a co s ní? Jak údaje z ní získat se samozřejmě dozvíme, nyní se pro představu podívejme třeba na zprávu oznamující stlačení levého tlačítka myši. Tato zpráva má identifikátor WM_LBUTTONDOWN. Tyto identifikátory zpráv (většina těch běžných začíná na WM_ (Windows Message)) jsou definovány v hlavičkových souborech, ty běžné konkrétně ve winuser.h, který sám je "inkludován" v souboru "windows.h", což je základní hlavičkový soubor, který musí být v každém Win32 programu. Naše zpráva konkrétně je definovaná takto:
#define WM_LBUTTONDOWN 0x0201
Podívejme se nyní na tuto zprávu do dokumentace. Zjistíme, že tato zpráva, stejně jako všechny ostatní má dva parametry: wParam typu WPARAM a lParam typu LPARAM1). Oba tyto parametry jsou ve Win32 32-ti bitová celá čísla. Řeknete si možná, že do 2 čísel, byť 32-ti bitových moc informací nevměstnáme. Jak uvidíte, většinou to co nás v tom okamžiku bude zajímat, se tam vejde a pokud ne, tak u mnoha zpráv je lParam adresou nějaké další datové struktury, která již může být libovolně velká. Vraťme se k naší zprávě WM_LBUTTONDOWN. Dokumentace nám říká, že wParam je indikátor virtuální klávesy. Zde je třeba vědět, že onou virtuální klávesou může být také některé z tlačítek myši. Pokud by nás totiž zajímalo, zda v okamžiku události byla současně stlačena některá "speciální" klávesa či nějaké další tlačítko myši, můžeme to zjistit z tohoto parametru. Ten může totiž být kombinací následujících hodnot:
  • MK_CONTROL - je stlačena klávesa <Ctrl>
  • MK_LBUTTON- je stlačené levé tlačítko myši
  • MK_MBUTTON - je stlačené střední tlačítko myši
  • MK_RBUTTON -  je stlačené pravé tlačítko myši
  • MK_SHIFT - stlačena klávesa <Shift>
  • MK_XBUTTON1 - (Windows 2000 a výše) stlačen 1. tzv. x-button (další tlačítka např. na Microsoft IntelliMouse)
  • MK_XBUTTON2 - totéž s x-buttonem 2
Stejný význam parametru wParam je u všech "příbuzných myších zpráv", tedy například WM_LBUTTONUP (levé tlačítko bylo uvolněno) nebo WM_RBUTTONDOWN (pravé tlačítko bylo stlačeno). Nyní se podívejme na lParam, který nás bude asi zajímat ve většině případů. Nese informace o souřadnicích myši v okamžiku události (což nemusí být totožné s těmi souřadnicemi, na kterých je myš v okamžiku, kdy zprávu dostaneme, pokud je systém dostatečně vytížen tak může dojít k výrazné prodlevě). Konkrétně x-ová souřadnice je v dolním WORDu parametru lParam, y-ová zase v horním. Pro jejich "lidštější" získání máme k dispozici různá makra, například makro MAKEPOINTS, které můžeme použít takto:
POINTS ptMouse;
ptMouse = 
MAKEPOINTS(lParam);
int x = ptMouse.x;
int y = ptMouse.y
Trochu s předstihem ještě tolik, že často nastává situace, kdy (z hlediska uživatele) jedna událost vyvolá více zpráv. Jako příklad lze uvést uvolnění pravého tlačítka myši, při němž je poslána zpráva WM_RBUTTONUP a "současně" WM_CONTEXTMENU (tedy podnět k vyvolání kontextové nabídky). Ta "současnost" je samozřejmě v uvozovkách. Můžete si spustit již zmíněný program Spy++ či WinSight a zjistit, která z uvedených zpráv přijde dříve. Možná si řeknete: "Proboha, tolik zpráv, to musím myslet na všechno a všechno nějak ošetřovat?". Samozřejmě nikoliv. Ty stovky a tisíce zpráv, na které nechceme v programu nijak specificky reagovat, prostě necháme systému, ať se o ně postará. Příkladem může mýt třeba tažení okna za titulek. S nějakou implementací posunu okna v závislosti na myších zprávách v titulku si vůbec nebudeme dělat těžkou hlavu, pouze musíme příslušné zprávy "pustit" k "defaultnímu" zpracování, a Windows za nás udělá zbytek. Některým možná již připadá, že na úvod příliš teoretizuji a vykládám banální záležitosti. Ale tento seriál by měl být určen i těm, kteří třeba Windows prakticky neznají a chtějí problematiku pochopit. Většinou bývá lepší vědět jak a proč to tak funguje, než jenom jak to udělat. Když nevím v kterém parametru zprávy jsou souřadnice, podívám se do dokumentace, ale když neznám princip, budu jen bezmyšlenkovitě opisovat nějaké (snad) fungující ukázky kódu a doufat, že to nějak rozchodím. Ale nyní se již vrhneme i do praxe. Vytvoříme si nový projekt. Ve Visual C++ ("File -> New ->Projects") typu Win32 Application, dále zvolíme "prázdný projekt", vše si vytvoříme sami. V C++ Builderu zvolíme "File -> New -> "Console Wizard".

win_api_2

Zde odškrtneme volby Use VCL a "Console Application", jak vidíme na obrázku. C++ Builder vám nyní vygeneruje hlavní zdrojový soubor aplikace i s funkcí WinMain. Ve Visual C++ si vše přidáme sami. Přidejme si tedy do projektu ("Project -> AddTo Project -> New" -> C/C++ Header File") hlavičkový soubor, nazvaný třeba "main.h" a obdobně zdrojový soubor (C/C++ Source File) nazvaný main.cpp. Zde si řekněme o významu koncovky. Ve standardním nastavení platí, že pokud zdrojovému souboru dáme koncovku .c, kompilátor ho považuje za céčko bez rozšíření C++. Pokud použijeme koncovku .cpp, můžeme použít i jazyk C++. Já jsem použil tu možnost s rozšířením na jazyk C++, i když v tomto "základním API"  bychom si vystačili s jazykem C. Dovolíme si však ten luxus občas použít některé konstrukce specifické pro C++, jako pohodlnější definice proměnných uvnitř kódu apod. Navíc dnes již existuje jako součást Windows SDK sada tříd GDI+, které jsou vlastně také součástí Win API. Konkrétně jde o rozšíření grafického rozhraní GDI, které již podporuje formáty jako .jpg či .png a má i další "fajnovosti"... Ale my musíme nyní začít od základů. Nejdříve si do našeho hlavičkového souboru "main.h" přidáme základní hlavičkový soubor <windows.h>
#include <windows.h>
Do zdrojovéhp souboru "main.cpp" si vložíme "main.h" a  můžeme začít. Každý standardní program ve Windows musí mít jednu "hlavní" funkci, ve Windows nazvanou WinMain.V C++ Builderu ji již máte vygenerovanou. Ve Visual C++ si jí napíšeme takto.
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE 
hPrevInst, LPSTR lpCmdLine, int nShow)
{
  return 0;
}
Co znamenají jednotlivé parametry?
  • hInstance - Handle instance je 32-bitové číslo. Z pohledu programátora se na něj lze dívat jako na identifikaci kódu programu, neboť toto číslo je často potřeba zadat jako parametr různých funkcí. Ve skutečnosti představuje adresu počátku kódu, resp. exe souboru v paměťovém prostoru aplikace. Adresu kam je tento kód umístěn určuje v zásadě linker, a ve výchozím nastavení bude její hodnota pravděpodobně hexadecimálně 0x00400000.
  • hPrev - tento parametr je zde pouze pro zpětnou kompatibilitu zdrojového kódu s Win16 programy. Tehdy, pokud došlo k tomu, že stejný program (.exe soubor) byl spuštěn vícekrát, tak v tomto parametru byl uložen handle předchozí spuštěné instance programu, a my jsme mohli snadno na tuto skutečnost nějak reagovat. Nyní je tento parametr vždy NULL, což však vůbec neznamená, že nelze zjistit předchozí spuštění téže aplikace. Před časem jsem zde psal článek na toto téma, takže ti znalejší se mohou s tímto tématem obeznámit podrobněji.
  • lpCmdLine - ukazatel na "řetězec" obsahující parametry příkazové řádky
  • nShow - hodnota určující požadavek na způsob zobrazení hlavního okna aplikace. To znamená, zda se má spustit v normálním stavu, mnaximalizovaná, minimalizovaná apod. Podrobněji se k tomuto samozřejmě vrátíme.
Vzhledem k tomu, že vytvoření "skutečné aplikace" s vlastním oknem již není tak banální jako zavolání MessageBoxu z úvodního dílu, necháme si pokračování do příště. Na závěr ještě pár slov a typech definovaných ve Windows SDK. Když se podíváte na funkci WinMain, zjistíte, že je zde typ LPSTR. Ve skutečnosti jde o pouhý "typedef" znamenající v tomto případě CHAR*, tedy běžný ukazatel na "céčkovský řetězec". Název může být ze slov "Long Pointer (to) String". Podobných "typedefů" je ve Windows mnoho, proto se nezalekněte při pohledu na ně. K dalším specifickým typům ve Windows se ještě dostaneme. 1)Nakonec jen vysvětlení názvů typů parametrů zpráv, se kterými se budeme setkávat na každém kroku. Tyto názvy pocházejí ještě z doby 16-ti bitových Windows, kde wParam byl 16-ti bitový typu WORD, od toho wParam (= WORD Param) a lParam byl i tam 32-ti bitový (tedy LONG - odtud lParam).

Základní stavební kameny Win32 programu

V minulém článku jsme skončili tím, že jsme si řekli o hlavní funkci programu, tedy WinMain a řekli něco o významu jejích parametrů. Abychom dostali ten nejjednodušší kompletní program ve Windows, musíme si říci o 3 základních stavebních kamenech takového programu:
  • smyčka zpráv
  • registrace třídy okna a vytvoření okna
  • procedura okna a zpracování zpráv
Jak hned uvidíte, "všechno souvisí se vším", takže nelze dost dobře vysvětlit a pochopit jeden ze zmíněných bodů bez toho, abychom něco věděli o dalších. Začněme nejlépe tou smyčkou zpráv, která bývá běžně umístěna přímo uvnitř těla funkce WinMain.

Smyčka zpráv

Rozšiřme si do náš kód tak, jak vidíte na následujícím výpisu:
MSG msg;
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nShow)
{
  while ( GetMessage(&msg, NULL, 0, 0) )
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  return 0;
}
To co jsme právě přidali, je ona smyčka zpráv, ve které běží celý náš program. Funkce GetMessage vezme zprávu z fronty zpráv aplikace, a odešle jí k dalšímu zpracování, s jedinou výjimkou, kterou je zpráva WM_QUIT. Při přijetí této zprávy funkce GetMessage vrátí FALSE a smyčka zpráv je tak ukončena a posléze dojde k ukončení celé funkce WinMain, a tedy celé aplikace. V prvním parametru funkce GetMessage je adresa struktury MSG, obsahující informace o zprávě vyjmuté z fronty zpráv aplikace:
typedef struct tagMSG
{
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG, *PMSG;
Prvek hwnd je handle okna, kterému je zpráva směrována. V této smyčce zpráv se totiž soustřeďují zprávy určené všem oknům naší aplikace. Prvek message je identifikátor zprávy, například minule zmíněný WM_LBUTTONDOWN. Dále následují již zmíněné parametry wParam a lParam, time je pak čas, kdy byla zpráva vložena do fronty a v pt je pozice kurzoru v absolutních souřadnicích obrazovky. Druhým parametrem funkce GetMessage je handle okna jehož zprávy má tato funkce vyjmout z fronty. V případě použití této funkce ve smyčce zpráv aplikace zde zadáme hodnotu NULL znamenající, že mají být vybírány zprávy všem oknům aplikace. Další dva parametry funkce GetMessage mohou představovat filtr zpráv. Můžeme tak specifikovat, že chceme vybírat pouze zprávy v určitém rozsahu identifikátorů. Ve smyčce zpráv opět zadáme tyto parametry nulové, což znamená, že chceme vybírat všechny zprávy Ve smyčce zpráv dále vidíte funkci TranslateMessage. Tato funkce "překládá" virtuální kódy zpráv klávesnice a generuje další "znakové" zprávy. Podrobněji se samozřejmě budeme zprávám klávesnice věnovat později, nyní není pro nás podrobné vysvětlení důležité. Následuje funkce DispatchMessage, která zprávu odešle do procedury příslušného okna, o které si řekneme později. Další základní věcí Win32 programu je registrace třídy okna a vytvoření okna této třídy. Třídy definují určité vlastnosti, které jsou společné všem oknům, které k této třídě náleží. Některé z těchto vlastností můžeme později u individuálního okna změnit, buď hned při jeho vytvoření nebo později za běhu programu. Existují některé systémové třídy, které nemusíme (a ani nemůžeme) v aplikaci registrovat, a můžeme ihned vytvořit okno patřící k takové systémové třídě. Možná vás již napadlo, že těmito třídami jsou například standardní prvky Windows, jako button, list-box, edit-box apod. Každá třída, jak systémová tak registrovaná aplikací má nějaké jméno ("řetězcová" hodnota), na které se odkazujeme při vytvoření okna patřícího této třídě.

Registrace třídy okna

Jak tedy zaregistrovat třídu okna? K registraci třídy okna použijeme funkci RegisterClassEx, jejímž parametrem je adresa struktury WNDCLASSEX obsahující požadované vlastnosti registrované třídy.
ATOM RegisterClassEx(CONST WNDCLASSEX *lpwcx) // data třídy
typedef struct _WNDCLASSEX
{
  UINT cbSize;
  UINT style;
  WNDPROC lpfnWndProc;
  int cbClsExtra;
  int cbWndExtra;
  HINSTANCE hInstance;
  HICON hIcon;
  HCURSOR hCursor;
  HBRUSH hbrBackground;
  LPCTSTR lpszMenuName;
  LPCTSTR lpszClassName;
  HICON hIconSm;
} WNDCLASSEX, *PWNDCLASSEX;
Nemělo by nyní asi smyl podrobně rozebírat význam jednotlivých parametrů. Řekneme si o těch základních a ukážeme si nejjednodušší příklad jejich naplnění. cbSize - je velikost této struktury. Musíme ji naplnit hodnotou sizeof(WNDCLASSEX) style - můžeme uvést kombinaci stylů třídy. Kompletní seznam možných parametrů naleznete v dokumentaci. Běžně se používá kombinace hodnot CS_VREDRAW (obsah celého okna se překreslí, pokud dojde ke změně výšky okna, tedy k jeho roztažení) a CS_HREDRAW (totéž pouze s tím, že jde o změnu šířky). lpfnWndProc - adresa procedury okna. O proceduře okna si řekneme později, zatím tolik, že zde jednoduše uvedeme název procedury okna kterou musíme mít v programu vytvořenu. cbClsExtra - zde můžeme v některých speciálních případech uvést počet bytů, které chceme alokovat spolu s třídou pro použití v některých speciálních případech. Běžně zadáme 0. cbWndExtra - obdobně můžeme alokovat dalších počet bytů spolu s instancí programu. Používá se opět v některých speciálních případech. hInstance - handle instance, které nám systém přidělí v 1. parametru (typu HINSTANCE) funkce WinMain. hIcon - handle ikony příslušné třídy okna, běžně získané pomocí funkce LoadIcon či LoadImage. V tom nejjednodušším případě můžeme použít některou ze systémových ikon, nejlépe tu symbolizující aplikaci. Získáme ji takto: LoadIcon(NULL, IDI_APPLICATION) hCursor - handle kurzoru, který bude zobrazen, pokud se ukazatel myši přesune do oblasti okna patřícího k této třídě. Systémový kurzor "šipka" získáme takto: LoadCursor(NULL, ICD_ARROW) hbrBackground - štětec typu HBRUSH definující pozadí, které bude vyplňovat okno této třídy. V nejjednodušším případě můžeme získat handle stětce definováním příslušné systémové barvy, například (HBRUSH)(COLOR_WINDOW+1) nám dá štětec v barvě nastavené jako barva pozadí oken. lpszMenuName - hlavní menu okna. Můžeme zatím nechat NULL, později si ukážeme, jak používat menu. lpszClassName - jméno identifikující třídu. Musí být jednoznačné v celém systému. hIconSm - handle malé ikony (obvykle 16x16) reprezentující třídu. Pokud uvedeme NULL, systém použije "zmenšenou" ikonu specifikovanou v prvku hIcon. Nyní si dejme vše dohromady a ukažme si registraci třídy. V naší aplikaci si nejprve vytvoříme funkci InitApp typu BOOL, do které budeme postupně přidávat jednotlivé kroky, prováděné při inicializaci aplikace, počínaje nyní registrací třídy. Před tím si nadefinujeme jméno třídy, které budeme potřebovat na více místech, proměnnou pro uložení handle instance, který budeme často v kódu potřebovat, dále handle hlavního okna aplikace:
#define _MainClassName "TohleExistovatNebude"
HINSTANCE g_hInstance
HWND g_hwndMain
Ten prefix g_ jsem zvyklý dávat u "globálních" proměnných, které pak na první pohled odliším od těch použitím uvnitř funkcí. Podobně hodnoty definované pomocí "#define", popř. konstanty označuji s podtržítkem na začátku, takže je na první pohled odliším od proměnných. Vlastní "rukopis" si samozřejmě každý vytvoří sám. A nyní již můžeme zaregistrovat třídu
BOOL InitApp()
{
  WNDCLASSEX wc;
  wc.cbSize = sizeof(WNDCLASSEX);
  wc.cbClsExtra = 0;
  wc.cbWndExtra = 0;
  wc.hbrBackground =  (HBRUSH)(COLOR_WINDOW +  1);
  wc.hCursor = LoadCursor(NULL,  IDC_ARROW);
  wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
  wc.hInstance = g_hInstance;
  wc.lpfnWndProc = WindowProcMain;
  wc.lpszClassName = _MainClassName;
  wc.lpszMenuName = NULL;
  wc.style = CS_HREDRAW | CS_VREDRAW;
  if ( (!RegisterClassEx(&wc))
    return FALSE;
}

Vytvoření okna

Nyní máme zaregistrovanou třídu, takže můžeme vytvořit hlavní (a zatím jediné) okno patřící této třídě. K tomu použijeme funkci CreateWindowEx.
// Toto bude ve funkci InitApp
g_hwndMain = CreateWindowEx(0, // rozšířený styl okna
  _MainClassName, // jméno třídy
  "Moje první nealkoholické okno", // text okna
  WS_OVERLAPPEDWINDOW, // styl okna
  100, 100, // souřadnice na obraziovce
  450, 350, // rozměry - šířka a výška
  (HWND)NULL, // okna předka
  (HMENU)NULL, // handle hlavní nabídky
  g_hInstance, // handle instance
  NULL); // další "uživatelská" data
  if (g_hwndMain == NULL)
   return FALSE;
Ještě dodám, že proměnnou g_hInstance si nastavíme hned na začátku funkce WinMain na hodnotu prvního parametru (hInstance) této funkce. Příště si podrobněji vysvětlíme význam jednotlivých parametrů funkce CreateWindowEx, vytvoříme proceduru okna a konečně budeme moci naši první Win API aplikaci spustit.

Seznámení s Unicode. Začínáne s GDI - výpis textu do okna.

V minulém díle jsme vytvořili tu nejjednodušší Win32 aplikaci, která má hlavní okno se všemi základními náležitostmi. Nejprve si řekněme alespoň to základní o používání Unicode. I když tato problematika nepatří mezi nezbytně nutné základy Win API, zmíním se o ní z toho důvodu, že jsem v kódu použil makro TEXT a také jsme již narazili na typ TCHAR, přesněji řečeno na ukazatel na tento typ - LPTSTR. V další části článku se pak již budeme zcela věnovat úvodu do GDI - tedy kreslení (mezi které z hlediska GDI patří i výpis textů) na plochu okna.

Pár slov o UNICODE

Unicode jednoduše řečeno používá znaky o rozměru 2 byte. Do takové znakové tabulky se pak vejde 256 * 256 znaků, tedy 65536 různých znaků, takže jedna taková znaková tabulka může obsahovat nejrůznější národní sady znaků. Při použití Unicode pak musíme pro znak použít typ WCHAR, tedy onen 16-ti bitový (2 byte) znak. Vzhledem k tomu, že v praxi často chceme, abychom měli univerzální zdrojový kód, z kterého pak nějakým jednoduchým nastavením dostaneme verzi programu pro  Unicode, máme k dispozici typ TCHAR. Tento typ je definován v závislosti na tom, zda je definována hodnota UNICODE. Pokud je hodnota UNICODE definována, je (pomocí typedef) TCHAR definován jako WCHAR, v opačném případě jako CHAR. Typ CHAR je opět pomocí typedef ve Windows definovaný jako standardní céčkovský typ char. V následujícím výpisu vidíte "proškrtanou" část souboru <WinNT.h>, která je sám, spolu s mnoha dalšímu hlavičkovými soubory, vložený do souboru <windows.h>:

typedef char CHAR;
typedef wchar_t WCHAR;
#ifdef UNICODE
typedef WCHAR TCHAR, *PTCHAR;
#else
typedef char TCHAR, *PTCHAR;
#endif
Pokud jde o použití konstantních řetězců, k jejich správné interpretaci podle nastavení Unicode nám slouží makro TEXT
TEXT(
  LPTSTR string  // ANSI or Unicode string
);
Toto makro identifikuje text jako Unicode, pokud je definována hodnota UNICODE, v opačném případě jako ANSI řetězec. Chceme-li tedy náš kód mít bez problémů přeložitelný i v případě definovaného Unicode, musíme toto makro důsledně používat. Příkladem v našem kódu je definice
#define _AppName TEXT("Učíme se WinAPI")
Tolik prozatím tento velmi stručný úvod do Unicode a nyní se již dáme do kreslení.

Začínáme kreslit - úvod do GDI

Aplikace ve Windows nemají běžným způsobem možnost přímého přístupu ke grafickému hardware. Proto zde existuje jakási mezivrstva, která bezpečným způsobem zprostředkovává tuto komunikaci s grafickým zařízením. Samozřejmě kromě GDI již existují další specializovaná rozhraní, jako je převážně "herní" DirectX nebo OpenGL. Ale jak asi všichni víte, aplikace používající některé z těchto rozhraní se více či méně odlišují od běžných aplikací ve Windows. Například tím, že hlavně hry většinou běží ve výhradním, celoobrazovkovém režimu, což samozřejmě není pro běžnou aplikaci vhodné. Na druhou stranu, tyto rozhraní poskytují mnohem větší rychlost, která je v případě takových aplikací většinou jedním z klíčových faktorů. My však zůstaneme u GDI, na kterém je zatím stále velká většina programů pro Windows. I když v současné době je již asi nutné se zmínit o GDI+, což je nové rozhraní, již objektové (tedy založené na třídách C++), které bylo uvedeno v souvislosti s nástupem Windows XP, v jejichž instalaci je již runtimová podpora těchto funkcí. Ale i pro starší verze Windows je možné zdarma získat tuto runtimovou podporu, takže aplikace používající GDI+ mohou pak běžet třeba pod Windows 98. Myslím ale že je žádoucí zvládnout nejdříve GDI, již proto, že pro použití GDI+ je třeba mít instalovanou jednu z nejnovějších verzí Windows SDK (Software Development Kit), což pro velkou část programátorů, kteří se k Internetu připojují přes telefon, zůstává prakticky nedostižným snem. Takže nyní již dále jen o GDI. Cenou za zmíněné zprostředkování přístupu ke grafickému hardware je samozřejmě rychlost. A výhody? Především bezpečnost a univerzálnost. Bezpečností mám na mysli to, že se nám nemůže stát, že bychom nějakou chybou pomocí GDI funkcí "sáhli někam, kam nesmíme". Je to dáno tím, že nemáme přímý přístup do paměti grafického adaptéru, jako tomu bylo v DOSu, což bylo velice nebezpečné, stačila malá chyba a .... Další výhodou je univerzálnost. Znamená to asi tolik, že tytéž funkce, které použijeme pro kreslení na obrazovku, budeme používat i při výstupu na tiskárnu nebo i jiné zařízení. Základním pojmem v GDI je tzv. kontext zařízení (device context).Kontext zařízení je jakási struktura, obsahující informace o konkrétním zařízením (jako je například barevná hloubka). Toto obhospodařuje GDI. Aplikace musí pouze pomocí příslušných funkcí získat handle tohoto kontextu zařízení a tento handle pak použít ve všech kreslících funkcích. Po jeho použití je nutné ho opět uvolnit, neboť jeho alokace je poměrně náročná na systémové zdroje a je proto žádoucí mít kontext zařízení "přivlastněn" na dobu minimálně nutnou. Tento handle nám samozřejmě kromě kreslení umožňuje získat nejrůznější informace o kontextu zařízení, a především na něj aplikovat různé vlastnosti, jako je barva textu, pozadí, barvy čar, písmo apod. Slovo vlastnosti není vždy zcela přesné (použil jsem ho zde pro zjednodušení), neboť některé z těchto "vlastností" jsou realizovány výběrem příslušných grafických objektů (nejde o objekty ve smyslu tříd C++), jako jsou pera, štětce apod. Ale k tomu se dostaneme postupně. Nyní se ale již vrhneme na kreslení do našeho okna. V té souvislosti si musíme říci o zprávě WM_PAINT a způsobu, jak je ve Windows realizováno překreslování oken. Vzhledem k tomu, že ve Windows dochází často k tomu, že jednotlivá okna jsou překryta zcela nebo zčásti jiným oknem a následně opět "zviditelněna", je samozřejmě třeba při tomto zviditelnění okna nebo jeho části toto okno ihned překreslit. O tom, že nastala potřeba překreslení okna informuje příslušnou aplikaci systém Windows tím, že pošle zprávu WM_PAINT do procedury příslušného okna. Ještě přesněji řečeno, před zprávou WM_PAINT je poslána ještě zpráva WM_ERASEBKGND, která je oknu poslána, když má být překresleno jeho pozadí. O výplň pozadí se nám postará systém (pokud mu to nezakážeme) tím způsobem, že pozadí okna vyplní štětcem, přiřazeným dané třídě oken, jak jsme si naznačili v 3. díle seriálu. Co je to přesně štětec si řekneme později. Na nás tedy je zachytit zprávu WM_PAINT a reagovat na ní překreslením okna nebo jeho části tak jak chceme, aby v daném okamžiku vypadalo. V tomto díle si ukážeme, jak realizovat výpis textu do okna. V proceduře okna si zachytíme zprávu WM_PAINT a zavoláme si naši funkci, ve které budeme realizovat vlastní kreslení. Podívejme se nejprve na hotový kód a pak si jej vysvětleme:
void OnWM_PAINT()
{
  PAINTSTRUCT ps;
  HDC hdc;
  TCHAR chText[] = TEXT("Ahoj Win API !");
  hdc = BeginPaint(g_hwndMain, &ps);
  TextOut(hdc, 10, 10, chText, lstrlen(chText));
  EndPaint(g_hwndMain, &ps);
}
LRESULT CALLBACK WindowProcMain(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
  switch ( message )
  {
    case WM_PAINT:
      OnWM_PAINT();
      break;
    case WM_DESTROY:
      PostQuitMessage(0);
      break;
  }
  return DefWindowProc(hwnd, message, wParam, lParam);
}
a výsledek po spuštění:

win api 5

V případě že kreslíme v reakci na zprávu WM_PAINT, používáme k získání kontextu zařízení funkci BeginPaint a k jeho uvolnění pak EndPaint. Funkce BeginPaint má jako první parametr handle okna, jehož kontext zařízení (přesněji nikoli celého okna, ale pouze jeho klientské oblasti) chceme získat. Druhým parametrem je adresa struktury PAINTSTRUCT, o které zatím jen tolik, že obsahuje informace, které můžeme využít v některých speciálních případech. Obvykle ji můžeme ignorovat, jako je tomu v našem případě. Když máme handle kontextu zařízení, můžeme použít tu asi nejjednodušší GDI funkci, kterou je TextOut.
BOOL TextOut(
  HDC hdc,           // handle kontextu zařízení
  int nXStart,       // x-ová souřadnice
  int nYStart,       // y-ová souřadnice
  LPCTSTR lpString,  // textový řetězec
  int cbString       // počet znaků
);
Tato funkce vypíše na místo určené souřadnicemi nXStart a nYStart text lpString, resp. tolik znaků. kolik určíme parametrem cbString. Délku celého textu určíme funkcí lstrlen, což je z hlediska Unicode "univerzální" varianta funkce strlen. Doprovodný projekt (Visual C++ 6) je ke stažení zde: win_api_5.zip

Více o výpisu textu do okna - nastavení parametrů.

Minulé pokračování jsme ukončili tím, že jsme si tím nejjednodušším způsobem vypsali do okna text tak, aby zůstal na svém místě po celou dobu běhu programu. V té souvislosti jsme si řekli obecně jak funguje překreslování okna v reakci na zprávu WM_PAINT. V tomto pokračování se naučíme o trochu více o funkcích GDI, tedy kreslení do okna, přičemž jak jsem již uvedl kreslením budu mít nadále na mysli také výstup textu. Nejdříve si řekneme o další funkci pro výpis textu, kterou je DrawText:
int DrawText(
  HDC hDC,          // handle  kontextu zařízení
  LPCTSTR lpString, // text
  int nCount,       // délka textu
  LPRECT lpRect,    // obdélník
  UINT uFormat      // volby
);
Tato funkce se liší od funkce TextOut především tím, že cílové souřadnice nejsou určeny souřadnicemi počátku textu, ale obdélníkem (struktura RECT), do kterého je text formátován způsobem určeným parametrem uFormat. Navíc je zde drobná výhoda v tom, že tato funkce akceptuje jako parametr nCount (délka textu) hodnotu -1, při které si sama spočítá celkovou délku zadaného textu a správně jej vypíše. Řekněme si o některých běžně používaných hodnotách, ze kterých se skládá parametr uFormat:
  • DT_CENTER - vystředí text horizontálně
  • DT_LEFT -  zarovná text horizontálně vlevo
  • DT_RIGHT - zarovná text horizontálně vpravo
  • DT_SINGLELINE - vypíše text do jedné řádky i kdyby obsahoval znaky konce řádků. Tento parametr je dále významný proto, že následující formátovací volby fungují pouze za současného použití DT_SINGLELINE.
  • DT_VCENTER - vystředí text vertikálně
  • DT_BOTTOM - zarovná text ke spodnímu okraji obdélníka lpRect
Další formátovací parametry umožňují s textem dělat ještě více, zatím je zde nebudu vysvětlovat, zájemci je samozřejmě naleznou v dokumentaci. Ukážeme si tedy, jak dosáhnout toho, aby náš text byl stále zobrazován uprostřed okna. Přesně řečeno uprostřed obdélníka jeho klientského oblasti. Pro lepší budoucí přehlednost našeho kódu, který budeme postupně rozšiřovat, si budeme pro jednotlivé postupy vytvářet vlastní funkce s parametrem HDC získaným v handleru zprávy WM_PAINT, z kterého tyto funkce budeme volat, případně jejich volání "zapoznámkujeme" pro lepší přehlednost výsledného výstupu. Vytvoříme si tedy funkci pro zmíněné vycentrování textu:
void centerText(HDC hdc)
{
  RECT rect;
  GetClientRect(g_hwndMain, &rect);
  DrawText(hdc, "Text uprostřed okna", -1, &rect,
    DT_SINGLELINE | DT_CENTER | DT_VCENTER);
 }
Tuto funkci si zavoláme z handleru zprávy WM_PAINT, který jsme si vytvořili již minule:
void OnWM_PAINT()
{
  PAINTSTRUCT ps;
  HDC hdc;
  hdc =  BeginPaint(g_hwndMain, &ps);
  centerText(hdc);
  EndPaint(g_hwndMain, &ps);
}
Když nyní program spustíme, text bude uprostřed okna. Když budeme okno roztahovat za okraj, text se bude automaticky překreslovat tak aby byl stále uprostřed. Ale pozor! Automatické překreslování textu, resp. celé klientské oblasti okna je závislé na nastavení stylu třídy okna. Když se podíváte do funkce InitApp, kde jsme registrovali třídu, najdete tam tento řádek:
wc.style = CS_HREDRAW | CS_VREDRAW;
Tyto vlastnosti ve stylu třídy určují, jak jejich název napovídá, zda se při horizontální či vertikální změně velikosti okna má překreslit celá klientská oblast. Když si zkusíte hodnotu wc.style nastavit na 0, zjistíte, že při roztažení okna zůstane text stále na místě do té doby, než bude jeho překreslení vyvoláno "standardním" způsobem, tedy například, skrytím a znovuodkrytím okna. Nyní provedeme změnu v nastavení pozadí okna. Nastavíme jeho barvu tak, aby odpovídala systémové barvě prostorových prvků. Změníme proto jeden řádek ve funkci InitApp takto:
wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
Po spuštění programu zjistíme, že již to není to pravé ořechové.

win-api-6-1

Bílý podklad textu nepůsobí příliš esteticky. Bývá zvykem, že texty se vypisují jako průhledné s podkladem odpovídajícím barvě, nebo i vzorku okna. Musíme se tedy naučit nastavovat barevné parametry textového výstupu. Je důležité vědět, že následující funkce, kterými se budeme zabývat, nastaví příslušnou vlastnost kontextu zařízení a nikoli textu nebo jen některé funkci. Znamená to, že po nastavení některé z vlastností bude tato vlastnost aplikována na všechny následné volání funkcí pro výstup textu (zatím používáme 2 - TextOut a DrawText). Na druhou stranu toto nastavení se "ztratí", jakmile ukončíme kreslení funkcí EndPaint. Nyní již ke třem základním funkcím, nastavujícím barvy textového výstupu. Nastavení barvy písma:
COLORREF SetTextColor(
  HDC hdc,         // handle kontextu zařízení
  COLORREF crColor // barva textu
);
Nastavení barvy pozadí textu:
COLORREF SetBkColor(
  HDC hdc,         // handle  kontextu zařízení
  COLORREF crColor // barva pozadí
);
Nastavení průhlednosti textu:
COLORREF SetBkMode(
  HDC hdc,        // handle  kontextu zařízení
  int iBkMode     // průhlednost
);
Hodnota crColor použitá u prvních 2 funkcí je 32-bitové číslo, v jehož nejnižší BYTE obsahuje hodnotu červené složky barvy, následující zelené a modré. Nejvyšší BYTE musí být 0. V tomto seriálu se postupně dostaneme k funkcím, resp. makrům, umožňujícím pohodlnější práci s typem COLORREF. Běžně se požívá hexadecimální zápis ve tvaru 0x00BBGGRR, kde BB, GG a RR jsou ony barevné složky v rozsahu 0x00 - 0xFF. Parametr iBkMode ve funkci SetBkMode může nabývat 2 předdefinovaných hodnot:
  • OPAQUE - pozadí textu je vyplněno barvou nastavenou funkcí SetBkColor.
  • TRANSPARENT - průhledný text, pozadí textu je tvořeno pozadím okna.
Tímto jsme se tedy dostali k odpovědi na otázku, jak vypsat text průhledný, jak to bývá zvykem a jak to i lépe vypadá. Ukažme si tedy na závěr praktickou realizaci. Nejprve se podívejte na výsledek:

win-api-6-2

A takto vypadá zdrojový kód funkce, kterou si zavoláme z funkce OnWM_PAINT:
void pozadiTextu(HDC hdc)
{
  TCHAR chText[100];
  lstrcpy(chText, TEXT("Modrá barva písma..."));
  SetTextColor(hdc, 0x00FF0000); // modrý text
  TextOut(hdc, 10, 10, chText, lstrlen(chText));
  SetBkColor(hdc, 0x0000FFFF); // žluté pozadí
  lstrcpy(chText, TEXT("Změníme pozadí, písmo zůstává..."));
  TextOut(hdc, 10, 40, chText, lstrlen(chText));
  lstrcpy(chText, TEXT("Konečně máme průhledný text !!!"));
  SetBkMode(hdc, TRANSPARENT); // průhlednost textu
  TextOut(hdc, 10, 70, chText, lstrlen(chText));
  lstrcpy(chText, TEXT("Pokračování příště ..."));
  SetTextColor(hdc, 0x000000FF); // modrý text
  TextOut(hdc, 10, 110, chText, lstrlen(chText));
}
Doprovodný projekt (Visual C++ 6) je ke stažení zde: win_api_6.zip

Jednorázové kreslení do okna. Kontext klientské oblasti.

Minulý článek jsme zakončili ukázkou výpisu textu do okna s možností volby barvy písma, barvy pozadí textu a volby průhlednosti. Na závěr jsem také položil "kontrolní otázku", jak v uvedeném případě lze zabránit problikávání textu při roztahování okna za okraj? Řešením je nastavení stylu třídy okna. Ukázali jsem si, že pokud chceme mít text (vypisovaný funkcí DrawText) stále vycentrovaný, musíme zajistit překreslování okna při každé změně jeho rozměru. K tomu slouží příznaky CS_HREDRAW a CS_VREDRAW v prvku style struktury WNDCLASSEX. Když tyto odstraníme, text se nebude překreslovat během roztahování. Je samozřejmě vždy třeba uvážit, co je v konkrétním případě výhodnější. V tomto pokračování si ukážeme, jak kreslit do okna jednorázově, tj. nezávisle na systémově řízeném překreslování. Naučíme se také, jak získat kontext zařízení celého okna, když budeme chtít kreslit do neklientské oblasti, třeba do titulkového pruhu okna. Řekněme že budeme chtít, aby při stisknutí levého tlačítka myši byly v daném místě okna vypsány souřadnice místa, ke bylo tlačítko stisknuto. Navíc při současném držení klávesy <Control> bude text v červené barvě. Tento text pak při dalším překreslení okna zmizí. Vytvoříme si tedy v proceduře okna handler zprávy WM_LBUTTONDOWN. Když se podíváme do dokumentace, zjistíme že zpráva WM_LBUTTONDOWN má ve svém parametru lParam souřadnice místa, kde k události došlo. Tyto souřadnice jsou relativné ke klientské oblasti okna, tedy bod (0,0) je v levém horním rohu klientské oblasti. Další parametr této zprávy - wParam - pak určuje, zda některá z tzv. virtuálních kláves je současně stlačena. Touto klávesou může být <Control>, <Shift> nebo některé z dalších tlačítek myši, včetně tzv. x-buttonů, což jsou další tlačítka na některých myších (např. Microsoft IntelliMouse), standardně s funkcí "zpět" a "vpřed". Možné hodnoty tedy jsou:

  • MK_CONTROL - stlačena klávesa <Ctrl>
  • MK_SHIFT - stlačena klávesa <Shift>
  • MK_LBUTTON - stlačeno levé tlačítko myši
  • MK_RBUTTON - stlačeno pravé tlačítko myši
  • MK_MBUTTON - stlačeno prostřední tlačítko myši
  • MK_XBUTTON1 - stlačeno 1. doplňkové tlačítko (pouze ve Windows 2000/XP)
  • MK_XBUTTON2 - stlačeno 2. doplňkové tlačítko (pouze ve Windows 2000/XP)

Přidáme si tedy vlastní funkci, které z procedury okna předáme zmíněné 2 parametry. Podívejme se na výsledek a její kompletní tvar a vysvětleme podrobnosti:

win-api-7-1

void VlastniKresleni(WPARAM wParam, LPARAM lParam)
{
  POINTS ptCursor;
  HDC hdc;
  TCHAR chText[30];
  ptCursor = MAKEPOINTS(lParam);
  _stprintf(chText, "x=%d, y=%d", ptCursor.x, ptCursor.y);
  hdc = GetDC(g_hwndMain);
  if ( wParam & MK_CONTROL )
    SetTextColor(hdc, 0x000000FF);
  TextOut(hdc, ptCursor.x, ptCursor.y, chText, lstrlen(chText));
  ReleaseDC(g_hwndMain, hdc);
}

K získání souřadnic z parametru lParam zprávy WM_LBUTTONDOWN použijeme makro MAKEPOINTS, které nám vrátí strukturu POINTS obsahující 2 souřadnice.

typedef struct tagPOINTS { 
  SHORT x; 
  SHORT y; 
} POINTS, *PPOINTS;

Samozřejmě, toto makro bychom nemuseli použít, neboť souřadnice jsou uloženy ve 32 ti bitovém parametru tak, že x-ová souřadnice je v dolním WORDu a y-ová v horním. Člověk je však tvor chybující (a někdy i líný) a snadno zamění x-ovou a y-ovou souřadnici. A právě makro MAKEPOINTS nám zabrání udělat tuto chybu. Dále si sestavíme textový řetězec pomocí funkce _stprintf. Tato funkce z hlediska použití Unicode "univerzální" varianta funkce sprintf. Je deklarována v hlavičkovém souboru <tchar.h>, kde v závislosti na existenci hodnoty _UNICODE je buď definována jako sprintf (bez Unicode) nebo swprintf (použití Unicode). Kromě souboru <tchar.h> musíme ještě vložit soubor <stdio.h>. Pokud by se stalo, že používáte nějaké vývojové prostředí, kde bude chybět soubor <tchar.h>, můžete "ručně" nahradit v našem kódu funkci _stprintf funkcí sprintf. Nyní se dostáváme k tomu nejdůležitějšímu z naší funkce, a to získání kontextu zařízení v případě, že nekreslíme jako odpověď na zprávu WM_PAINT. V tomto případě můžeme kdykoli "na požádání" získat handle kontextu zařízení pomocí finkce GetDC:

HDC GetDC(
  HWND hWnd   // handle okna
);

Takto získaný kontext zařízení musíme po jeho použití opět uvolnit funkcí ReleaseDC:

int ReleaseDC(
  HWND hWnd,  // handle okna
  HDC hDC     // handle kontextu zařízení
);

Tento kontext zařízení zahrnuje, stejně jako v případě jeho získání funkcí BeginPaint, pouze klientskou oblast okna. Pokud bychom chtěli mít přístup i do neklientské oblasti, museli bychom použít funkci GetWindowDC:

HDC GetWindowDC(
  HWND hWnd   // handle to window
);

Ukažme si použití této funkce na modifikovaném příkladě, kde budeme informace o souřadnicích kliknutí vypisovat do titulkového pruhu. Samozřejmě jednodušší a rychlejší (pro systém) by bylo tento text nastavit jako text okna, který se zobrazuje v titulkovém pruhu. Tento text je však zobrazen systémově nastaveným písmem, které nemůžeme nijak ovlivnit, pokud tedy nepoužijeme vlastní kreslení. Funkce, která bude uvedené realizovat, bude podobná funkci VlastníKreslení. Rozdíl bude v získání handle na kontext zařízení a dále v souřadnicích vypisovaného textu, které v tomto případě "ne zcela profesionálně" zkusmo nastavíme tak, aby text nepřekrýval text titulku okna a byl "někde uprostřed". Až se budeme později věnovat uživatelskému kreslení neklientské oblasti okna, ukážeme si samozřejmě, jak vypočítat souřadnice třeba tak, aby byl text vertikálně vycentrován při jakémkoli uživatelském nastavení výšky titulkového pruhu. Funkce tedy může vypadat takto:

void KresleniDoTitulku(WPARAM wParam, LPARAM lParam)
{
  POINTS ptCursor;
  HDC hdc;
  TCHAR chText[30];
  ptCursor = MAKEPOINTS(lParam);
  _stprintf(chText, "x=%d, y=%d", ptCursor.x, ptCursor.y);
  hdc = GetWindowDC(g_hwndMain);
  if ( wParam & MK_CONTROL )
    SetTextColor(hdc, 0x000000FF);
  TextOut(hdc, 150, 8, chText, lstrlen(chText));
  ReleaseDC(g_hwndMain, hdc);
}

Na závěr si ještě vytvoříme funkci, kterou budeme volat při pohybu myši v klientské oblasti (zpráva WM_MOUSEMOVE) a která bude souřadnice myši "vypisovat" nastavením získaného textu jako textu okna, který se u standardních oken vypisuje v titulkovém pruhu.

void OnWM_MOUSEMOVE(WPARAM wParam, LPARAM lParam)
{
  POINTS ptCursor;
  TCHAR chText[30];
  ptCursor = MAKEPOINTS(lParam);
  _stprintf(chText, "x=%d, y=%d", ptCursor.x, ptCursor.y);
  SetWindowText(g_hwndMain, chText);
}

Nyní si do proceduru okna upravíme tak, že funkci OnWM_MOUSEMOVE budeme volat při pohybu myši a funkci KresleniDoTitulku při stlačení levého tlačítka:

LRESULT CALLBACK WindowProcMain(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
  switch ( message )
  {
    case WM_DESTROY:
      PostQuitMessage(0);
      break;
    case WM_MOUSEMOVE:
      OnWM_MOUSEMOVE(wParam, lParam);
      break;
    case WM_LBUTTONDOWN:
      KresleniDoTitulku(wParam, lParam);
      break;
    case WM_PAINT:
      OnWM_PAINT();
      break;
  }
  return DefWindowProc(hwnd, message, wParam, lParam);
}

Podívejme se na výsledek:

win-api-7-2

Když však program spustíte, zjistíte že po kliknutí myší se text vypsaný funkcí KresleniDoTitulku ztratí jakmile myší pohneme o jediný pixel. Je to způsobeno tím, že změna textu okna, který má být u tohoto typu okna vypsán v titulkovém pruhu vyvolá překreslení celého titulkového pruhu, nejen tedy obdélníka tvořícího text v titulku. Samozřejmě že je možné okno kreslit zcela ve vlastní režii, tedy včetně korektního překreslování neklientské blasti. To však již patří mezi pokročilejší techniky, ke kterým se v tomto seriálu také dostaneme. Pro posílení motivace ke studiu Win API se můžete podívat na okno, které je celé kreslené "uživatelsky". Jedná se o jednoduchý příklad, který jsem před časem použil jako doprovod k svému článku. Nicméně tlačítko "Zavřít" a check-box "možno zavřít" jsou opravdu funkční, i když to samozřejmě nejsou samostatná okna, ale jsou simulována jako grafické objekty umístěné v titulkovém pruhu okna. Pokud tedy vytrváte, bude pro vás něco podobného a samozřejmě graficky mnohem lépe zpracovaného, hračkou.

win-api-7-3

Doprovodný projekt (Visual C++ 6) je ke stažení zde: win_api_7.zip

Prostředky (resources) - úvod.

V minulých dílech jsme se naučili různým způsobem vypisovat text, včetně zatím primitivního výpisu do neklientské oblasti. Mohli bychom nyní pokračovat na téma GDI tím, jak kreslit geometrické obrazce jako čáry, plošné útvary apod. Ale bude asi lepší, abychom si při výuce a testování takového kreslení uměli volat jednotlivé funkce přes nabídku okna. Na čas tedy opustíme GDI a budeme se věnovat dalšímu základnímu tématu Win API, a to jsou prostředky (resources). Mezi prostředky (překládané také jako "zdroje") totiž patří i zmíněné hlavní menu okna. Začneme ale tím nejjednodušším, a to jsou vlastní ikony a popř. i kurzory. Prostředky obecně jsou uloženy přímo ve spustitelném souboru ve formátu, který odpovídá typu zdroje. Tedy například data ikony jsou uložena ve spustitelném souboru zcela stejně jako v souboru .ico. Tím spustitelným souborem se zdroji bývá většinou přímo exe soubor aplikace, ale může být například i externí dll knihovna, jako je třeba knihovna shell32.dll, obsahující mnoho zdrojů, především ikon, známých uživatelům Windows. Abychom mohli prostředky používat, musíme je nějakým způsobem dostat (přilinkovat) do programu. Ve Visual C++ je to velice jednoduché. Nejprve přidáme do projektu tzv. "resource script", což je textový soubor (s koncovkou .rc) obsahující informace o zdrojích, které pak kompilátor a linker použijí při sestavení exe souboru. Tento soubor má tvar určený syntaxí jakéhosi "skriptovacího jazyka", který obsahuje odkazy na externí soubory (ikony, kurzory, bitmapy apod.) nebo úplný popis daného prostředku, jako je tomu v případě nabídek (menu) či dialogových oken. Jak jsem zde již uvedl, Visual C++ je vizuální v tom smyslu, že nám umožňuje tento skriptový soubor prostředků vygenerovat pomocí vizuálního editoru. Vzhledem k tomu, že toto čtou i programátoři používající C++ Builder, bude lépe s ohledem na ně si říci ještě trochu více v obecné rovině. Totiž kromě textových skriptů (.rc) existuje ještě formát tzv. zkompilované prostředky. Jsou to soubory s příponou res, které produkuje například ImageEditor, který je součástí C++ Builderu (a Delphi). C++ Builder bohužel neumí přímo editovat (a ukládat) soubory rc. Navíc pokud v C++ Builderu máte aplikaci s VCL, můžete hotový skript (.rc) snadno přidat do projektu, kde bude automaticky kompilován a linkován. Avšak v případě aplikace bez VCL nelze použít USERC, kterým jinak vkládáme rc skripty do projektu. Je možné použít kompilátor zdrojů brcc32.exe (volaný z příkazové řádky), který ze vstupního skriptu rc vytvoří kompilovaný soubor res. Ten pak lze přidat do zdrojového kódu pomocí
#pragma resource "jmeno_souboru.res"
Navíc tento res soubor umí Visual C++ (a jistě více editorů) přímo editovat. Image editor v C++ Builderu totiž umí pracovat pouze se zdroji typu bitmapa, ikona a kurzor, a to ještě v těch nejprimitivnějších formátech (třeba barevné kurzory vůbec nezná). Samozřejmě vždy lze upravovat skripty zdrojů (rc) textově. Například u ikon je to velice jednoduché neboť obsahují pouze identifikátor ikony a jméno souboru (ico) obsahujícího vlastní ikonu. Příklad řádku rc souboru definující ikonu:
IDR_MAINICON  ICON  DISCARDABLE "jmeno_souboru.ico"
zde identifikátor IDR_MAINICON musí být samozřejmě definován nejlépe ve vloženém hlavičkovém souboru třeba takhle:
#define IDR_MAINICON 101
Dále lze zdroje identifikovat textovými názvy. Pak na místě IDR_MAINICON bude příslušný text v uvozovkách. Vrátíme se nyní k našemu "většinovému" Visual C++. Do projektu si přidáme nový skript zdrojů (například) pomocí příkazu z nabídky "Project -> Add To Project -> New", kde na záložce "Files" vybereme "Resource Script". Nazveme si jej třeba main.rc a musíme nechat zaškrtnutou volbu (check-box) "Add To Project". Po potvrzení nám vedle záložek "ClassView" a "FileView" přibude záložka "ResourceView". Zde je jedna prázdná složka, do které si přidáme vlastní ikonu. Pokud chceme použít nějakou hotovou ikonu, kterou máme na disku, klikneme pravým tlačítkem na tuto složku a zvolíme "Import". Otevře se nám dialog, umožňující vybrat a vložit soubor s ikonou. Tuto ikonu pak můžeme dále editovat již přímo v editoru Visual C++. Přidejme si tedy do projektu ikonu a stejným způsobem třeba nějaký vlastní kurzor. Importovanou ikonu si přejmenujme z defaultního IDI_ICON1 na IDR_MAINICON. Toto přejmenování má svůj význam. Pokud totiž budeme postupně přidávat a mezitím třeba odstraňovat další ikony, jejichž identifikátory budou začínat na IDI_, editor by měl zajistit, že skutečné číslo, představující symbol IDR_MAINICON, bude nejnižší ze všech  identifikátorů ikon. Při zobrazení exe souboru třeba v průzkumníku Windows je pak vybrána ikona s nejnižším číslem. Chceme-li tedy, aby některá z ikon byla "hlavní", tedy reprezentující náš program, je nejlepší použít pro ni zmíněný identifikátor IDR_MAIN. Když nyní projekt překompilujeme, uvidíme již v průzkumníku náš exe soubor již reprezentovaný vloženou ikonou, aniž bychom museli cokoli přidávat do zdrojového kódu. Nyní se ale do zdrojového kódu opět podíváme. Budeme samozřejmě chtít, aby naše "hlavní ikona" byla zobrazena také na pruhu úloh s naším programem, a dále v systémovém menu (vlevo nahoře). Tuto ikonu určíme nastavením vlastnosti třídy, ke které okno patří. Později uvidíme,  že ji lze měnit i za běhu programu, zatím si ji určíme při registraci třídy okna. Nejprve musíme ještě vložit hlavičkový soubor "resource.h", obsahující definice identifikátorů. Ve funkci InitApp si upravíme následující 3 řádky na tento tvar:
  wc.hCursor = LoadCursor(g_hInstance, MAKEINTRESOURCE(IDC_MAIN));
  wc.hIcon = LoadIcon(g_hInstance, MAKEINTRESOURCE(IDR_MAIN));
  wc.hIconSm = LoadIcon(g_hInstance, MAKEINTRESOURCE(IDR_MAIN));
Jaký význam má použití prvku hIconSm? Máme zde možnost určit samostatnou ikonu, která bude použita v případě malé ikony zobrazované například vlevo nahoře jako místo pro rozbalení systémové nabídky okna. Pokud tento prvek nezadáme (bude mít hodnotu NULL), systém použije vhodný obrázek z ikony určené prvkem hIcon. Co mám na mysli tím vhodný obrázek? Každá ikona, ať se jedná o soubor (.ico) nebo ikonu ve zdrojích, může obsahovat (a v profesionálních programech obvykle obsahuje) více jednotlivých obrázků pro různá nastavení grafického adaptéru a systému co do barevné hloubky a pro různé požadované rozměry. Většinou vypadá lépe ikona, kterou si sami (resp. dobrý grafik) nakreslíme přímo v rozměru 16x16, než nechat systém, aby prostě zmenšil obrázek ikony 32x32, kde systém většinou jednoduše vyhodí každý druhý řádek. Když nyní takto upravený program spustíme, uvidíme již vlevo nahoře a na pruhu úloh vlastní ikonu a po najetí myší do okna se kurzor změní v zelenou šipku (kterou jsem do projektu importoval podobně jako ikonu). Dále si ukážeme, jak můžeme naši ikonu (nebo jakoukoli jinou kreslit do kontextu zařízení). Nejjednodušší funkce pro tento účel je DrawIcon:
BOOL DrawIcon(
  HDC hDC,      // handle kontextu zařízení
  int X,        // x-ová souřadnice ikony
  int Y,        // y-ová souřadnice
  HICON hIcon   // handle ikony
);
Pro získání handle ikony okna máme více možností. Mohli bychom použít funkci LoadIcon stejně, jako jsme ji použili při registraci třídy okna. Tato funkce, pokud je volána opakovaně, vrátí handle ikony, která byla již načtena. Nedochází tedy ke kumulovanému načítání stejné ikony do paměti při každém kreslení (pokud kreslíme na WM_PAINT). Alespoň podle dokumentace by nemělo. Ale člověk nikdy neví jak se tato funkce bude z tohoto hlediska chovat na různých (třeba i budoucích) verzích Windows. Je zde proto ještě jeden, podle mého názoru lepší a i o nějaký ten zlomek sekundy rychlejší způsob získání handle ikony nastavené třídě okna. Použijeme funkci GetClassLongPtr (popřípadě starší variantu GetClassLong, která není kompatibilní s 64 bitovou verzí Windows):
ULONG_PTR GetClassLongPtr(
  HWND hWnd,  // handle to window
  int nIndex  // offset of value to retrieve
);
Tato funkce nám umožňuje získat některou z hodnot, spojených s třídou okna. Kterou hodnotu chceme získat, určíme parametrem nIndex. Pro získání handle ikony použijeme hodnotu GCLP_HICON (v případě použití GetClassLong pak hodnotu GCL_HICON). Ukážeme si tedy jak v handleru WM_PAINT nakreslit ikonu okna na souřadnice 10,10 klientské oblasti:
void OnWM_PAINT()
{
  PAINTSTRUCT ps;
  HDC hdc;
  hdc = BeginPaint(g_hwndMain, &ps);
  centerText(hdc);
  DrawIcon(hdc, 10,10,
    (HICON)GetClassLongPtr(g_hwndMain, GCLP_HICON));
  EndPaint(g_hwndMain, &ps);
}
< zde je výsledek:/p>

win-api-8

K mnohem sofistikovanějším funkcím pro načítání a kreslení ikon a kurzorů se v tomto seriálu samozřejmě dostaneme, ale nyní jsme na začátku a musíme nejprve probrat ostatní základní témata. Příště se budeme věnovat nabídkám (menu) a zpracování uživatelského výběru z nabídky okna. Doprovodný projekt (Visual C++ 6) je ke stažení zde: win_api_8.zip

Hlavní nabídka okna.

V minulých dílech jsme se naučili použít vlastní ikonu a kurzor a také již umíme ikonu (zatím tím nejjednodušším způsobem) zobrazit v klientské oblasti okna. V tomto pokračování se naučíme používat hlavní nabídku okna a zachytávat výběr některé položky nabídky. Přidáme si tedy do projektu nabídku jako resource typu menu. V editoru zdrojů si v menu vytvoříme položku "Soubor" s jediným příkazem "Konec" (identifikátor třeba ID_END) a položku nápověda s oblíbeným "O programu" (identifikátor ID_ABOUT). Jak nyní zobrazit nabídku v okně. Máme v zásadě 2 možnosti:

  • Nastavit menu jako vlastnost třídy - toto menu pak budou mít implicitně všechna okna této třídy (pokud jim neurčíme jinak)
  • Nastavit menu pouze konkrétnímu oknu při jeho vytvoření funkcí CreateWindowEx

Ukažme si nejprve 1. způsob. Jedimé, co musíme do našeho kódu přidat, je řádka (tučně zvýrazněná) ve funkci InitApp, kde nastavujeme prvky struktury WNDCLASSEX:

wc.lpszClassName = _MainClassName;
wc.lpszMenuName = MAKEINTRESOURCE(IDR_MAINMENU);
wc.style = CS_HREDRAW | CS_VREDRAW;

V tomto případě můžeme nechat ve funkci CreateWindowEx parametr hMenu NULL a menu bude zobrazeno. Nyní 2. způsob. Pokud při registraci třídy nebude menu zadáno, nebo pokud chceme u konkrétního okna toto "třídní menu" změnit, můžeme tak učinit ve funkci CreateWindowEx, která bude vypadat třeba následovně:

g_hwndMain = CreateWindowEx(0, _MainClassName,
  _AppName,
  WS_OVERLAPPEDWINDOW | WS_VISIBLE,
  100, 100, 350, 250,
  NULL, 
LoadMenu(g_hInstance, MAKEINTRESOURCE(IDR_MAINMENU)),
  g_hInstance, NULL);

Nyní tedy máme jedním ze dvou způsobů menu zobrazené, a musíme se naučit zachytávat výběr některé položky uživatelem. Zde se dostáváme k jedné nejběžnějších zpráv Windows, kterou je WM_COMMAND. Tato zpráva je poslána oknu, pokud například uživatel vybere položku menu, klikne na tlačítko umístěné v okně, použije nějakou klávesovou zkratku nebo pokud některý prvek posílá oznámení o změně stavu (například změna textu v edit-boxu). O těchto záležitostech si samozřejmě řekneme později. Nyní k výběru z nabídky. V tomto případě dolní WORD parametru wParam obsahuje identifikátor položky nabídky, v našem případě tedy například ID_END nebo ID_ABOUT. Nám nyní zbývá pouze v proceduře okna vytvřit handler zprávy WM_COMMAND takto:

 switch ( message )
  {
    case WM_COMMAND:
      switch ( LOWORD(wParam) )
      {
        case ID_ABOUT:
          MessageBox(hwnd, "Win API demo díl 9.nnRadek Chalupa, 2002",
            "O programu WinApi demo", MB_ICONINFORMATION);
          break;
        case ID_END:
          SendMessage(hwnd, WM_CLOSE, 0, 0);
          break;
      }
      break;
// atd....

Podívejme se nyní jak lze programově zavřít okno a ukončit aplikaci. Každé okno lze zrušit funkcí DestroyWindow:

BOOL DestroyWindow(
  HWND hWnd   // handle okna
);

Tato funkce se kromě zrušení daného okna postará o zrušení všech podřízených oken (např.edit-boxů a buttonů na dialogu), nabídky a dalších věcí (o nichž ztím nic nevíme) patřících oknu tak, že nezůstane neuvolněná paměť. Pokud takto zrušíme hlavní okno aplikace, dojde k ukončení celé aplikace. Tuto funkci bychom mohli použít v reakci na výběr položky ID_END z nabídky, avšak pokud místo toho pošleme oknu zprávu WM_CLOSE (jak je uvedeno ve výpisu) máme ještě možnost v handleru zprávy WM_CLOSE zobrazit například výzvu k uložení souboru nebo k potvrzení ukončení aplikace. Řeknete si možná že, totéž můžeme udělat přímo v handleru příkazu ID_END. To je samozřejmě pravda, ale v handleru WM_CLOSE zachytíme i požadavek na zavření okna pomocí kliknutí na systémovou ikonu okna nebo klávesovou zkratkou <alt>+F4. Je to tedy ideální "centalizované" místo pro "ošetřovací kód" před zavřením okna. Jak jsem jž naznačil, při použití funkce DestroyWindow zpráva WM_CLOSE poslána není a tak bychom obešli náš ukončovací kód, což by nemuselo být žádoucí. V této souvislosti si ukažme, jak se dotázat uživatele, zda si opravdu přeje zavřít okno a podle toho korektně reagovat. Pro dotaz použijeme již zmíněnou funkci MessageBox, která kromě prostého zobrazení zprávy může sloužit jako dotazovací dialog. Když se podíváte do dokumentace, zjistíte že 4. parametr funkce MessageBox může nabývat mnoha různých hodnot a jejich kombinací. Jedna sada příznaků určuje například, jaká ze systémových ikon má být v dialogu zobrazena. Pro náš dotaz použijeme MB_ICONQUESTION, což znamená že bude zobrazena ikona "otázka" na levé straně dialogu. Kromě tohoto kosmetického vylepšení je samozřejmě důležité zobrazit 2 tlačítka "Ano" a "Ne". V tom případě můžeme použít hodnotu MB_YESNO, nebo MB_YESNOCANCEL, která nám přidá ještě třetí tlačítko "Storno". Nyní už zbývá jen podle návratové hodnoty funkce MessageBox zjistit, které tlačítko uživatel stiskl, případně zda ukončil dialog klávesou <Escape>. V případě stisknutí tlačítka "Ano" funkce vrací hodnotu IDYES. Popis všech možných návratových hodnot je samozřejmě také v dokumentaci. Po tomto výkladu se již můžeme podívat jak bude vypadat handler zprávy WM_CLOSE v proceduře okna:

switch ( message )
{
  case WM_CLOSE:
    if ( MessageBox(hwnd, "Opravdu končit?", _AppName,
      MB_YESNO | MB_ICONQUESTION) != IDYES )
      return 0;
    break;

Když se podíváte na kód, vidíte že v případě že nebylo stisknuto tlačítko "Ano" vrátíme v proeduře okna hodnotu 0 místo abychom zprávu předali dále k výchozímu zpracování funkcí DefWindowProc. Takto můžeme jakoukoli zprávu zadržet, tedy zrušit její "účinek". Doprovodný projekt (Visual C++ 6) je ke stažení zde: win_api_9.zip

Ovládací prvky Windows - úvod.

V minulých dílech jsme se naučili základům práce s nabídkou okna a s tím souvisejícím zpracováním výběru z nabídky, a v té souvislosti jsem se seznámili se zprávou WM_COMMAND. Dnes se naučíme vytvářet dětská okna, tedy většinou prvky jako tlačítka, editační pole apod. umístěné na hlavním okně.

Na úvod je třeba připomenout, že v běžné praxi programů vytvořených ve Visual C++ se tyto výše uvedené prvky Windows umisťují na dialogová okna. Dialogová okna jsou poněkud jednodušší a méně náročné na systémové zdroje. Proč tomu tak je poznáme v jednom z nejbližších pokračování, kdy se jim budeme věnovat podrobněji. Naopak v aplikacích vytvářených například pomocí knihovny VCL (C++ Builder a Delphi) se dialogová okna v pravém slova smyslu nepoužívají a i formuláře, plnící funkci dialogů jsou ve skutečnosti běžná okna simulující chování standardního dialogu. I přes výše uvedené se však "ruční" vytváření prvků Windows samozřejmě často používá i ve Visual C++, proto začneme tímto tématem, jehož pochopení nám pak dá dobrý základ při používání dialogových oken.

Vytvořme si na našem okně jedno editační pole a 2 tlačítka. Jedno z nich bude ukončovat aplikaci a při stisknutí druhého si někam, konkrétně třeba do message-boxu vypíšeme aktuální obsah editačního pole. Nejdříve si deklarujeme příslušné proměnné typu HWND, udržující handle vytvořených oken:

HWND g_hwndEnd;
HWND g_hwndButton1;
HWND g_hwndEdit;

Vlastní vytvoření těchto prvků provedeme opět pomocí funkce CreateWindowEx. Třídu okna v tomto případě není třeba registrovat, protože pro tyto tzv. Standardní prvky Windows jsou již v systému registrovány příslušné třídy. Potřebné názvy těchto tříd zjistíme v dokumentaci (např. v MSDN u popisu funkce CreateWindowEx). V našem případě půjde o třídy "EDIT" a "BUTTON". Vytvořme si tedy editační pole:

g_hwndEdit = CreateWindowEx(WS_EX_CLIENTEDGE,
  TEXT("EDIT"),
  TEXT("editační pole"),
  WS_CHILD | WS_VISIBLE | ES_LEFT,
  10, 15, 200, 25,
  g_hwndMain,
  (HMENU)NULL,
  g_hInstance,
  NULL);
if ( g_hwndEdit == NULL )
  return FALSE;

Jako parametr dwExStyle použijeme WS_EX_CLIENTEDGE, aby edit-box měl vnořené okraje, jak bývá u tohoto prvku zvykem. V parametru dwStyle je důležitá hodnota WS_CHILD. Říká, že se jedná o dětské okno umístěné v klientské oblasti rodičovského okna uvedeného v parametru hWndParent. Když tuto hodnotu neuvedeme, edit-box se vytvoří jako plovoucí okno s vlastním titulkovým pruhem (při těchto rozměrech ovšem pak na vlastní editační pole zůstanou nějaké 2-4 pixely), s tím že funkčnost zůstane zachována, tj budeme moci do něj psát a získávat jeho obsah. Pokud bychom neuvedli hodnotu WS_VISIBLE, museli bychom po vytvoření edit-boxu ještě pomocí funkce ShowWindow tento zviditelnit. Hodnota ES_LEFT pak určuje zarovnání textu doleva.

Podobně si vytvořme dvě tlačítka:

g_hwndButton1 = CreateWindowEx(0,
    TEXT("BUTTON"),
    TEXT("Text?"),
    WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
    10, 50, 75, 25,
    g_hwndMain,
    (HMENU)NULL,
    g_hInstance,
    NULL);
if ( g_hwndButton1 == NULL )
    return FALSE;
g_hwndEnd = CreateWindowEx(0,
    TEXT("BUTTON"),
    TEXT("Konec"),
    WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
    95, 50, 75, 25,
    g_hwndMain,
    (HMENU)ID_END,
    g_hInstance,
    NULL);
if ( g_hwndEnd == NULL )
    return FALSE;

Zde hodnota BS_PUSHBUTTON znamená, že jde o běžné tlačítko. Například check-box je také button s vlastností BS_CHECKBOX. Lze sice říci, že s velkou pravděpodobností je hodnota BS_PUSHBUTTON defaultní, ale vždy je lépe na to nespoléhat a uvést ji.

Nyní tedy máme požadované prvky na okně.

Určitě si všimnete, že písmo takto vytvořených prvků je jiné než u běžných prvků umístěných na dialogovém okně. O fontech a jejich použití se samozřejmě naučíme více později, proto na tomto místě uvedu bez podrobnějšího komentáře jeden způsob, jak nastavit písmo našich prvků na font běžný v grafickém rozhraní Windows.

HFONT hFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
SendMessage(g_hwndEdit, WM_SETFONT, (WPARAM)hFont, (LPARAM)TRUE);
SendMessage(g_hwndButton1, WM_SETFONT, (WPARAM)hFont, (LPARAM)TRUE);
SendMessage(g_hwndEnd, WM_SETFONT, (WPARAM)hFont, (LPARAM)TRUE);

Když tento kód přidáme do funkce InitApp za vytvoření oken prvků dostaneme již výsledek, na který jsme zvyklí

Nyní nám již zbývá jen umět reagovat na některé události na těchto prvcích. U zachytávání kliknutí na button máme v zásadě 2 možnosti. Když se podíváte na kód vytvoření tlačítka "Konec", zjistíte, že jsem jako parametr hMenu funkce CreateWindowEx uvedl hodnotu identifikátoru položku menu ID_END. V tomto případě button při stlačení pošle oknu zprávu WM_COMMAND stejně jako menu při vybrání položky. Vzhledem k tomu, že zachycení identifikátoru ID_END již máme hotové, nic dalšího nemusíme do kódu přidávat a stlačení buttonu vyvolá stejnou akci jako vybrání položky menu ID_END ("Konec"). Druhou možností jak identifikovat tlačítko v handleru zprávy WM_COMMAND je test parametru lParam na jeho handle:

case WM_COMMAND:
  if ( lParam == (LPARAM)g_hwndButton1 )
  {
    GetWindowText(g_hwndEdit, chText, 200);
    MessageBox(g_hwndMain, chText, TEXT("Obsah editačního pole"), MB_ICONINFORMATION);
  }

Pro získání obsahu edit-boxu použijeme funkci GetWindowText, která nám naplní parametr lpString textem okna, v případě edit-boxu samozřejmě textem v editačním poli. Třetí parametr (nMaxCount) pak je maximální počet znaků, které se mají kopírovat. Samozřejmě že v našem případě použití textového bufferu s pevnou délkou (200) způsobí uříznutí textu delšího než 200 znaků. Vyřešení tohoto problému pomocí dynamicky alokovaného bufferu je ale už jiné téma.

Tolik pro dnešek a pokud si chcete zaexperimentovat, zkuste si různě měnit hodnoty stylů při vytváření edit-boxu a tlačítek a sledovat výsledek.

Doprovodný projekt (Visual C++ 6) je ke stažení zde: win_api_10.zip

Použití časovače. Vzhled prvků v novém stylu Windows.

V minulém článku jsme se naučili vytvářet ty nejjednodušší dětská okna - prvky Windows, kterým byl editbox a tlačítko a také již umíme zachytávat zprávy od těchto prvků. V tomto díle si vytvoříme stavový řádek okna do kterého budeme vypisovat aktuální čas, abychom se seznámili a časovači (timer). Vzhledem k narůstajícímu počtu uživatelů Windows XP si také, zatím bez podrobnějšího výkladu, ukážeme, jak jednoduše zařídit, aby naše prvky v kklientské oblasti měly vzhled podle nastavení Windows XP. Pokud chceme, aby prvky klientské oblasti používaly nový vzhled odvozený od nastavení Windows XP, musíme to systému říci tzv. "manifestem", což může být externí soubor s přesně definovaným názvem odvozeným od názvu aplikace: JmenoProgramu.exe.manifest, název tedy vytvoříme přidáním "přípony" manifest za název programu (včetně exe). Druhou možností je tento soubor vložit do resources programu zdroj typu RT_MANIFEST nazvaný CREATEPROCESS_MANIFEST_RESOURCE_ID. Tento manifest je textový soubor v XML formátu obsahující příslušné informace. Vzor takového manifestu najdete mezi soubory doprovodného projektu ("WinApi.exe.manifest"). Kromě tohoto manifestu musíme dále v kódu registrovat a inicializovat třídy pro běžné prvky (common controls). To lze udělat funkcí InitCommonControls, ale lépe novější a doporučovanou funkcí InitCommonControlsEx.

BOOL InitCommonControlsEx(
    LPINITCOMMONCONTROLSEX lpInitCtrls
);

Tato funkce umožňuje registrovat jednotlivě různé "speciální" prvky, jako například progress-bar, tool-tip a pod, přičemž účinek je kumulativní. Podrobnosti jsou samozřejmě vysvětleny v MSDN. Základní použití tak jak je i v našem doprovodném programu (umístěné na začátek funkce InitApp) vypadá takto:

INITCOMMONCONTROLSEX icc;
icc.dwSize = sizeof(INITCOMMONCONTROLSEX);
icc.dwICC = ICC_WIN95_CLASSES;
if ( !InitCommonControlsEx(&icc) )
  return FALSE;

Pro použití této funkce je ještě potřeba mít ve zdrojovém kódu vložen příslušný hlavičkový soubor:

#include <commctrl.h>

do projektu přidat knihovnu comctl32.lib, pokud tedy ji tam již nepřidá "wizard" při vytváření projektu v určitém vývojovém prostředí. Nyní již by ve Windows XP měly i prvky klientské oblasti mít požadovaný vzhled.

Nyní k slíbenému stavovému řádku a výpisu času do něj. Stavový řádek můžeme vytvořit jako okno systémové třídy (kterou tedy nemusíme registrovat) STATUSCLASSNAME, kterou použijeme jako jméno třídy ve funkci CreateWindowEx. Existuje ještě funkce CreateStatusWindow, která je však již zastaralá a je doporučeno používat CreateWindowEx. Takto tedy vytvoříme v našem programu stavový řádek:

g_hwndStatusBar = CreateWindowEx(0,
  STATUSCLASSNAME,
  TEXT("Stavový řádek"),
  WS_CHILD | WS_VISIBLE,
  0, 0, 0, 0,
  g_hwndMain,
  (HMENU)NULL,
  g_hInstance,
  NULL);
if ( g_hwndStatusBar == NULL )
  return FALSE;

Hodnota STATUSCLASSNAME je definována jako "msctls_statusbar32", což je skutečné jméno třídy, které získáte třeba pomocí programu Spy++, když si necháte zobrazit údaje o okně stavového řádku. Možná se podivujete nad zadanými rozměry okna, kde jsou "samé nuly". Stavový řádek má totiž jednu šikovnou vlastnost: při změně velikosti jeho rodičovského okna nemusíme přepočítávat a nastavovat jeho rozměry, ale stačí když při zachycení zprávy WM_SIZE v proceduře hlavního okna tuto zprávu přepošleme oknu stavového řádku s tím, že parametry wParam a lParam předáme beze změny. Stavový řádek se pak automaticky drží spodního okraje okna tak jak od něj očekáváme. A vzhledem k tomu že po vytvoření stavového řádku, když dojde k zobrazení hlavního okna, zpráva WM_SIZE je poslána, tak ani počáteční velikost nemusíme počítat a nastavovat, proto ty nuly v rozměrech. Tedy jediné co ještě uděláme, je následující handler v proceduře hlavního okna:

case WM_SIZE:
   SendMessage(g_hwndStatusBar, WM_SIZE, wParam, lParam);
   break;

Stavový řádek máme tedy vytvořen, nyní něco o časovačích. Každé okno může mít jeden nebo více časovačů (timer), které tomuto oknu posílají v nastaveném intervalu zprávu WM_TIMER. Její parametr wParam obsahuje identifikátor časovače, abychom mohli jednotlivé časovače rozlišit, pokud jich jednomu oknu nastavíme více. Jak časovač vytvořit a zastavit. K vytvoření a aktivaci časovače slouží funkce SetTimer:

UINT_PTR SetTimer(
  HWND hWnd,              // handle okna
  UINT_PTR nIDEvent,      // identifikátor časovače
  UINT uElapse,           // interval časovače
  TIMERPROC lpTimerFunc   // procedura časovače
);

Procedury časovače se používají pouze ve zvláštních případech a nemusíme se jí zatím zabývat. Důležité je vědět že interval, ve kterém budou posílány zprávy WM_TIMER se zadává v milisekundách. Pokud chceme časovač zastavit, použijeme funkci KillTimer:

BOOL KillTimer(
  HWND hWnd,          // handle okna
  UINT_PTR uIDEvent   // identifikátor časovače
);

Nyní si tedy ukažme, jak ve stavovém řádku zobrazíme hodiny s aktualizací času každou sekundu. Nejprve musíme vytvořit časovač:

SetTimer(g_hwndMain, _TimerClock, 1000, NULL);

Toto volání funkce umístíme opět do funkce InitApp, hodnota _TimerClock je definovaná "někde na začátku" jako:

#define _TimerClock 1

Poté již naše okno dostávaá každou sekundu zprávu WM_TIMER, na kterou budeme reagovat tak, že získáme systémový čas, převedeme je to textového řetězce a ten nastavíme jako text okna stavového řádku:

// část kódu procedury okna
SYSTEMTIME sysTime
// ...
case WM_TIMER:
  GetLocalTime(&sysTime);
  _stprintf(chText, TEXT(" Dnes je: %d.%d.%d, %d:%.2d:%.2d hod."),
    sysTime.wDay, sysTime.wMonth, sysTime.wYear,
    sysTime.wHour, sysTime.wMinute, sysTime.wSecond);
  SetWindowText(g_hwndStatusBar, chText);
  break;

Pro získání systémového času je zde použita funkce GetLocalTime, která nám naplní strukturu SYSTEMTIME

typedef struct _SYSTEMTIME { 
    WORD wYear; 
    WORD wMonth; 
    WORD wDayOfWeek; 
    WORD wDay; 
    WORD wHour; 
    WORD wMinute; 
    WORD wSecond; 
    WORD wMilliseconds; 
} SYSTEMTIME, *PSYSTEMTIME;

Hodnotami určujícími aktuální datum a čas. Zde vidíte výsledek i s použitím manifestu pro nový vzhled prvků ve Windows XP:

win-api-11

Ještě technická poznámka k doprovodnému projektu. Počínaje tímto dílem je projekt ve verzi Visual C++ NET. I když většina čtenářů asi ještě nemá tuto poslední verzi Visual C++ (součást Visual Studia .NET) k dispozici, není problém si vytvořit v příslušné verzi Visual C++ projekt a vždy pouze kopírovat zdrojové soubory .cpp, .h a skript zdrojů .rc do stávající verze projektu a překompilovat.

Doprovodný projekt (Visual C++) je ke stažení zde: win_api_11.zip

Dialogy

V minulých dílech jsme si ukázali na jednoduchém příkladě editačního pole a tlačítka, jak vytvořit tzv. Běžné prvky Windows na nějakém rodičovském okně, které pak přijímá zprávy od těchto prvků, týkající se především uživatelského vstupu. Ve standardních programech se většinou takovéto prvky umísťují na dialogová okna. Tímto výrazem mám na mysli dialogová okna v pravém slova smyslu, tedy nikoli okna chovající se jako dialog. Z hlediska Win API jsou totiž dialogy speciální třídou oken, mající některé specifické vlastnosti odlišující je od oken, jako je třeba hlavní okno naší aplikace. Jaké jsou tedy hlavní rozdíly? Pro vlastní dialogové okno neregistrujeme vlastní třídu. Dialogy totiž patří do třídy, která je již zaregistrovaná (podobně jako jsme tvořili edit-box a button). Mají také proceduru okna, která má stejné parametry jako "běžná" procedura okna, avšak její návratovou hodnotou v případě požadavku na výchozí zpracování zprávy není výsledek funkce DefWindowProc, jako u "klasického" okna, ale pouze vrátíme FALSE pokud se má zpráva zpracovat defautlně a TRUE, pokud je zpráva zpracovaná v aplikaci. Dále dialogové okno můžeme vytvořit jako modální nebo nemodální. Modální dialog běžně vytváříme (a zobrazujeme) funkcí DialogBox:
INT_PTR DialogBox(
  HINSTANCE hInstance,  // handle modulu(instance)
  LPCTSTR lpTemplate,   // název zdroje dialogu
  HWND hWndParent,      // handle rodičovského okna
  DLGPROC lpDialogFunc  // procedura okna dialogu
);
Pokud chceme vytvořit nemodální dialogové okno, použijeme funkci CreateDialog:
HWND CreateDialog(
  HINSTANCE hInstance,  // handle instance
  LPCTSTR lpTemplate,   // název zdroje dialogu
  HWND hWndParent,      // handle rodičovského okna
  DLGPROC lpDialogFunc  // procedure okna dialogu
);
Tato funkce nám vrátí handle okna vytvořeného dialogu, které pak zobrazíme funkcí ShowWindow stejně jako "bežné" okno. Takový dialog je pak nemodální, to znamená, že bez jeho zavření můžeme pracovat s jiným oknem aplikace a opět se do dialogu vracet bez jeho opakovaného vytváření. V praxi ale většina dialogových oken bývá modální. Nyní vyvstane otázka, jak se dialog ukončuje. Zde je rozdíl oproti běžnému oknu. Pokud bychom totiž v proceduře dialogu sami nezařídili jeho ukončení v reakci na nějaký podnět uživatele, okno dialogu se samo nezavře ani třeba po kliknutí na systémovou ikonku "zavřít". Musíme tedy alespoň na jednom místě použít funkci EndDialog:
BOOL EndDialog(
  HWND hDlg,        // handle okna dialogu
  INT_PTR nResult   // návratová hodnota dialogu
);
Parametrem nResult určíme hodnotu, kterou pak dostaneme jako návratovou hodnotu funkce DialogBox, pokud jsme dialog vytvořili jako modální. Tímto způsobem pak můžeme rozlišit například zda uživatel ukončil dialog stisknutím tlačítka "OK" nebo "Storno". Vše si za chvilku ukážeme. Posledním významným rozdílem mezi dialogem a běžným oknem je, že prvky na dialogu (edit, list-box, button atd.) se nevytvářejí (přesněji řečeno nemusí se takto vytvářet) programově tak, jak jsme to dělali na našem hlavním okně, ale jsou vytvořeny automaticky podle předpisu v skriptu zdroje resource skript). Nic nám samozřejmě nebrání abychom po vytvoření dialogového okna stejným způsobem přidali další prvky (pomocí CreateWindowEx), ale v praxi se to nedělá, pokud k tomu není nějaký zvláštní důvod. Ukažme si nyní vše na příkladě. V editoru zdrojů si vytvoříme dialog (zde nazvaný IDD_DIALOG1), zatím pouze s tlačítky "OK" a "Cancel", která mají identifikátory IDOK a IDCANCEL (které ve Visual C++ jsou takto nazvány defaultně). Dále si napíšeme následující (zatím tu nejjednodušší) proceduru dialogu:
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch ( uMsg )
  {
    case WM_COMMAND:
      switch ( LOWORD(wParam) )
      {
        case IDOK:
          EndDialog(hwndDlg, IDOK);
          break;
        case IDCANCEL:
          EndDialog(hwndDlg, IDCANCEL);
          break;
      }
      break;
  }
  return FALSE;
}
Jak je vidět, zachytávání "příkazové" zprávy WM_COMMAND je stejné jako v případě bežného okna. V reakci na stisknutí jednoho z tlačítek pak dialog ukončíme zmíněnou funkcí EndDialog s předáním identifikátoru použitého tlačítka. Vlastní dialog pak vyvoláme (v proceduře hlavního okna) na výběr položky, kterou si přidáme do nabídky hlavního okna:
    case WM_COMMAND:
      switch ( LOWORD(wParam) )
      {
        // ostastní příkazy....
case ID_SHOW_DIALOG:
          dlgResult = DialogBox(g_hInstance, MAKEINTRESOURCE(IDD_DIALOG1),
            hwnd, (DLGPROC)DialogProc);
          if ( dlgResult == IDOK )
            MessageBox(hwnd, "Dialog ukončen OK", "Dialog", MB_ICONINFORMATION);
          else
            MessageBox(hwnd, "Dialog ukončen Cancel", "Dialog", MB_ICONEXCLAMATION);
          break;
      }
Zde je také vidět jak určíme z návratové hodnoty funkce DialogBox, jakým tlačítkem byl dialog ukončen. Pro úplnost ještě dodám, že stisknutí systémové ikonky "Zavřít" v dialogu vyvolá zprávu s identifikátorem IDCANCEL, proto je výhodné nechat identifikátor tlačítka quot;Storno" takto nazvaný abychom nemuseli testovat každý případ zvlášť. Doprovodný projekt je ke stažení zde: win_api_12.zip

Ovládací prvky na dialogovém okně - úvod.

V předchozím článku jsme si ukázali, jak vytvořit ten nejjednodušší dialog, založený na standardní třídě dialogových oken. Umíme už zjistit, jakým způsobem uživatel dialog ukončil, tedy v typickém případě rozlišit stisk "OK" a "Storno". V tomto pokračování se začneme zabývat prací s běžnými dialogovými prvky. Jedním ze specifických rysů dialogového okna je, že prvky na něm umístěné definujeme ve skriptu zdrojů a nemusíme je vytvářet "ručně" pomocí CreateWindowEx. Výhodou je kromě zjednodušení kódu snadná práce s rozmístěním prvků na dialogu (pokud samozřejmě používáme nějaký vizuální editor zdrojů). Začneme tím, že si na dialog umístíme jedno editační pole (IDC_EDIT1), tlačítko (IDC_ZOBRAZ_TEXT), prvek Static text (IDC_KOPIE) a check-box (IDC_SLEDOVAT). Identifikátory prvků uvedené v závorkách si samozřejmě můžete zvolit podle svého, pak budete muset upravit i zdrojový kód. Cílem v této fázi bude naučit se jak získat text obsahu edit-boxu, jak zjistit okamžitý stav zaškrtnutí check-boxu a jak zachytit událost, kdy dojde ke změně obsahu edit-boxu, tedy když uživatel zapíše či smaže znak. Řekněme, že budeme chtít po stisknutí tlačítka přenést aktuální obsah edit-boxu do titulku hlavního okna aplikace. Tím se také přesvědčíme o tom, že i když je otevřen modální dialog, další okna aplikace mohou běžně přijímat zprávy a reagovat na ně, i když pro uživatele jsou v té době nedostupná. V proceduře dialogu již máme handler zprávy WM_COMMAND, kde zachytáváme stisk tlačítek "OK" a "Storno". Přidejme tedy do handleru WM_COMMAND následující case:
case IDC_ZOBRAZ_TEXT:
  GetDlgItemText(hwndDlg, IDC_EDIT1, chText, 200);
  SendMessage(g_hwndMain, WM_SETTEXT, 0, (LPARAM)chText);
  break;
To je vše co musíme udělat pro přenesení textu edit-boxu do titulku hlavního okna. Jak je vidět, pro získání textu prvku na dialogu můžeme použít funkci GetDlgItemText:
UINT GetDlgItemText(
  HWND hDlg,       // handle okna dialogu
  int nIDDlgItem,  // identifikátor prvku
  LPTSTR lpString, // buffer pro text
  int nMaxCount    // maximální počet zkopírovaných znaků
);
Samozřejmě bychom mohli použít již dříve uvedenou funkci GetWindowText. Pak bychom ale museli získat handle (HWND) prvku (edit-boxu). K získání handle kteréhokoli prvku na dialogu slouží funkce GetDlgItem:
HWND GetDlgItem(
  HWND hDlg,       // handle okna dialogu
  int nIDDlgItem   // identifikátro prvku
);
Řekněme si ještě o funkci, která nám vrátí číselnou hodnotu (pokud ji lze převést) obsahu prvku dialogu. U edit-boxu je možné a výhodné pro tento případ nastavit v editoru zdrojů vlastnost "Number" odpovídající stylu okna ES_NUMBER. Příslušnou funkcí je pak GetDlgItemInt:
UINT GetDlgItemInt(
  HWND hDlg,           // handle okna dialogu
  int nIDDlgItem,      // identifikátor prvku
  BOOL *lpTranslated,  // indikátor úspešného převodu
  BOOL bSigned         // zda jde o hodnotu se znaménkem
);
Tato funkce nám vrátí číselnou hodnotu obsahu prvku, pokud má obsah číselný smysl. Úspěšnost převodu je uložena do parametru lpTranslated. Dále si řekneme, jak zjistíme v požadovaném okamžiku stav zaškrtnutí check-boxu. Zde zjistíme, že jednou z nejčastěji používaných funkcí je SendMessage, tedy funkce která pošle zvolenému oknu zprávu, která může sloužit k "ovládání" okna, tedy například nastavení textu, přidání řádku do list-boxu a stovky podobných. Kromě toho tato funkce vrací výsledek zpracování zprávy v příslušném okně. Mnoho zpráv je totiž dotazovacích, kterými požadujeme od cílového okna nějakou informaci, například právě stav zaškrtnutí check-boxu. V dialogových oknem můžeme místo SendMessage použít funkci SendDlgItemMessage:
LRESULT SendDlgItemMessage(
  HWND hDlg,      // handle okna dialogu
  int nIDDlgItem, // identifikátor prvku
  UINT Msg,       // kód zprávy
  WPARAM wParam,  // 1. parametr zprávy
  LPARAM lParam   // 2. parametr
);
V této funkci nemusíme uvádět handle okna, ale místi toho identifikátor prvku dialogu a systém si handle zjistí sám. Je to obdobná jako u vztahu funkcí GetWindowText a GetDlgItemText. V případě zjištení stavu check-boxu použijeme zprávu BM_GETCHECK. Identifikátor zprávy začíná na "BM", což je ze slov "button message". Znamená to, že check-box je prvek třídy "BUTTON". Návratová hodnota zprávy (tedy to co nám vrátí funkce SendMessage nebo SendDlgItemMessage) pak může být jednou ze 3 hodnot:
  • BST_CHECKED - check-box je zaškrtnut
  • BST_UNCHECKED - check-box není zaškrtnutý
  • BST_INDETERMINATE - check-box je "zašedlý", tedy v neurčitém stavu
V této souvislosti si řekněme o "opačné" zprávě, tedy nastavení stavu check-boxu programově. Tou zprávou je BM_SETCHECK, a v parametru wParam zprávy SendDlgItemMessage uvedeme jeden z výše uvedených stavů, do kterého chceme check-box nastavit. Toto počáteční nastavení se obvykle realizuje v handleru zprávy WM_INITDIALOG, kterou obdržíme pouze jednou "na začátku" po vytvoření dialogového okna, těsně před tím, než je dialog zobrazen. Nakonec si ukážeme, jak sledovat jakékoli změny v obsahu editačního pole. V tomto případě je posílána tzv. oznamovací (notifikační) zpráva EN_CHANGE. Tato a další oznamovací zprávy nejsou ve skutečnosti samostatnými zprávami, ale jsou posílány jako horní WORD parametru wParam zprávy WM_COMMAND. Vše si ukážeme na příkladě, ve kterém budeme změny v editačním poli "kopírovat" do prvku "static text", tedy při každé zachycení změny obsahu editu (IDC_EDIT1), zjistíme jeho obsah a ten nastavíme jako text prvku static text (IDC_KOPIE). Toto budeme provádět pouze pokud je v daném okamžiku zaškrtnut check-box (IDC_SLEDOVAT). Do handleru WM_COMMAND si přidejme násjedující kód, který vše zrealizuje:
case IDC_EDIT1:
  if ( HIWORD(wParam) == EN_CHANGE )
  {
    if ( SendDlgItemMessage(hwndDlg, IDC_SLEDOVAT, BM_GETCHECK, 0,0) == BST_CHECKED )
    {
      GetDlgItemText(hwndDlg, IDC_EDIT1, chText, 200);
      SetDlgItemText(hwndDlg, IDC_KOPIE, chText);
    }
  }
  break;
Výsledek "v akci" vidíte na následujícím obrázku:

win-api-13

Ještě si můžeme nastavit výchozí stav check-boxu na zaškrtnutý v handleru zmíněné zprávy WM_INITDIALOG:
case WM_INITDIALOG:
  SendDlgItemMessage(hwndDlg, IDC_SLEDOVAT, BM_SETCHECK, BST_CHECKED, 0);
  break;
Tolik pro toto pokračování a příště se samozřejmě budeme prvkům na dialogu  dále věnovat, neboť jde o základní část většiny programů, tedy uživatelské rozhraní. Doprovodný projekt (Visual C++ 6) je ke stažení zde: win_api_13.zip

ListBox a ComboBox. Program založený na dialogovém okně.

V tomto pokračování si ukážeme práci s běžnými ovládacími prvky na dialogu - bude to zejména ListBox a ComboBox. V té souvislosti si také ukážeme, jak můžeme vytvořit projekt založený na dialogovém okně. Založme si nejprve nový projekt, opět prázdnou aplikaci Win32. Do projektu si přidáme zdrojový soubor main.cpp a soubor zdrojů (resource skript) main.rc. Do zdrojů si přidáme dialog s prvky které vidíte na následujícím screen-shotu již hotového programu:

win-api-14

Pokud chceme mít aplikaci založenou pouze na hlavním dialogovém okně, zdrojový kód bude jednodušší než v případě vytváření běžného "overlapped" okna, jako jsme to dělali dosud. Již jsme si řekli v předchozích dílech, pro dilogové okno neregistrujeme vlastní třídu a nebude zde ani smyčka zpráv, protože celá funkce WinMain bude obsahovat vyvolání modálního dialogu funkcí DialogBox:

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst,
  LPSTR lpCmdLine, int nShow)
{
  return DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG_MAIN),
    NULL, (DLGPROC)DialogProc);
}

Vlastní funkčnost prvků dialogu budeme realizovat v proceduře dialogu, přesněji řečeno pro větší přehlednost budeme volat jednotlivé samostatné funkce, představující handlery zpráv od ovládacích prvků. Procedura okna dialogu je obdobná proceduře "běžného" okna s tím zjednodušením, že nevoláme funkci DefWindowProc pro výchozí zpracování zprávy, ale vrátíme hodnotu FALSE. Pokud zprávu zpracováváme zcela ve vlastní režii, vrátíme hodnotu TRUE. Podívejme se tedy na celou proceduru dialogu a začneme vysvětlováním jednotlivých handlerů:

INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch (uMsg)
  {
    case WM_COMMAND:
      switch ( LOWORD(wParam) )
      {
        case IDOK:
        case IDCANCEL:
          EndDialog(hwndDlg, LOWORD(wParam));
          break;
        case IDC_ADD_TEXT:
          OnAddText(hwndDlg, wParam, lParam);
          break;
        case IDC_DELETE_TEXT:
          OnDeleteText(hwndDlg, wParam, lParam);
          break;
      }
      break;
  }
  return FALSE;
}

Budeme chtít na stisknutí tlačítka přidat aktuální obsah edit-boxu do položek list-boxu a dále na druhé tlačítko vybranou položku list-boxu odstranit a její text přidat do combo-boxu nadepsaného "Odebrané položky". Aktuální počet položek list-boxu pak budeme zobrazovat v prvku typu "static".

Veškeré ovládání list-boxu spočívá v posílání příslušných zpráv, popřípadě zjištění návratové hodnoty funkce SendMessage. Uvedu zde přehled těch zpráv, které budeme používat:

LB_ADDSTRING - slouží pro přidání jednoduchého textu do list-boxu. Přidávaný text je v parametyru lParam.

b>LB_GETCOUNT - návratová hodnota zprávy určuje aktuální počet položek list-boxu.

LB_GETCURSEL - vrací index vybrané položky (počítáno od 0) nebo -1, pokud není vybraná žádná položka.

LB_SETCURSEL - nastavuje vybranou položku, její index je v parametru wParam

LB_DELETESTRING - odstraní položku s indexem určeným parametrem wParam.

LB_GETTEXT - naplní nám buffer (parametr lParam) obsahem položky na indexu určeném parametem wParam.

V případě combo-boxu je ovládání obdobné s tím, že zprávy combo-boxu začínají na CB_ místo LB_ jako u list-boxu.

Ještě poznámku k zobrazování číselné hodnoty (zde počet položek list-boxu) v prvku static. V tom případě musíme u vybraného static prvku změnit identifikátor z defaultního IDC_STATIC na nějakou vlastní hodnotu a poté můžeme použít funkci SetDlgItemInt:

BOOL SetDlgItemInt(
  HWND hDlg,       // handle dialogu
  int nIDDlgItem,  // identifikátor prvku
  UINT uValue,     // hodnota požadovaná ke zobrazení
  BOOL bSigned     // určuje zda jde o číslo se znaménkem
);

Takto vybaveni se již můžeme podívat na realizaci přidání položky do list-boxu:

void OnAddText(HWND hwndDlg, WPARAM wParam, LPARAM lParam)
{
  TCHAR chText[150];
  GetDlgItemText(hwndDlg, IDC_EDIT, chText, 150);
  SendDlgItemMessage(hwndDlg, IDC_LIST_BOX, LB_ADDSTRING, 0,
    (LPARAM)chText);
  int count = SendDlgItemMessage(hwndDlg, IDC_LIST_BOX, LB_GETCOUNT, 0, 0);
  SetDlgItemInt(hwndDlg, IDC_LB_COUNT, count, FALSE);
}

Odebrání a přidání odebírané položky do combo-boxu vypadá pak následovně:

void OnDeleteText(HWND hwndDlg, WPARAM wParam, LPARAM lParam)
{
  TCHAR chText[150];
  int curSel = (int)SendDlgItemMessage(hwndDlg, IDC_LIST_BOX, LB_GETCURSEL, 0, 0);
  if ( curSel < 0 ) // není vybraná žádná položka (dostaneme hodnotu -1) return;
  SendDlgItemMessage(hwndDlg, IDC_LIST_BOX, LB_GETTEXT, curSel, (LPARAM)chText);
  SendDlgItemMessage(hwndDlg, IDC_COMBO, CB_ADDSTRING, 0, (LPARAM)chText);
  SendDlgItemMessage(hwndDlg, IDC_LIST_BOX, LB_DELETESTRING, curSel, 0);
  if ( curSel >= SendDlgItemMessage(hwndDlg, IDC_LIST_BOX, LB_GETCOUNT, 0, 0))
    curSel--;
  SendDlgItemMessage(hwndDlg, IDC_LIST_BOX, LB_SETCURSEL, curSel, 0);
  SendDlgItemMessage(hwndDlg, IDC_COMBO, CB_SETCURSEL, 0, 0);
  int count = SendDlgItemMessage(hwndDlg, IDC_LIST_BOX, LB_GETCOUNT, 0, 0);
  SetDlgItemInt(hwndDlg, IDC_LB_COUNT, count, FALSE);
}

Doprovodný projekt je ke stažení zde: win_api_14.zip

Rozšiřujeme znalost GDI.

V tomto pokračování na čas opustíme dialogové prvky a podíváme se trochu více na GDI, tedy grafické rozhraní. Zatím jsme se naučili pouze vypisovat text a měnit jeho základní vlastnosti, tedy barvu písma a pozadí. Samozřejmě výraz vlastnosti textu zde není z programátorského pohledu přesný, neboť uvedené vlastnosti se nastavují kontextu zařízení (HDC) a nikoli samotnému textu v kreslící funkci.

Dnešní doprovodný projekt jsem založil pomocí wizarda s tím, že jsem si nechal vygenerovat kostru aplikace (je to defaultní nastavení, když zakládáte nový Win32 projekt - ve Visual Studiu NET). Jednak si ušetříme psaní či kopírování kódu, který už umíme (procedura okna, registrace třídy apod) a hlavně si řekneme o některých "úskalích" editoru zdrojů. Takto vygenerované zdroje mají totiž nastavený jazyk na angličtinu. Když přidáváme nové zdroje, jsou již nastaveny na češtinu (samozřejmě v české verzi Windows). Je proto třeba zdrojům s angličtinou nastavit (ve vlastnostech příslušného zdroje) jazyk na češtinu, a to ještě před tím, než do tohoto zdroje budeme vkládat nějaké texty obsahující diakritiku.

Ale nyní již ke GDI. Ukážeme si nejprve kreslení čar. Budeme například chtít, aby při stisknutím levého tlačítka myši se nakreslila čára spojující tento bod s předchozím bodem kliknutí. Postupně si tuto funkci rozšíříme o různé typy čar. K vykreslení úsečky složí funkce LineTo:

BOOL LineTo(
  HDC hdc,    // handle kontextu zařízení
  int nXEnd,  // x-souřadnice koncového bodu
  int nYEnd   // y-souřadnice koncového bodu
);

Tato funkce vykreslí čáru z aktuální pozice do bodu určeného souřadnicemi nXEnd a nYEnd a současně nastaví aktuální bod na tyto souřadnice. Při získání kontextu zařízení (třeba pomocí GetDC) je aktuální bod nastaven na souřadnice (0,0). Pokud chceme změnit aktuální bod bez kreslení čáry, máme k dispozici funkci MoveToEx:

BOOL MoveToEx(
  HDC hdc,          // handle kontextu zařízení
  int X,            // x-souřadnice nového "aktuálního bodu"
  int Y,            // y-souřadnice
  LPPOINT lpPoint   // původní "aktuální bod"
);

Jak je zřejmé, tato funkce kromě nastavení aktuálního bodu nám vrátí v parametry lpPoint souřadnice původního bodu. Pokud nás tyto původní souřadnice nezajímají, můžeme jako parametr lpPoint uvést NULL.

Vzhledem k tomu, že na stisknutí myši budeme vždy znova získávat kontext zařízení (GetDC), budeme si koncový bod předchozí čáry ukládata do proměnné ptDraw, kterou si na počátku vynulujeme:

POINT ptDraw = {0,0};

Nyní se již můžeme podívat, jak bude vypadat ta nejjednodušší varianta kreslící funkce, kterou budeme volat z procedury okna při přijetí zprávy WM_LBUTTONDOWN s tím, že si předáme parametry, které budeme (již nyní i později) potřebovat:

void MyDrawLine(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
  POINTS point = MAKEPOINTS(lParam);
  HDC hdc = GetDC(hWnd);
  MoveToEx(hdc, ptDraw.x, ptDraw.y, NULL);
  LineTo(hdc, point.x, point.y);
  ptDraw.x = point.x;
  ptDraw.y = point.y;
  ReleaseDC(hWnd, hdc);
}

Výsledkem nyní bude posloupnost navazujících úseček černé barvy a tloušťce 1 pixelu. Pokud budeme chtít měnit barvu, tloušťku popřípadě další vlastnosti čáry, musíme se seznámit s objektem GDI, zvaným pero (Pen). Tento objekt definující vlastnosti čáry, musíme nejprve vytvořit, a získaný handle objektu (typ HPEN) pak nastavíme kontextu zařízení. K vytvoření pera máme k dispozici nejjednodušší funkci CreatePen:

HPEN CreatePen(
  int fnPenStyle,    // styl pera
  int nWidth,        // šířka pera
  COLORREF crColor   // barva pera
);

Parametr fnPenStyle v určuje, zda se jedná o běžnou spojitou čáru či některou z čar přerušovaných, popř. další vlastnost, o které se dozvíme později. Pro spojitou čáru použijeme hodnotu PS_SOLID. Další možnosti definují čáru tečkovanou, čárkovanou nebo kombinace těchto vlastností. Nebudu zde opisovat celý přehled, najdete jej v dokumentaci u popisu této funkce, zde si kromě spojité čáry vyzkoušíme čáru tečkovanou.

Parametr nWidth určuje šířku pera. V nejjednodušším případě v pixelech. Co jsou to logické jednotky (uvedené v dokumentaci) si řekneme někdy později.

Barvu pera pak učuje parametr crColor nám již známého typu COLORREF.

Takto vytvořené pero musíme po jeho použití odstranit fukcí DeleteObject:

BOOL DeleteObject(
  HGDIOBJ hObject   // handle to graphic object
);

Možná se zeptáte, proč zde zmiňuji zrušení pera, když jsme ho ještě nepoužili. Je totiž možná dobrý zvyk (který samozřejmě nikomu nevnucuji, ale sám se ho držím) v podobných případech (dalším jsou třeba funkce GetDC a ReleaseDC) si nejprve napsat "začátek a konec" a mezi tyto řády pak psát kód tento objekt používající. Snížím tak pravděpodobnost, že po napsání zejména nějakého delšího kódu, zapomenu vytvořený objekt (spíše více objektů) na konci zrušit. Takže nyní k tomu "uprostřed". Pokud chceme vytvořené pero použít, musíme ho vybrat do příslušného kontextu zařízení funkcí SelectObject:

HGDIOBJ SelectObject(
  HDC hdc,          // handle kontextu zařízení
  HGDIOBJ hgdiobj   // handle grafického objektu
);

Tuto funkci budeme používat velmi často. Slouží totiž, jak napovídá obecný typ handle grafického objektu (HGDIOBJ) nejen pro výběr pera, ale i pro ostatní grafické objekty, ke kterým se postupně dostaneme. Upravme si tedy na závěr náš program tak, aby pokud při kliknutí myši držíme klávesu <Ctrl> se čára kreslila modrá s tloušťkou 4 pixely.

void MyDrawLine(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
  POINTS point = MAKEPOINTS(lParam);
  HDC hdc = GetDC(hWnd);
  HPEN hPen = CreatePen(PS_SOLID, 4, 0x00FF0000);
  if ( wParam & MK_CONTROL )
    SelectObject(hdc, hPen);
  MoveToEx(hdc, ptDraw.x, ptDraw.y, NULL);
  LineTo(hdc, point.x, point.y);
  ptDraw.x = point.x;
  ptDraw.y = point.y;
  ReleaseDC(hWnd, hdc);
  DeleteObject(hPen);
}

Jeden z možných výsledků vidíte na obrázku:

win-api-15

Zjištění zda je v okamžiku stlačení tlačítka myši stisknuta některá klávesa, popřípadě jiné tlačítko myši, provedeme otestováním parametru wParam na přítomnost jedné z možných hodnot. Jejich popis i symbolické konstanty naleznete v dokumentaci v popisu zprávy WM_LBUTTONDOWN. Stejně je tomu i u dalších "myších zpráv", jako třeba WM_RBUTTONUP apod.

Samozřejmě celá ukázka v žádném případě není ani zárodkem nějakého kreslícího programu. Nakreslené čáry zůstanou v okně pouze do doby, než dojde k jeho překreslení, neboť toto kreslení nemá s handlerem zprávy WM_PAINT žádnou vazbu.

Doprovodný projekt je ke stažení zde: win_api_15.zip

Kreslení čárových objektů a další typy per.

V minulém článku jsem si ukázali úplné základy kreslení čárových objektů a použití per. Dnes si ukážeme další funkce pro kreslení čárových objektů a předvedeme další typy per. Opět si založíme projekt typu "Win32 aplikace" a necháme si vygenerovat jeho"kostru". Na rozdíl od minulého článku budeme dnes kreslící funkce používat v handleru zprávy WM_PAINT, takže požadovaný grafický výsledek se bude na ploše okna automaticky překreslovat bez ohledu na akce uživatele, jako tomu bylo v minulém článku. Podívejme se nejprve na výsledek a pak si postupně popíšeme jak je ho dosaženo:

win-api-16

Nebudeme zde popisovat a demonstrovat úplný přehled všech funkcí kreslících čárové objekty, ukážeme si 2 z nich jako ukázku. Další si můžete sami vyzkoušet podle popisu v dokumentaci. Jako první si tedy ukážeme funkci, která vykreslí sadu navazujících úseček na základě definovaných bodů. Jde o funkci PolyLine:
BOOL Polyline(
  HDC hdc,            // handle to kontextu zařízení
  CONST POINT *lppt,  // body konců úseček
  int cPoints         // počet návazných bodů
);
Příklad použití této funkce je modrá hvězda na screen-shotu, nakreslená takto:
void DrawPolyLine(HDC hdc)
{
  HPEN hPen, hpOld;
  POINT MyPoints[6] = {50,2, 2,98, 98,33, 2,33, 98,98, 50,2};
  hPen = CreatePen(PS_SOLID, 4, 0x00FF0000);
  hpOld = (HPEN)SelectObject(hdc, hPen);
  Polyline(hdc, MyPoints, 6);
  // Před zrušením objektu ho vyjmeme z DC
  SelectObject(hdc, hpOld);
  DeleteObject(hPen);  
}
Dále vidíme kružnici, skládající se ze 4 navazujících oblouků, odlišených vybraným perem. Jde o demonstraci funkce AngleArc:
BOOL AngleArc(
  HDC hdc,            // handle kontextu zařízení
  int X,              // x-souřadnice středu kružnice
  int Y,              // y-souřadnice středu kružnice
  DWORD dwRadius,     // poloměr kružnice
  FLOAT eStartAngle,  // úhel počátku oblouku
  FLOAT eSweepAngle   // úhel oblouku
);
Tato funkce vykreslí oblouk na kružnici definované souřadnicemi středu a poloměrem. Parametr eStartAngle určuje úhel počátku oblouku ve stupních měřeno od osy x, tedy úhel 0 je bod na pravém okraji kružnice. Délka oblouku je určena úhlem eSweepAngle (také ve stupních, tedy 90 je čtvrt kružnice). Podívejme se opět na příklad vykreslení oblouků v naší ukázce:
void DrawAngles(HDC hdc)
{
  HPEN hPen1, hPen2, hpOld;
  hPen1 = CreatePen(PS_SOLID, 4, 0x0000FF00);
  hPen2 = CreatePen(PS_DASHDOT, 1, 0x000000FF);
  hpOld = (HPEN)SelectObject(hdc, hPen2);
  MoveToEx(hdc, 250, 50, NULL);
  AngleArc(hdc, 200, 50, 50, 0, 90);
  SelectObject(hdc, hPen1);
  AngleArc(hdc, 200, 50, 50, 90, 90);
  SelectObject(hdc, hPen2);
  AngleArc(hdc, 200, 50, 50, 180, 90);
  SelectObject(hdc, hPen1);
  AngleArc(hdc, 200, 50, 50, 270, 90);
  SelectObject(hdc, hpOld);
  DeleteObject(hPen1);
  DeleteObject(hPen2);
}
K oběma ukázkám ještě poznámku o korektním rušení grafických objektů (v našem případě per): Dříve než objekt zrušíme funkcí DeleteObject, musíme ho vyjmout z kontextu zařízení. Toto provedeme nejlépe vložením původního objektu stejného typu (tedy například pera). Již víme, že funkce SelectObjekt vrátí hodnotu objektu, který byl novým objektem nahrazen. Tuto hodnotu (handle pera typu HPEN) si při prvním vložení vlastního objektu uložíme a před zrušením našeho objektů ji opět vložíme. Jako poslední ukázka je vykreslení různých nespojitých typů per. Jak můžete vidět na obrázku, doprovodný text je umístěn tak, že jeho střed je (plus/mínus 1 pixel v případě lichých čísel) přesně na y-ové souřadnici dané čáry. Samozřejmě metoda pokus/omyl by v daném případě dosáhla téhož. Pokud ale chceme mít kód nezávislý na daném systémovém fontu, musíme na to trochu sofistikovaněji s využitím funkce GetTextExtentPoint32, kterou můžeme zjistit rozměry textu s fontem, který je aktuálně vybrán v daném kontextu zařízení. Funkce nám naplní strukturu SIZE, určující "opsaný bdélník" textového řetězce.
BOOL GetTextExtentPoint32(
  HDC hdc,           // handle kontextu zařízení
  LPCTSTR lpString,  // textový řetězec
  int cbString,      // počet znaků
  LPSIZE lpSize      // rozměry obdélníka textu
);
V naší ukázce jsem vytvořil funkci pro vykreslení jednoho typu pera, která nakreslí daným typem úsečku a vedle ní příslušný popis:
void DrawPenType(HDC hdc, LPCTSTR lpText, int nType, int yPos)
{
  HPEN hPen, hpOld;
  MoveToEx(hdc, 10, yPos, NULL);
  hPen = CreatePen(nType, 1, 0x00);
  hpOld = (HPEN)SelectObject(hdc, hPen);
  LineTo(hdc, 100, yPos); 
  SIZE size;
  GetTextExtentPoint32(hdc, lpText, strlen(lpText), &size);
  TextOut(hdc, 110, yPos-(size.cy/2),
    lpText, lstrlen(lpText));
}
A by byl ukázkový kód kompletní, na závěr handler zprávy WM_PAINT, ze kterého jsou volány zde uvedené funkce:
void OnPaint(HDC hdc)
{
  DrawPolyLine(hdc);
  DrawAngles(hdc);
  int yPos = 120;
  DrawPenType(hdc, "PS_DASH", PS_DASH, yPos);
  yPos += 20;
  DrawPenType(hdc, "PS_DOT", PS_DOT, yPos);
  yPos += 20;
  DrawPenType(hdc, "PS_DASHDOT", PS_DASHDOT, yPos);
  yPos += 20;
  DrawPenType(hdc, "PS_DASHDOTDOT", PS_DASHDOTDOT, yPos);
}
V příštím pokračování se budeme zabývat kreslením plošných objektů a použití štětců. Doprovodný projekt je ke stažení zde: win_api_16.zip

Kreslení plošných objektů. Štětce.

Zatím jsme se zabývali pouze takříkajíc čárovými grafickými objekty. V tomto pokračování se seznámíme s tím, jak kreslit plošné objekty a definovat vlastnosti jejich výplně pomocí štětců.

Geometrické útvary, které budeme dnes kreslit, budou mít definovány 2 typy "vlastností". Čáry tvořící jejich okraj, budou mít vlastnosti definované nám již známými pery a výplně štětci (brush), se kterými se seznámíme dnes. Princip použití štětců je stejný jako per. Nejdříve si jednou z příslušných funkcí vytvoříme objekt typu štětec (handle HBRUSH), ten pak vybereme pomocí funkce SelectObject do kontextu zařízení (HDC) a poté budou mít nakreslené grafické objekty výplň tvořenou aktuálně vybraným štětcem.

Nejjednodušším tvarem je obyčejný obdélník. Pokud chceme nakreslit obdélník, jehož okraj je kreslený aktuálně vybraným perem a výplň je tvořená aktuálně vybraným štětcem, použijeme funkci Rectangle:

BOOL Rectangle(
  HDC hdc,         // handle kontextu zařízení
  int nLeftRect,   // x-souřadnice levého horního rohu
  int nTopRect,    // y-souřadnice levého horního rohu
  int nRightRect,  // x-souřadnice pravého dolního rohu
  int nBottomRect  // y-souřadnice pravého dolního rohu
);

Dalším základním tvarem je elipsa nebo kružnice jako zvláštní případ elipsy, kterou kreslíme pomocí funkce Ellipse:

BOOL Ellipse(
  HDC hdc,        // handle kontextu zařízení
  int nLeftRect,  // x-souřadnice levého horního rohu opsaného obdélníka
  int nTopRect,   // y-souřadnice
  int nRightRect, // x-souřadnice pravého dolního rohu opsaného obdélníka
  int nBottomRect // y-souřadnice
);

Uveďme si zde ještě jeden plošný útvar kterým je obdélník (čtverec) se zaoblenými rohy, kreslený funkcí RoundRect:

BOOL RoundRect(
  HDC hdc,         // handle kontextu zařízení
  int nLeftRect,   // x-souřadnice levého horního rohu opsaného obdélníka
  int nTopRect,    // y-souřadnice
  int nRightRect,  // x-souřadnice pravého dolního rohu opsaného obdélníka
  int nBottomRect, // y-souřadnice
  int nWidth,      // šířka elipsy tvořící zaoblené rohy
  int nHeight      // výška elipsy tvořící zaoblené rohy
);

Další plošné geometrické objekty, resp. funkce pro jejich kreslení jsou samozřejmě popsány v dokumentaci. Pro nás je nyní důležité seznámit se se štětci tvořícími výplň plošných objektů. Nejjednodušším případem je štětec tvořený jednou barvou, který vytvoříme funkcí CreateSolidBrush:

HBRUSH CreateSolidBrush(
  COLORREF crColor   // definice barvy
);

Handle typu HBRUSH vrácený touto funkcí pak vybereme již známou funkcí SelectObject do kontextu zařízení (HDC) a všechny další kreslené plošné objekty pak budou mít výplň tvořenou tímto štětcem. Ukážeme si příklad, ve kterém se současně ještě vrátíme k perům, přesněji řečeno k demonstraci stylu pera PS_INSIDEFRAME. Tento styl pera určuje, že pokud je kreslen obrazec, který definujeme opsaným obdélníkem (jako výše zmíněná elipsa/kružnice) a pero má šířku větší než 1 pixel, je obrazec nakreslen tak že se celý vejde dovnitř tohoto obdélníka. Bez tohoto stylu k okrajům obdélníka přiléhá střed čáry a tedy polovina šířky čáry pera je mimo tyto hranice. Podívejme se na příklad, který toto demonstruje:

win-api-17-1

V obou případech (jak uvidíte z výpisu kódu) je kružnice (kreslená funkcí Ellipse) definována stejným (rozměrově) opsaným obdélníkem. Rozdíl je ve vybraném peru. Pero použité pro levý obrázek má ve stylu příznak PS_INSIDEFRAME, tj je vytvořené takto:

hPen = CreatePen(PS_SOLID | PS_INSIDEFRAME, 20, 0x00FF8080);

Současně je ukázán příklad obdélníka kresleného systémovým černým perem a vyplněného systémovým bílým štětcem. Je to ukázka použítí funkce GetStockObject:

HGDIOBJ GetStockObject(
  int fnObject   // typ objektu
);

Touto funkcí můžeme získat již existující handle na některý ze systémových objektů. Jejich úplný seznam naleznete v dokumentaci této funkce. Zbytek kódu pak ukazuje vytvoření spojitého pera, a jeho použití jako výplň kružnice. Zde je tedy opis kódu, volaného z handleru zprávy WM_PAINT, realizujícího výše uvedený obrázek:

void InsideFrameDemo(HDC hdc)
{
  HPEN hPen;
  HBRUSH hBrush;
  hPen = CreatePen(PS_SOLID | PS_INSIDEFRAME, 20, 0x00FF8080);
  hBrush = CreateSolidBrush(0x0080FFFF);
  SelectObject(hdc, GetStockObject(BLACK_PEN));
  Rectangle(hdc, 15, 15, 115, 115);
  SelectObject(hdc, hPen);
  SelectObject(hdc, hBrush);
  Ellipse(hdc, 15, 15, 115, 115);
  SelectObject(hdc, GetStockObject(BLACK_PEN));
  DeleteObject(hPen);
  SelectObject(hdc, GetStockObject(WHITE_BRUSH));
  Rectangle(hdc, 140, 15, 240, 115);
  hPen = CreatePen(PS_SOLID, 20, 0x00FF8080);
  SelectObject(hdc, hPen);
  SelectObject(hdc, hBrush);
  Ellipse(hdc, 140, 15, 240, 115);
  SelectObject(hdc, GetStockObject(WHITE_PEN));
  DeleteObject(hPen);
  DeleteObject(hBrush);
}

Dále si ještě ukážeme příklad štětce, tvořeného mřížkou zvolené barvy a typu. K tomu nám slouží funkce CreateHatchBrush:

HBRUSH CreateHatchBrush(
  int fnStyle,      // styl mřížky
  COLORREF clrref   // barva čar mřížky
);

Parametr fnStyle určuje typ mřízky, může mít jednu z hodnot, které jsou vypsány na následujícím obrázku, z něhož je nejlépe patrný jejich význam:

win-api-17-3

Kód toto realizující vypadá následovně:

void ShowBrush(HDC hdc, int Style, int x, int y, LPCTSTR lpText)
{
  RECT rect;
  rect.left = x;
  rect.top = y;
  rect.right = rect.left + 140;
  rect.bottom = rect.top + 80;
  HBRUSH hb = CreateHatchBrush(Style, 0x00A00000);
  SelectObject(hdc, hb);
  FillRect(hdc, &rect, hb);
  DrawText(hdc, lpText, lstrlen(lpText), &rect,
    DT_SINGLELINE | DT_CENTER | DT_VCENTER);
  DeleteObject(hb);
}

void OnPaint(HDC hdc)
{
  ShowBrush(hdc, HS_BDIAGONAL, 10, 10, "HS_BDIAGONAL");
  ShowBrush(hdc, HS_CROSS, 160, 10, "HS_CROSS");
  ShowBrush(hdc, HS_DIAGCROSS, 310, 10, "HS_DIAGCROSS");
  ShowBrush(hdc, HS_FDIAGONAL, 10, 100, "HS_FDIAGONAL");
  ShowBrush(hdc, HS_HORIZONTAL, 160, 100, "HS_HORIZONTAL");
  ShowBrush(hdc, HS_VERTICAL, 310, 100, "HS_VERTICAL");
}

Na závěr ještě ukázku použití funkce Rect  a RoundRect, opět ve spojení s mřížkovaným štětcem a vlastním perem:

win-api-17-2

Kód tohoto kreslení:

void HatchBrush(HDC hdc)
{
  HBRUSH hBrush, hbOld;
  HPEN hPen, hpOld;
  hBrush = CreateHatchBrush(HS_CROSS, 0x000000A0);
  hPen = CreatePen(PS_SOLID, 8, 0x00D00000);
  hbOld = (HBRUSH)SelectObject(hdc, hBrush);
  hpOld = (HPEN)SelectObject(hdc, hPen);
  Rectangle(hdc, 10,10, 180, 120);
  SelectObject(hdc, hbOld);
  DeleteObject(hBrush);
  hBrush = CreateHatchBrush(HS_DIAGCROSS, 0x0000A000);
  SelectObject(hdc, hBrush);
  RoundRect(hdc, 10,140, 180, 230, 50, 50);
  SelectObject(hdc, hpOld);
  DeleteObject(hPen);
}

Doprovodný projekt je ke stažení zde: win_api_17.zip

Nastavení písma. Fonty.

V tomto pokračování se seznámíme s dalším objektem GDI, kterým bude font (handle typu HFONT). Ve všech dosavadních ukázkách jsme volbu písma nechávali na systému, který nám při výpisu textu do kontextu zařízení vybral příslušný systémový font. Jediné, co jsme mohli ovlivnit, byla barva písma a pozadí. A to proto, jak jsme si řekli, že tyto atributy jsou vlastnostmi (nikoli objekty), které jsme nastavili kontextu zařízení (HDC). Fonty (HFONT) jsou však objekty obdobné perům (HPEN) a štětcům (HBRUSH), se kterými jsme se již seznámili. Znamená to tedy, že musíme nějakým způsobem vytvořit (nebo získat již existující) handle objektu HFONT, přiřadit ho do kontextu zařízení funkcí SelectObject a poté veškeré následné výstupy textu budou používat tento font. Ukážeme si jak dosáhnout třeba takovýto výsledek:

win-api-18-1

Vytvoření fontu

Pro přímé vytvoření fontu můžeme použít funkci CreateFont:

HFONT CreateFont(
  int nHeight,               // height of font
  int nWidth,                // average character width
  int nEscapement,           // angle of escapement
  int nOrientation,          // base-line orientation angle
  int fnWeight,              // font weight
  DWORD fdwItalic,           // italic attribute option
  DWORD fdwUnderline,        // underline attribute option
  DWORD fdwStrikeOut,        // strikeout attribute option
  DWORD fdwCharSet,          // character set identifier
  DWORD fdwOutputPrecision,  // output precision
  DWORD fdwClipPrecision,    // clipping precision
  DWORD fdwQuality,          // output quality
  DWORD fdwPitchAndFamily,   // pitch and family
  LPCTSTR lpszFace           // typeface name
);

Jak je vidět, je třeba zadat různé parametry písma. Jejich přesný význam a definované konstanty pro jednotlivé hodnoty naleznete v nápovědě a nebudu je zde rozepisovat. Řekněme si však o další, někdy šikovnější možnosti vytvoření fontu. Je jí funkce CreateFontIndirect:

HFONT CreateFontIndirect(
  CONST LOGFONT* lplf   // vlastnosti písma
);

Když se však podíváte na strukturu, LOGFONT, zjistíte, že její prvky odpovídají parametrům funkce CreateFont. V čem je tedy výhoda? Spočívá v tom, že si můžeme nejprve nechat "předvyplnit" strukturu LOGFONT parametry nějakého systémového písma, a poté pouze změnit ty vlastnosti, které požadujeme. K tomu použijeme již zmíněnou funkci GetObject:

int GetObject(
  HGDIOBJ hgdiobj,  // handle grafického objektu
  int cbBuffer,     // velikost bufferu
  LPVOID lpvObject  // buffer pro informace o objektu
);

Handle na některý systémový font můžeme získat pomocí funkce GetStockObject:

HGDIOBJ GetStockObject(
  int fnObject   // typ objektu
);

Pokud chceme získat výchozí font, který systém Windows používá pro grafické objekty, použijeme hodnotu DEFAULT_GUI_FONT.

Použití fontu v kontextu zařízení

Ukažme si nyní na konkrétním příkladě aplikaci těchto poznatků. Nejdříve se podívejte na kompletní výpis handleru zprávy WM_PAINT, který realizuje to, co vidíte na úvodním screen-shotu:

void OnPaint(HDC hdc)
{
  TCHAR chText[100];
  LOGFONT logFont;
  HFONT hFont;
  GetObject(GetStockObject(DEFAULT_GUI_FONT), sizeof(logFont), &logFont);
  logFont.lfItalic = TRUE;
  
  hFont = CreateFontIndirect(&logFont);
  SelectObject(hdc, hFont);
  lstrcpy(chText, TEXT("Výchozí font s přidaným sklonem písma"));
  TextOut(hdc, 40, 10, chText, lstrlen(chText));
  DeleteObject(hFont);
  
  logFont.lfHeight = -18;
  logFont.lfItalic = FALSE;
  lstrcpy(logFont.lfFaceName, TEXT("Comic Sans MS"));
  hFont = CreateFontIndirect(&logFont);
  SelectObject(hdc, hFont);
  SetTextColor(hdc, 0x00D00000);
  lstrcpy(chText, TEXT("Zvětšíme a změníme písmo"));
  TextOut(hdc, 40, 30, chText, lstrlen(chText));
  DeleteObject(hFont);

  logFont.lfEscapement = 1800;
  lstrcpy(logFont.lfFaceName, TEXT("Comic Sans MS"));
  hFont = CreateFontIndirect(&logFont);
  SelectObject(hdc, hFont);
  SetTextColor(hdc, 0x000000D0);
  lstrcpy(chText, TEXT("Nyní píšeme vzhůru nohama"));
  SIZE size;
  GetTextExtentPoint32(hdc, chText, lstrlen(chText), &size);
  TextOut(hdc, 40 + size.cx, 90, chText, lstrlen(chText));
  DeleteObject(hFont);

  logFont.lfEscapement = 900;
  logFont.lfHeight = -22;
  lstrcpy(logFont.lfFaceName, TEXT("Times New Roman"));
  hFont = CreateFontIndirect(&logFont);
  SelectObject(hdc, hFont);
  SetTextColor(hdc, 0x0008000);
  lstrcpy(chText, TEXT("Svislý text "));
  GetTextExtentPoint32(hdc, chText, lstrlen(chText), &size);
  TextOut(hdc, 10, 10 + size.cx, chText, lstrlen(chText));
  DeleteObject(hFont);
}

Získání handle kontextu zařízení v proceduře okna by jste již měli znát z minulých dílů, pro úplnost ho zde uvedu:

case WM_PAINT:
  hdc = BeginPaint(hWnd, &ps);
  OnPaint(hdc);
  EndPaint(hWnd, &ps);
  break;

Jak je vidět z výpisu, jde vždy o modifikaci požadované vlastnosti písma ve struktuře LOGFONT, použití funkce CreateFontIndirect a vybrání fontu do kontextu zařízení. Samozřejmě nesmíme zapomenout vytvořený font po použití vždy zrušit funkcí DeleteObject, a pak teprve toto handle použít pro vytvoření dalšího fontu. V ukázce vidíte také, jak vypsat písmo pod libovolným úhlem. V tom nejjednodušším případě (výchozím grafickém módu) nastavíme prvek lfEscapement na hodnotu vyjadřující sklon základny textu vůči ose x v desetinách stupně. Použitá hodnota 90 tedy představuje pravý úhel a 180 písmo otočené "vzhůru nohama".

Nastavení písma oknu (prvku na dialogu)

Nyní si ještě ukážeme, jak nastavit vlastní font některému prvku Windows. Mohl by jím být edit, button apod. Ukážeme si to u dialogu "about.box", kde máme prvek typu "STATIC". V editoru zdrojů musíme pouze změnit identifikátor static prvku na hodnotu jinou než je IDC_STATIC. Vše ostatní zařídíme v proceduře dialogu, kde při inicializaci (handler zprávy WM_INITDIALOG) vytvoříme vlastní font a nastavíme jej prvku pomocí zprávy WM_SETFONT. Před ukončením dialogu font opět zrušíme.

HFONT hfDialog;
LOGFONT logFont;

LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
  switch (message)
  {
  case WM_INITDIALOG:
    GetObject(GetStockObject(DEFAULT_GUI_FONT), sizeof(logFont), &logFont);
    logFont.lfHeight = -22;
    logFont.lfItalic = TRUE;
    logFont.lfUnderline = TRUE;
    lstrcpy(logFont.lfFaceName, TEXT("Times New Roman"));
    hfDialog = CreateFontIndirect(&logFont);
    SendDlgItemMessage(hDlg, IDC_FONT, WM_SETFONT, (WPARAM)hfDialog, (LPARAM)TRUE);
    return TRUE;

  case WM_COMMAND:
    if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) 
    {
      DeleteObject(hfDialog);
      EndDialog(hDlg, LOWORD(wParam));
      return TRUE;
    }
    break;
  }
  return FALSE;
}

Výsledek vidíte na obrázku:

win-api-18-2

Ještě poznámku ke kódu. Jak si můžete všimnout, pro poslání zprávy požadovanému prvku na dialogu, u něhož známe identifikátor ale nikoli handle, můžeme použít místo funkce SendMessage funkci SendDlgItemMessage:

LRESULT SendDlgItemMessage(
  HWND hDlg,      // handle dialogu
  int nIDDlgItem, // identifikátor prvku
  UINT Msg,       // zpráva
  WPARAM wParam,  // 1. parametr zprávy
  LPARAM lParam   // 2. parametr zprávy
);

kde zadáváme handle dialogu a identifikátor prvku. Pokud bychom opravdu potřebovali znát handle prvku, samozřejmě můžeme ho zjistit pomocí funkce GetDlgItem:

HWND GetDlgItem(
  HWND hDlg,       // handle dialogu
  int nIDDlgItem   // identifikátor prvku
);

která nám vrátí handle, na základě handle rodičovského dialogu a identifikátoru prvku.

Doprovodný projekt je ke stažení zde: win_api_18.zip

Bitmapa a její vykreslení.

V tomto pokračování se seznámíme s dalším objektem GDI - bitmapou. Ukážeme si jak vykreslit bitmapu uloženou v prostředcích programu do okna. V té souvislosti si ukážeme použití štětců tvořených vzorkem z bitmapy.

Na obrázku vidíte konečný výsledek dnešního výkladu: jde o okno, jehož štětec pozadí je definován vzorkem z bitmapy a v handleru zprávy WM_PAINT je uprostřed okna vykreslena bitmapa (fotografie v true-color barevné hloubce).

win-api-19

Co je to bitmapa ?

Z pohledu programátora je bitmapa grafický objekt podobný jako nám již známá pera nebo štětce. Znamená to že musíme "nějakým způsobem" získat handle bitmapy (ať už již existující nebo námi vytvořené), poté tento handle vybrat do kontextu zařízení, s kterým pak můžeme "pracovat". Jednotlivé pixely tohoto kontextu zařízení (obsahujícího bitmapu) pak představují barevné body tvořící bitmapu. Barva každého pixelu je určena třemi barevnými složkami - červená, zelená a modrá - každá z nich může mít definovanou intenzitu v rozsahu 0-255 (1 BYTE).

Nejjednodušším způsobem získání handle bitmapy je její načtení z prostředků (resources) programu funkcí LoadBitmap:

HBITMAP LoadBitmap(
  HINSTANCE hInstance,  // handle modulu
  LPCTSTR lpBitmapName  // jméno zdroje - bitmap
);

Tato funkce nám vrátí handle bitmapy. Často chceme získat nějaké informace o takto načtené bitmapě. Obvykle nás zajímají především její rozměry (z důvodu jejího umístění při kreslení). Protože bitmapa je grafický objekt, použijeme již známou funkci GetObject:

int GetObject(
  HGDIOBJ hgdiobj,  // handle grafického objektu
  int cbBuffer,     // velikost bufferu
  LPVOID lpvObject  // buffer pro informace o objektu
);

V případě bitmapy je bufferem pro informaci o objektu struktura BITMAP:

typedef struct tagBITMAP {
  LONG   bmType; 
  LONG   bmWidth; 
  LONG   bmHeight; 
  LONG   bmWidthBytes; 
  WORD   bmPlanes; 
  WORD   bmBitsPixel; 
  LPVOID bmBits; 
} BITMAP, *PBITMAP; 

Většinou chceme bitmapu vykreslit do kontextu zařízení okna. Musíme proto nejprve vytvořit vlastní paměťový kontext zařízení, do něj vybrat handle bitmapy a poté můžeme použít tu nejjednodušší funkci pro kopírování (případně další bitové operace) kontextů zařízení - BitBlt:

BOOL BitBlt(
  HDC hdcDest, // handle cílového DC
  int nXDest,  // x-souřadnice horního levého rohu
  int nYDest,  // y-souřadnice horního levého rohu
  int nWidth,  // šířka cílového obdélníka
  int nHeight, // výška cílového obdélníka
  HDC hdcSrc,  // handle zdrojového DC
  int nXSrc,   // x-souřadnice levého horního rohu
  int nYSrc,   // y-souřadnice
  DWORD dwRop  // typ rastrové operace
);

V tom nejjednodušším případě kopírování bitmapy do kontextu zařízení okna je cílovým hdcDest kontext okna a hdcSrc kontext bitmapy. 2 - 5 parametr definují obdélník v cílovém kontextu zařízení, do kteréhoje bitmapa kreslena a parametry nXSrc a nYSrc definují levý horní roh v bitmapě, od kterrého se začíná kreslit. Pokud je různý od souřadnice (0,0) jde o výřez z bitmapy.

Pokud tedy chceme vykreslit celou bitmapu, musíme nejprve zjistit její rozměry (funkcí GetObject), které pak uvedeme jako parametry nXDest a nYDest. Pro běžné kopírování je pak typ rastrové operace definován hodnotou SRCCOPY. O dalších možnostech si řekneme někdy příště, nyní si ukažme výpis kódu, který (v handleru zprávy WM_PAINT) realizuje to co vidíme na úvodním screen-shotu. Nejprve k pozadí okna. Již víme že každá třída okna má definovaný štětec, tvořící výplň jeho pozadí. Kromě již probíraných typů štětců existuje ještě štětec tvořený vzorkem bitmapy, který získáme funkcí CreatePatterBrush:

HBRUSH CreatePatternBrush(
  HBITMAP hbmp   // handle bitmapy
);

Jediné co potřebujeme, je opět handle bitmapy. V projektu jsou přidané do zdrojů 2 bitmapy (IDB_BACKGROUD a IDB_BITMAP1). Pozadí tedy definujeme při registraci třídy okna takto:

// .... kód registrace třídy okna
wcex.hbrBackground  = 
  CreatePatternBrush(LoadBitmap(hInstance, MAKEINTRESOURCE(IDB_BACKGROUND)))

Nyní již zbývá jen handler zprávy WM_PAINT:

void OnPaint(HDC hdc)
{
  BITMAP bitmap;
  RECT rect;
  GetClientRect(hwndMain, &rect);
  HDC hdcBitmap = CreateCompatibleDC(hdc);
  HBITMAP hBitmap = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP1));
  GetObject(hBitmap, sizeof(bitmap), &bitmap);
  SelectObject(hdcBitmap, hBitmap);
  int xPos = (rect.right - bitmap.bmWidth) / 2;
  int yPos = (rect.bottom - bitmap.bmHeight) / 2;
  BitBlt(hdc,
    xPos, yPos,
    bitmap.bmWidth,
    bitmap.bmHeight,
    hdcBitmap, 0,0, SRCCOPY);
  DeleteObject(hBitmap);
  DeleteDC(hdcBitmap);
}

Jak je vidět, po použití musíme opět bitmapu zrušit funkcí DeleteObject, stejně jako jiný grafický object. Pro vytvoření kontextu zařízení bitmapy jsem použit tu nejjednodušší funkci CreateComaptibleDC:

HDC CreateCompatibleDC(
  HDC hdc   // handle to DC
);

která nám vytvoří kontext zařízení se stejnými vlastnostmi jako zadaný existující kontext zařízení, v našem případě kontext okna.

Doprovodný projekt je ke stažení zde: win_api_19.zip

Další funkce pro práci s grafikou.

V minulém pokračování jsme si ukázali ten nejjednodušší způsob, jak vykreslit bitmapu. V tomto pokračování si ukážeme některé další funkce pro kreslení grafických objektů, které mají více možností. Podívejme se na výsledek dnešního ukázkového příkladu. Je zde opět vykreslena bitmapa (jiným způsobem než v minulém článku) a dále ikona v různých režimech zobrazení, a nakonec ikona o velikosti 48x48.

win-api-20

Pokud chceme například na grafický objekt (včetně výpisu textu) aplikovat efekty jako je například "disabled", tedy vykreslení objektu tak jak bývá obvyklé u zakázaných prvků, jako tlačítka na panelu nástrojů nebo texty i obrázky u zakázaných položek menu, můžeme použít funkci DrawState:

BOOL DrawState(
  HDC hdc,                     // handle kontextu zařízení
  HBRUSH hbr,                  // handle štětce
  DRAWSTATEPROC lpOutputFunc,  // callback funkce
  LPARAM lData,                // informace o grafickém objektu
  WPARAM wData,                // další info o objektu
  int x,                       // horizontální souřadnice
  int y,                       // vertikální souřadnice
  int cx,                      // šířka
  int cy,                      // výška
  UINT fuFlags                 // typ objektu a další volby
);

Tato funkce nám například v případě bitmapy umožňuje její přímé vykreslení bez nutnosti vytvářet pro ni kontext zařízení. Stačí nám pouze mít platný handle bitmapy. Kresleným objektem může být kromě bitmapy také ikona, kurzor nebo text. V parametru fuFlags máme pak možnost například specifikovat tyto parametry:

  • DSS_DISABLED - obrázek nebo text je vykreslen ve stavu "zakázán"
  • DSS_MONO - objekt je vykreslen monochromaticky s tím, že můžeme specifikovat handle štětce (parametr hbr) určující tento monochromatický štětec.

Při použití této funkce navíc nemusíme uvádět rozměry kresleného objektu, pokud uvedeme hodnoty 0, funkce sama vypočítá velikost objektu a vykreslí jej celý. Typ grafického objektu určíme v parametru fuFlags, který může nabývat například následujících hodnot:

  • DST_BITMAP - objektem je bitmapa. Handle bitmapy je v parametru lData.
  • DST_ICON - Ikona, handle ikony je lData.
  • DST_TEXT - Vypisujeme text. Parametr lData je adresa textového řetězce.
  • DST_PREFIXTEXT - Text může obsahovat akcelerátory, tedy znak '&' je považován za příkaz k podtržení následujícího znaku tak, jak se to používá u zkratkových kláves nabídek.

Dále si řekněme o univerzálnější a šikovnější funkci pro načítání bitmap, ikon a kurzorů, kterou je LoadImage:

HANDLE LoadImage(
  HINSTANCE hinst,   // handle  instance
  LPCTSTR lpszName,  // grafický objekt
  UINT uType,        // typ grafického objektu
  int cxDesired,     // požadovaná šířka
  int cyDesired,     // požadovaná výška
  UINT fuLoad        // volby načtení
);

Tato funkce umí například načítat všechny uvedené typy grafických objektů nejen ze zdrojů, ale také ze souboru. Uveďme si některé volby, které můžeme specifikovat v parametru fuLoad:

  • LR_LOADFROMFILE - načte objekt ze souboru místo ze zdrojů
  • LR_LOADTRANSPARENT - všechny pixely, které mají stejnou barevnou hodnotu jako první pixel (0,0) jsou nahrazeny barvou pozadí okna.
  • LR_MONOCHROME - načte obrázek černobíle

Dále máme možnost také u ikon a kurzorů specifikovat vlastní rozměry. Lze tak například načíst ikonu nebo kurzor rozměru 48x48, což je zejména ve Windows XP jeden z běžných rozměrů obrázku obsažených v ikoně.

V souvislosti s kreslením ikon či kurzorů si řekněme ještě o jedné funkci, kterou je DrawIconEx:

BOOL DrawIconEx(
  HDC hdc,                   // handle kontextu zařízení
  int xLeft,                 // x-ová souřadnice
  int yTop,                  // y-ová souřadnice
  HICON hIcon,               // handle ikony (kurzoru)
  int cxWidth,               // šířka ikony
  int cyWidth,               // výška
  UINT istepIfAniCur,        // pro animovaný kurzor index obrázku
  HBRUSH hbrFlickerFreeDraw, // brush pozadí, může být NULL
  UINT diFlags               // volby kreslení
);

Jak je patrné z parametrů, můžeme zde například vykreslit libovolný "krok" v animovaném kurzoru. Můžeme samozřejmě také specifikovat požadované výsledné rozměry obrázku, lze tak tedy ikonu či kurzor kreslit roztažené do libovolné velikosti.

Další možnosti všech uvedených funkcí naleznete samozřejmě v dokumentaci a můžete tedy sami dále experimentovat.

Nyní se podívejme na kód handleru WM_PAINT dnešního doprovodného příkladu:

void OnPaint(HDC hdc)
{
  HBITMAP hBitmap = (HBITMAP)LoadImage(hInst, "eso.bmp", IMAGE_BITMAP, 0,0,
      LR_DEFAULTSIZE | LR_LOADFROMFILE);
  DrawState(hdc, NULL, NULL, (LPARAM)hBitmap, 0, 10, 10, 0, 0, DST_BITMAP);
  DeleteObject(hBitmap);
  DrawState(hdc, NULL, NULL,
    (LPARAM)LoadIcon(hInst, MAKEINTRESOURCE(IDI_ICON1)),
    0, 190, 10, 0, 0, DST_ICON);
  HBRUSH hBrush = CreateSolidBrush(0x00A0A0FF);
  DrawState(hdc, hBrush, NULL, (LPARAM)LoadIcon(hInst, MAKEINTRESOURCE(IDI_ICON1)),
    0, 190, 50, 0, 0, DST_ICON | DSS_MONO);
  DeleteObject(hBrush);
  DrawState(hdc, NULL, NULL,
    (LPARAM)LoadIcon(hInst, MAKEINTRESOURCE(IDI_ICON1)),
    0, 190, 90, 0, 0, DST_ICON | DSS_DISABLED);
  HICON hIcon = (HICON)LoadImage(hInst, MAKEINTRESOURCE(IDI_ICON2),
    IMAGE_ICON, 48,48, LR_SHARED);
  DrawIconEx(hdc, 190, 130, hIcon, 0, 0, 0, NULL, DI_NORMAL);
}

Nejprve je zde ukázka načtení bitmapy ze souboru a její vykreslení (bez použití dalšího kontextu zařízení) pomocí funkce DrawState. Stejnou funkcí je také vykreslena ikona, v prvním případě v běžném stavu, ve druhém monochromaticky, přičemž je použit vlastní štětec definující monochromatickou barvu, a dále tatáž ikona jako "zakázaná". Na konec je nakreslena ikona obsahující obrázek velikosti 48 x 48 tak, že je načten právě tento obrázek. O tom, že je vybrán právě tento a nikoliv 32x32, který by byl roztažen, se lze přesvědčit z parametrů funkce DrawIconEx, kde jsou rozměry uvedeny jako 0, a tedy funkce použije velikost obrázku, který je načten.

Nakonec ještě důležitá poznámka pro spuštění projektu. Bitmapa "eso.bmp" musí být v adresáři, kde běží program, tedy obvykle \debug nebo \release. Já jsem ji přidal mezi zdrojové kódy a další soubory projektu. Samozřejmě můžete ve funkci LoadImage uvést libovolnou plnou cestu k vlastní bitmapě.

Doprovodný projekt je ke stažení zde: win_api_20.zip

Subclassing ovládacích prvků.

V tomto článku se opět vrátíme k dialogovým prvkům. Dost častým dotazem na diskusním fóru bývá téma, jak zabránit zavření dialogu například po stisknutí klávesy <Enter> v editačním poli na dialogu, nebo jak vůbec obecně reagovat po svém na libovolnou klávesu v edit-boxu nebo podobném dialogovém prvku. K tomuto účelu se musíme seznámit s takzvaným "subclassingem", což znamená "napíchnutí" se na proceduru okna libovolného okna (tedy i prvku na dialogu) v aplikaci. Když se nám toto podaří, pak z toho logicky vyplývá, že můžeme odchytit libovolnou zprávu tomuto prvku, a nejen ty oznamovací zprávy, které prvek posílá dialogu.

Jak tedy bude fungovat dnešní ukázka? Na dialogu jsou 2 běžné jednořádkové edit-boxy a jeden víceřádkový. Budeme chtít, aby po stisknutí klávesy <Enter> v některém ze 2 jednořádkových edit-boxů, se nám jeho obsah vložil jako nový řádek do víceřádkového editu, samozřejmě bez zavření dialogu. Současně si ukážeme, jak v aplikaci založené na dialogu zobrazit vlastní ikonu jako systémovou ikonu dialogu (což také bývá dotazem v diskusním fóru). Výsledek vidíte na obrázku:

win-api-21

Jak tedy na to. Podívejme se, jak vypadá handler zprávy WM_INITDIALOG:

case WM_INITDIALOG:
  SetClassLongPtr(hWnd, GCLP_HICONSM, 
    (LONG_PTR)LoadIcon(g_hInst, MAKEINTRESOURCE(IDI_ICON_MAIN)));
  oldProc = (WNDPROC)SetWindowLongPtr(GetDlgItem(hWnd, IDC_EDIT1),
    GWLP_WNDPROC, (LONG_PTR)WindowProcEdit);
  oldProc = (WNDPROC)SetWindowLongPtr(GetDlgItem(hWnd, IDC_EDIT2),
    GWLP_WNDPROC, (LONG_PTR)WindowProcEdit);
	break;

Jako první je nastavení vlastní ikony dialogovému oknu. Jak vidíte, stačí použít již známou funkci SetClassLongPtr s parametrem GCLP_HICONSM, kde jejím třetím parametrem je handle ikony. K tomu ještě 2 poznámky. Je třeba nastavit malou ikonu, nikoli velkou (GCLP_HICON). Dále je třeba si uvědomit, že po zavolání této funkce budou všechny další dialogové boxy v naší aplikaci mít tuto ikonu jako systémovou, neboť ikonu lze nastavit třídě oken (tedy třídě dialogových oken) jako celku. Na to je třeba myslet v případě více dialogů, z nichž jen některé mají mít svoji ikonu. Pak je třeba si uložit hodnotu (handle ikony HICON) vrácené funkcí SetClassLongPtr a nejlépe před voláním EndDialog ji opět "vrátit zpět".

Dále vidíme použití funkce SetWindowLongPtr pro nastavení procedury okna na vlastní funkci. Jak hned uvidíme,je důležité uložit si původní hodnotu (vrácenou voláním této funkce) do proměnné typu WNDPROC:

	WNDPROC oldProc;

Funkce WindowProcEdit může být společná pro více prvků stejného typu (v našem případě oba edit-boxy). Podívejme se, jak vypadá v našem případě:

// wParam je kód klávesy, lParam je ID prvku
#define UM_KEY_IN_CONTROL ( WM_APP + 1 )
LRESULT CALLBACK WindowProcEdit(HWND hWnd, UINT uMsg,
  WPARAM wParam, LPARAM lParam)
{
  switch ( uMsg )
  {
    case WM_GETDLGCODE:
      return DLGC_WANTALLKEYS;
    case WM_KEYDOWN:
      SendMessage(GetParent(hWnd), UM_KEY_IN_CONTROL, wParam /*kod lávesy*/,
        (LPARAM)GetMenu(hWnd));
      break;
  }
  return CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam);
}

Nejprve jeden důležitý rozdíl oproti "běžné" proceduře okna: jak návratovou hodnotu musíme použít hodnotu,kterou nám vrátí původní procedura okna (uložená v oldProc). To zajistíme použitím funkce CallWindowProc, které má jako 1. parametr právě hodnotu procedury okna, která se má volat. Dalšími parametry pak jsou předané (popřípadě i modifikované) parametry naší procedury.

Dále vidíte handler zprávy WM_GETDLGCODE. Tímto musíme říci systému, že chceme, aby do dialogového prvku byly posílané některé další zprávy, především jde o zprávy klávesnice. Hodnotou DLGC_WANTALLKEYS si jednoduše řekneme o všechny zprávy, které jsou nabízeny. Když se podíváte do dokumentace, zjistíte že máme na výběr další hodnoty, určující separátně pouze některé klávesy. Klíčem k vlastnímu zpracování klávesy <Enter> je zachycení zprávy WM_KEYDOWN, po kterém pošleme dialogu (obecně vlastníku editu) uživatelskou zprávu UM_KEY_IN_CONTROL, kde jako parametr lParam zadáme kód klávesy a do lParam vložíme identifikátor prvku pro případné rozlišení konkrétního prvku v proceduře dialogu. Tento parametr získáme pomocí funkce GetMenu, neboť když se podíváte na význam parametru hMenu funkce CreateWindowEx, zjistíte, že tento parametr určuje buď handle hlavního menu okna, nebo identifikátor dětského okna, který právě takto zjišťujeme.  Toto řešení je pro náš jednoduchý případ možná zbytečně obecné, ale uvádím ho proto, abych naznačil možné řešení i pro rozsáhlejší případy. Například, pokud jde o program s MFC, můžete si vytvořit třídu odvozenou od CEdit, kde samozřejmě předem neznáte handle okna vlastníka, a musíte použít funkci pro jeho zjištění (nejlépe GetParent()). Reakci na zprávu pak obecně provádíme v dialogu, i když zde bychom opět mohli jednodušeji (s použitím globálních proměnných) vše vyřešit bez dalšího posílání vlastní zprávy přímo v proceduře edit-boxů.

Nyní se podívejme, jak vypadá zpracování naší uživatelské zprávy v proceduře okna dialogu:

case UM_KEY_IN_CONTROL:
  switch (lParam)
  {
    case IDC_EDIT1:
    case IDC_EDIT2:
      if (wParam == VK_RETURN)
      {
        GetDlgItemText(hWnd, lParam, chText, MAX_LINE);
        lstrcat(chText, newLine);
        SendDlgItemMessage(hWnd, IDC_EDIT_MULTI, 	EM_REPLACESEL,
          FALSE, (LPARAM)chText);
      }
      break;
  }
  break;

Nejdříve otestujeme kód virtuální klávesy, uložený v parametry wParam (zprávy WM_KEYDOWN a předaný do stejného parametru naší uživatelské zprávy). V případě VK_RETURN (klávesa <Enter>) načteme již známým způsobem (GetDlgItemText) text edit-boxu určeného parametrem lParam, do kterého jsme si uložili identifikátor prvku pro rozlišení edit.boxů navzájem. Dále přidáme na konec textu znaky ukončující řádek, definované v tomto řetězci:

	TCHAR newLine[] = {0x0D, 0x0A, 0};

Pozor, zde nelze použít znak "nové řádky", tedy "\n"!

Přidání textu do edit-boxu

Dále použijeme zprávu EM_REPLACESEL, která obecně nahradí aktuálně vybraný text v edit-boxu textem zadaným v parametru lParam. Pokud není vybrán žádný text, je tento text vložen na pozici karetu, který po přidání textu zůstává nastaven na konec textu. Pro programové nastavení výběru znaků v edit boxu (samozřejmě i víceřádkovém) slouží zpráva EM_SETSEL, v jejímž parametru wParam je pozice znaku určujícího počátek výběru a v parametru lParam pak znak ukončující výběr. Pokud chceme jednoduše vybrat celý text, zadáme wParam = 0 a lParam = -1. Pokud chceme zrušit aktuální výběr, zadáme parametr wParam = -1. Nastavení pozice karetu (bez nastavení výběru textu) na libovolné místo provedeme tak, že oba parametry této zprávy nastavíme na hodnotu této pozice. Na konec textu se tady můžeme nastavit tak, že zjistíme délku textu v editu pomocí funkce GetWindowTextLength a máme tak hodnotu, na kterou nastavíme pozici karetu pro tento požadovaný případ.

Doprovodný projekt je ke stažení zde: win_api_21.zip

Výběr a načtení souboru.

V tomto pokračování si ukážeme něco málo ze základů práce se soubory. Naučíme se použít standardní dialog pro otevření souboru a dále si ukážeme ten nejjednodušší způsob načtení souboru (textu) například do víceřádkového editačního pole. Výsledek dnešního ukázkového příkladu vidíte na obrázku:

win-api-22

K vyvolání standardního systémového dialogu pro otevření (nebo uložení) souboru složí funkce GetOpenFileName:

BOOL GetOpenFileName(
  LPOPENFILENAME lpofn   // inicializační data
);

Jak je vidět, veškeré parametry funkce jsou ve struktuře OPENFILENAME:

typedef struct tagOFN { 
  DWORD         lStructSize; 
  HWND          hwndOwner; 
  HINSTANCE     hInstance; 
  LPCTSTR       lpstrFilter; 
  LPTSTR        lpstrCustomFilter; 
  DWORD         nMaxCustFilter; 
  DWORD         nFilterIndex; 
  LPTSTR        lpstrFile; 
  DWORD         nMaxFile; 
  LPTSTR        lpstrFileTitle; 
  DWORD         nMaxFileTitle; 
  LPCTSTR       lpstrInitialDir; 
  LPCTSTR       lpstrTitle; 
  DWORD         Flags; 
  WORD          nFileOffset; 
  WORD          nFileExtension; 
  LPCTSTR       lpstrDefExt; 
  LPARAM        lCustData; 
  LPOFNHOOKPROC lpfnHook; 
  LPCTSTR       lpTemplateName; 
#if (_WIN32_WINNT >= 0x0500)
  void *        pvReserved;
  DWORD         dwReserved;
  DWORD         FlagsEx;
#endif // (_WIN32_WINNT >= 0x0500)
} OPENFILENAME, *LPOPENFILENAME;

Prozatím se nebudeme zabývat podrobnějším vysvětlením všech prvků této struktury. Nejlépe si ukázat jednoduchý příklad. Důležité je pamatovat na "vynulování" struktury před plněním jednotlivých prvků, abychom ty které nás nezajímají, mohli ignorovat a měli jistotu, že jsou "nulové". Dále je nutné runě naplnit prvek lStructSize na velikost struktury (pomocí sizeof). Nyní již k příkladu.  V dnešní ukázce je aplikace založená na dialogu se 2 edit-boxy, z nichž do jednoho vypíšeme plný název vybraného souboru, do druhého (víceřádkového) pak obsah tohoto (textového) souboru. Dialog otevření souboru vyvoláme na talčítko "Vybrat soubor" (IDC_OPEN_FILE). Pro vybrání souboru si vytvoříme následující funkci:

BOOL OpenDialog(LPTSTR lpFile, HWND hwndOwner)
{
  OPENFILENAME ofn;
  TCHAR chFile[_MAX_PATH];
  lstrcpy(chFile, "");
  ZeroMemory(&ofn, sizeof(OPENFILENAME));
  ofn.lStructSize = sizeof(OPENFILENAME);
  ofn.lpstrFile = chFile;
  ofn.hwndOwner = hwndOwner;
  ofn.nMaxFile = sizeof(chFile);
  ofn.lpstrFilter = 
     TEXT("Textové soubory (*.txt)\0*.txt\0Všechny soubory (*.*)\0*.*\0");
  ofn.nFilterIndex = 1;
  ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
  if ( !GetOpenFileName(&ofn) )
    return FALSE;
  lstrcpy(lpFile, chFile);
  return TRUE;
}

Pokud uživatel stiskne tlačítko "OK", funkce zkopíruje plný název vybraného souboru do parametru lpFile.

Nyní tedy máme jméno souboru a můžeme přistoupit k jeho otevření a načtení. Pro otevření (nebo vytvoření nového) souboru se používá funkce CreateFile:

HANDLE CreateFile(
  LPCTSTR lpFileName,                         // jméno souboru
  DWORD dwDesiredAccess,                      // přístup
  DWORD dwShareMode,                          // sdílení
  LPSECURITY_ATTRIBUTES lpSecurityAttributes, // atributy zabezpečení
  DWORD dwCreationDisposition,                // způsob vytvoření
  DWORD dwFlagsAndAttributes,                 // atributy souboru
  HANDLE hTemplateFile                        // soubor "šablony"
);

Podrobněji se k této funkci a jejím možnostem vrátíme později, zde si ukážeme ten nejjednodušší způsob použití pro otevření existujícího souboru. Funkce vrátí handle otevřeného souboru nebo v případě neúspěchu hodnotu INVALID_HANDLE_VALUE. Podívejme se nyní na celou vlastní funkci, která načte zadaný soubor a jeho obsah nastaví jako text okna zadaného jako parametr:

BOOL LoadFile(LPCTSTR lpFileName, HWND hWndTo)
{
  HANDLE hFile;
  DWORD dwSize;
  DWORD dw;
  LPBYTE lpBuffer = NULL;
  hFile =  CreateFile(lpFileName, GENERIC_READ, 0,
    NULL, OPEN_EXISTING, 0, NULL);
  if ( hFile == INVALID_HANDLE_VALUE )
    return FALSE;
  dwSize = GetFileSize(hFile, NULL);
  lpBuffer = (LPBYTE)HeapAlloc(GetProcessHeap(),
    HEAP_GENERATE_EXCEPTIONS, dwSize+1);
  if ( !ReadFile(hFile, lpBuffer, dwSize, &dw, NULL) )
  {
    CloseHandle(hFile);
    HeapFree(GetProcessHeap(), 0, lpBuffer);
    return FALSE;
  }
  CloseHandle(hFile);
  lpBuffer[dwSize] = 0;
  SetWindowText(hWndTo, (LPCTSTR)lpBuffer);
  if ( !HeapFree(GetProcessHeap(), 0, lpBuffer) )
    return FALSE;
  return TRUE;
}

Jak je zřejmé, celý obsah souboru načítáme v jednom čtení. Musíme tedy nejprve zjistit velikost souboru pomocí funkce GetFileSize. Buffer si naalokujeme na tuto velikost plus jeden byte navíc pro uložení nulového znaku ukončujícího řetězec, neboť k tomuto bufferu budeme přistupovat jako k céčkovskému řetězci, použijeme ho jako parametr funkce SetWindowText. Pro alokaci bufferu by samozřejmě bylo možné použít standardní céčkovské funkce malloc/free. Na tomto příkladě jsem ale také ukázal jednoduchý způsob alokace paměti pomocí Win32 funkcí, konkrétně HeapAlloc/HeapFree. Vlastní načtení souboru pak provádíme funkcí ReadFile:

BOOL ReadFile(
  HANDLE hFile,                // handle otevřeného souboru
  LPVOID lpBuffer,             // datový buffer
  DWORD nNumberOfBytesToRead,  // počet bytů k načtení
  LPDWORD lpNumberOfBytesRead, // počet načtených bytů
  LPOVERLAPPED lpOverlapped    // "overlapped" buffer
);

Tato funkce nám vrátí logickou hodnotu indikující úspěch načtení a současně naplní parametr lpNumberOfBytesRead počtem skutečně načtených bytů, který může být menší než počet bytů požadovaných, pokud například narazíme na konec souboru.

Doprovodný projekt je ke stažení zde: win_api_22.zip

Práce s registry

V tomto pokračování se naučíme základům používání registrační databáze pro ukládání konfigurace programu nebo nějakých dalších informací. V doprovodném příkladu budeme mít za úkol zapsat do registrů polohu a velikost okna při ukončení aplikace a při dalším spuštění okno zobrazit v této pozici. Dále si uložíme (ze cvičných důvodů) jako řetězec datum a čas ukončení programu a toto pak zobrazíme při dalším spuštění jako text v okně.

win-api-23

Podobně jako při práci se soubory musíme nejprve požadovaný klíč v registrační databázi otevřít (nebo vytvořit), tím získáme jeho handle. Tento handle pak použijeme v dalších funkcích pro práci s údaji v registrech. Po provedení požadovaných operací handle klíče opět zavřeme.

Otevření klíče

Pro otevření nebo vytvoření klíče nám slouží funkce RegCreateKeyEx:

LONG RegCreateKeyEx(
  HKEY hKey,                                  // handle klíče
  LPCTSTR lpSubKey,                           // jméno podklíče
  DWORD Reserved,                             // rezervováno
  LPTSTR lpClass,                             // třída
  DWORD dwOptions,                            // volby
  REGSAM samDesired,                          // bezpečnost
  LPSECURITY_ATTRIBUTES lpSecurityAttributes, // děditelnost
  PHKEY phkResult,                            // handle klíče 
  LPDWORD lpdwDisposition                     // způsob "otevření"
);

Řekněme si o významu těch důležitých parametrů:

hKey - handle již otevřeného klíče nebo jedna z předdefinovaných hodnot většinou určující větev v registrech, jak ostatně napovídá přehled možných hodnot:

HKEY_CLASSES_ROOT
HKEY_CURRENT_CONFIG
HKEY_CURRENT_USER
HKEY_LOCAL_MACHINE
HKEY_USERS
Windows NT/2000/XP: HKEY_PERFORMANCE_DATA 
Windows 95/98/Me: HKEY_DYN_DATA

lpSubkey - jméno podklíče, který chceme otevřít.

samDesired - požadovaný přístup ke klíči. Pokud chceme plný přístup pro zápis a čtení, můžeme jednoduše uvést hodnotu KEY_ALL_ACCESS, nebo pro zvýšení "bezpečnosti" některou z dalších hodnot (jejich seznam s popisem naleznete v dokumentaci Windows SDK), které nějakým způsobem omezují jisté operace.

phkResult - v tomto parametru nám funkce vrátí handle požadovaného klíče.

lpdwDisposition - v tomto parametru nám funkce vrátí hodnotu určující, zda byl otevřen existující klíč nebo vytvořen klíč nový. Může nabývat hodnot REG_CREATED_NEW_KEY nebo REG_OPENED_EXISTING_KEY, jejichž význam je jistě vypovídající.

Pokud funkce skončí úspěšně, návratová hodnota je ERROR_SUCCESS, jejíž název zní možná trochu podivně (česky "chyba_úspěch"), nicméně opravdu značí úspěšné otevření požadovaného klíče v registrech.

Zápis do registru

Když máme klíč úspěšně otevřen nebo vytvořen, můžeme zapisovat hodnoty funkcí RegSetValueEx:

LONG RegSetValueEx(
  HKEY hKey,           // handle klíče
  LPCTSTR lpValueName, // název hodnoty
  DWORD Reserved,      // rezervováno
  DWORD dwType,        // typ hodnoty
  CONST BYTE *lpData,  // data
  DWORD cbData         // velikost dat
);

Podrobnější popis a význam jednotlivých parametrů naleznete v dokumentaci Windows SDK a nebudu ho zde popisovat, raději si vše ukážeme na příkladě. Vytvořme si funkci, která nám otevře zadaný klíč. Název klíče si v programu vytvoříme pomocí direktivy define:

#define REGISTRY_KEY_NAME "Software\\Radek Chalupa\\Ucime se WinAPI"
HKEY registry_OpenKey(LPCTSTR lpKeyName, LPBOOL lpbCreated)
{
  DWORD dwDisp = 0;
  HKEY hkResult = NULL;
  if ( RegCreateKeyEx(HKEY_CURRENT_USER, lpKeyName, 0, NULL, REG_OPTION_NON_VOLATILE,
    KEY_ALL_ACCESS, NULL, &hkResult, &dwDisp) != ERROR_SUCCESS )
    return NULL;
  if ( lpbCreated )
    *lpbCreated = ( dwDisp & REG_CREATED_NEW_KEY );
  return hkResult;
}

Dále si vytvoříme funkce pro zápis číselné hodnoty (typ DWORD) a textových řetězců:

BOOL registry_WriteDWORD(HKEY hKey, LPCTSTR lpValueName, DWORD dwValue)
{
  return  ( RegSetValueEx(hKey, lpValueName, 0, REG_DWORD,
    (CONST BYTE*)&dwValue, sizeof(DWORD)) == ERROR_SUCCESS );
}

BOOL registry_WriteString(HKEY hKey, LPCTSTR lpValueName, LPCTSTR lpText)
{
  return ( RegSetValueEx(hKey, lpValueName, 0, REG_SZ, (CONST BYTE*)lpText,
    (lstrlen(lpText)+1)*sizeof(TCHAR)) ==  ERROR_SUCCESS );
}

V obou případech zadáme jako parametr hKey handle předtím otevřeného klíče.

Na zavření klíče v registrech si také napíšeme jednoduchou funkci volající API funkci RegCloseKey:

BOOL registry_CloseKey(HKEY hKey)
{
  return ( RegCloseKey(hKey) == ERROR_SUCCESS);
}

Nyní se podívejme, jak s využitím těchto funkcí zapíšeme v handleru zprávy WM_CLOSE požadované výše zmíněné informace do registru:

BOOL WriteRegistry()
{
  BOOL bRes = TRUE;
  HKEY hKey =  registry_OpenKey(REGISTRY_KEY_NAME, NULL);
  if ( !hKey )
    return FALSE;
  if ( !registry_WriteDWORD(hKey, TEXT("WindowPos"), dwWindowPos) )
    bRes = FALSE;
  if ( !registry_WriteDWORD(hKey, TEXT("WindowSize"), dwWindowSize) )
    bRes = FALSE;
  if ( !registry_WriteString(hKey, TEXT("Info"), chInfoText) )
    bRes = FALSE;
  if ( !registry_CloseKey(hKey) )
    bRes = FALSE;
  return bRes;
}

// Handler zprávy WM_CLOSE
void OnClose()
{
  SYSTEMTIME st;
  GetLocalTime(&st);
  sprintf(chInfoText, "Předchozí ukončení: %d.%d.%d  %.2d:%.2d",
    st.wDay, st.wMonth, st.wYear, st.wHour, st.wMinute);
  RECT rect;
  GetWindowRect(g_hWnd, &rect);
  dwWindowPos = MAKELONG(rect.left, rect.top);
  dwWindowSize = MAKELONG(rect.right-rect.left, rect.bottom-rect.top);
  WriteRegistry();
}

Nyní se ještě musíme naučit informace z registru také přečíst. K tomu nám slouží funkce RegQueryValueEx, kterou zde uvedu také bez podrobného popisu parametrů (je v dokumentaci), ale ukážeme si samozřejmě příklad.

LONG RegQueryValueEx(
  HKEY hKey,            // handle klíče
  LPCTSTR lpValueName,  // název hodnoty
  LPDWORD lpReserved,   // rezervováno
  LPDWORD lpType,       // typ bufferu
  LPBYTE lpData,        // datový buffer
  LPDWORD lpcbData      // velikost datového bufferu
);

Také pro načítání si připravíme 2 vlastní funkce pro čtení číselné a textové hodnoty:

BOOL registry_ReadDWORD(HKEY hKey, LPCTSTR lpValueName, LPDWORD lpdwResult)
{
  if ( !registry_KeyExists(hKey, lpValueName) )
    return FALSE;
  DWORD dwValue, dwSize, dwType;
  dwSize = sizeof(DWORD);
  dwValue = 0;
  if ( RegQueryValueEx(hKey, lpValueName, NULL, &dwType, (LPBYTE)&dwValue, &dwSize)
    != ERROR_SUCCESS )
    return FALSE;
  *lpdwResult = dwValue;
  return TRUE;
}

BOOL registry_ReadString(HKEY hKey, LPCTSTR lpValueName, LPTSTR lpString)
{
  if ( !registry_KeyExists(hKey, lpValueName) )
    return FALSE;
  DWORD dwSize, dwType;
  dwSize = sizeof(chInfoText);
  if ( RegQueryValueEx(hKey, lpValueName, NULL, &dwType, (LPBYTE)lpString, &dwSize)
    != ERROR_SUCCESS )
    return FALSE;
  return TRUE;
}

Jako příklad si ukažme, jak v doprovodném programu načteme informace zapsané při předchozím ukončení a jak zjistíme, zda záznam v registrech neexistuje, tedy že se pravděpodobně jedná o první spuštění programu v tomto uživatelském účtu.

BOOL ReadRegistry()
{
  BOOL bRes = TRUE;
  BOOL bFirst = FALSE;
  HKEY hKey = registry_OpenKey(REGISTRY_KEY_NAME, &bFirst);
  if ( !hKey )
    return FALSE;
  if ( bFirst )
  {
    dwWindowPos =  0x00600080;
    dwWindowSize = 0x01500200;
    lstrcpy(chInfoText, TEXT("První spuštění programu"));
    return TRUE;
  }
  if ( !registry_ReadDWORD(hKey, "WindowPos", &dwWindowPos) )
    bRes = FALSE;
  if ( !registry_ReadDWORD(hKey, "WindowSize", &dwWindowSize) )
    bRes = FALSE;
  if ( !registry_ReadString(hKey, "Info", chInfoText) )
    bRes = FALSE;
  if ( !registry_CloseKey(hKey) )
    bRes = FALSE;
  return bRes;
}

Tuto funkci zavoláme před vytvořením okna, pokud není nalezen záznam v registrech, nastavíme poloho a rozměry okna na výchozí hodnotu, stejně jako informativní text, jehož výpis v handleru WM_PAINT, je pro čtenáře jistě již rutinní záležitostí.

Jako "domácí úkol" si mohou zájemci program doplnit tak, aby byl odstraněn následující "bug": Pokud totiž okno zavřeme dostatečně vpravo či dole, poté zmenšíme rozlišení obrazovky, při dalším spuštění se nám může stát že zaznamenáme "aut", tedy okno bude celé "zobrazené" mimo viditelnou oblast obrazovky.

Doprovodný projekt je ke stažení zde: win_api_23.zip

Ikona aplikace v oznamovací oblasti.

Ukážeme si jak aplikaci tzv. "minimalizovat do traye", což z programátorského pohledu není přesný termín, také proto ty uvozovky. Žádný režim zobrazení okna "v trayi" samozřejmě neexistuje, jedná se o pouhou simulaci, která se z pohledu uživatele jeví jako minimalizace do tray-ikony. Uveďme si nejprve přehled kroků, které musíme učinit pro dosažení kýženého efektu:
  • Při zachycení požadavku na minimalizaci okna okno skrýt příkazem ShowWindow(handle, SW_HIDE)
  • Zaregistrovat a zobrazit ikonu v oznamovací oblasti.
  • Zachytávat "myší zprávy" od této ikony.
  • Po zachycení vhodné zprávy (např. dvojklik na ikoně) okno opět zobrazit příkazem ShowWindow(handle, SW_RESTORE), volitelně můžeme okno navíc přenést do popředí příkazem SetForegroundWindow(handle).
  • Nejpozději před zrušením okna ikonu z oznamovací oblasti opět odebrat.
Je na nás, zda ikonu v oznamovací oblasti necháme po celou dobu života okna, nebo zda ji tam zobrazíme pouze při skrytí okna a při jeho zobrazení ji odstraníme. Ukažme si tedy, jak zaregistrovat a zobrazit ikonu v oznamovací oblasti. Slouží k tomu funkce Shell_NotifyIcon:
BOOL Shell_NotifyIcon(
    DWORD dwMessage, 
    PNOTIFYICONDATA lpdata
);
Parametr dwMessage určuje akci, kterou chceme provést. V našem případě si ukážeme ty 2 základní, tedy přidání a odebrání ikony. Pro přidání ikony použijeme hodnotu NIM_ADD a pro odebrání NIM_DELETE. Nastavení vlastností a parametrů ikony provedeme naplněním struktury NOTIFYICONDATA, jejíž adresu pak použijeme jako 2. parametr funkce Shell_NotifyIcon. Podrobný popis prvků struktury naleznete v dokumentaci, zde jen upozorním, že pokud je definován požadavek na InternetExplorer verze 5 a vyšší (tj. hodnota _WIN32_IE větší nebo rovna 0x0500), má tato strukrura navíc několik prvků, které umožňují zobrazit takzvaný balónový tooltip, jaký vidíte na obrázku:

win-api-24

Podívejme se nyní jak vypadá funkce, realizující přidání ikony do oznamovací oblasti:
// Přidání ikony do oznamovací oblasti

BOOL AddNotifyIcon()
{
  NOTIFYICONDATA nid;
  ZeroMemory(&nid, sizeof(nid));
  nid.cbSize = sizeof(NOTIFYICONDATA);
  nid.hWnd = g_hWnd;
  nid.hIcon = (HICON)GetClassLongPtr(g_hWnd, GCLP_HICONSM);
  if ( !nid.hIcon )
    return FALSE;
  lstrcpyn(nid.szTip, chAppName, sizeof(nid.szTip));
  nid.uID = 1;
  nid.uCallbackMessage = WM_NOTIFY_ICON;
  
#if _WIN32_IE >= 0x0500 
  nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_INFO;
  nid.dwInfoFlags = NIIF_INFO;
  nid.dwState = 0;
  nid.uVersion = NOTIFYICON_VERSION; 
  nid.uTimeout = 10000;
  lstrcpy(nid.szInfo, TEXT("Tady může být\nnějaký text do 255 znaků\n:-))"));
  lstrcpy(nid.szInfoTitle, chAppName);
#else 
  nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
#endif 
  if ( !Shell_NotifyIcon(NIM_ADD, &nid) )
  {
    MessageBox(g_hWnd, TEXT("Nelze přidat ikonu"),
    	NULL, MB_ICONERROR | MB_TASKMODAL);
    return FALSE;
  }
  return TRUE;
}
Zde poznámku a současně výzvu k diskusi. Podle dokumentace může být parametr uTimeout, určující dobu, po kterou bude balónový dialog zobrazen, v rozsahu 10 - 30 sekund, tedy mít hodnotu 10000 - 30000 (v milisekundách). Avšak podle mých zkušeností ve Windows XP zůstane balónový dialog zobrazen do té doby, dokud uživatel neprovede nějakou akci, která způsobí jeho deaktivaci, bez ohledu na nastavený time-out. Naopak ve Windows 2000 nelze (alespoň mě se to nepodařilo) nechat "balón" zobrazen déle než uvedených 30 sekund (v souladu s dokumentací). Pokud tedy někdo víte nebo zjistíte na toto téma více, jistě všichni uvítáme tyto poznatky, zatím to považujme za nedokumentovanou vlastnost Windows XP. Při najetí myší na ikonu v oznamovací oblasti se zobrazí standardní tool-tip (ten známý většinou žlutý plovoucí text), který zadáme jako prvek szTip struktury NOTIFYICONDATA. Tento text je vždy jednořádkový a jeho maximální délka závisí opět na verzi InternetExoloreru, konkrétně ve verzi nižší než 5 může mít 64 znaků, ve verzi 5 a výš až 128 znaků. Podívejme se dále na prvek uCallbackMessage. Jde u uživatelsky definovanou zprávu, kterou ikona posílá svému vlastníku (oknu určeného prvkem hWnd), pokud dojde k nějaké události myši na této ikoně. Parametr lParam této zprávy pak je identifikátor odpovídající myší zprávy (např. WM_LBUTTONDOWN apod.). Je však třeba počítat s tím, že ne všechny myší zprávy jsou zachytávány, například zprávy WM_MOUSEWHEEL (kolečko myši) nebo WM_MOUSELEAVE detekovány nejsou. Tuto uživatelskou zprávu si nadefinujeme tak, aby měla hodnotu vyšší než WM_USER, nebo lépe WM_APP, tedy např. takto:
#define WM_NOTIFY_ICON  ( WM_APP + 1 )
Nyní si uveďme kód, který naopak odebere ikonu z oznamovací oblasti. Zde stačí uvést pouze identifikátor ikony (uID) a handle okna, kterému ikona patří (hWnd):
// Odebrání ikony z oznamovací oblasti

BOOL DeleteNotifyIcon()
{
  NOTIFYICONDATA nid;
  ZeroMemory(&nid, sizeof(nid));
  nid.cbSize = sizeof(NOTIFYICONDATA);
  nid.hWnd = g_hWnd;
  nid.uID = 1;
  if ( !Shell_NotifyIcon(NIM_DELETE, &nid) )
  {
    MessageBox(g_hWnd, TEXT("Nelze odebrat ikonu"), NULL, MB_ICONERROR | MB_TASKMODAL);
    return FALSE;
  }
  return TRUE;
}
Jen připomenu, že v obou případech nesmíme zapomenou naplnit prvek cbSize této struktury. V našem ukázkovém programu přidáme ikonu hned po vytvoření okna a odebereme ji těsně před jeho zrušením. Opět upozornění - odebrání musíme udělat opravdu před zrušením okna, nikoli až třeba někde před koncem programu, abychom v tomto okamžiku měli ještě platný handle okna, který použijeme jako prvek hWnd struktury NOTIFYICONDATA. Nyní se podívejme na proceduru okna, kde provedeme další nutné kroky k realizaci toho nejjednoduššího "skrývání okna do oznamovací oblasti":
// Procedura okna 

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch ( uMsg ) 
  {
    case WM_CLOSE:
      DeleteNotifyIcon();
      break;
    case WM_DESTROY:
      PostQuitMessage(0);
      break;
    case WM_NOTIFY_ICON:
      switch (lParam)
      {
        case WM_LBUTTONDBLCLK:
          ShowWindow(hWnd, SW_RESTORE);
          SetForegroundWindow(hWnd);
          break;
      }
      break;
    case WM_SYSCOMMAND:
      switch ( wParam )
      {
        case SC_MINIMIZE:
          ShowWindow(hWnd, SW_HIDE);
          return 0;
      }
      break;
  }
  return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
Je vidět, jak zachytíme požadavek na minimalizaci okna v handleru zprávy WM_SYSCOMMAND, kde při parametru wParam rovném hodnotě SC_MINIMIZE tuto zprávu zadržíme, tedy vrátíme 0 místo zpracování výchozí procedurou, a místo toho skryjeme okno funkcí ShowWindow s parametrem SW_HIDE. Naopak při zachycení dvojkliku na oznamovací ikonu okno opět zobrazíme v původní pozici před skrytím a ještě ho navíc přesuneme do popředí. Doprovodný projekt je ke stažení zde: win_api_24.zip

Zvuky a animace.

V tomto pokračování se naučíme, jak můžeme oživit program zvuky (typu wave) a animacemi (avi). V obou případech budeme mít data zvuku a animace uložena v prostředcích (resources) programu.

win-api-25

Vytvořme si tedy aplikaci založenou na dialogu. Přidejme si zdroj dialogu a dále si importujme dva zdroje ze souboru. Prvním bude soubor .wav, obsahující vlnový zvuk wave. Tento zvuk by neměl být nijak komprimovaný. Pokud se resource editor při importu zeptá na typ zdroje, zadáme "WAVE". Druhým zdrojem bude soubor .avi, obsahující animaci, která nesmí obsahovat zvuk. Já jsem použil jednu ze systémových animací, vytaženou a uloženou do souboru ze systémové knihovny Windows (shell32.dll). U tohoto zdroje jako typ zadáme "AVI". Na dialog si umístíme prvek "Animation Control". V jeho vlastnostech si můžeme přímo v resource editoru nastavit vlastnost "Animation transparent" (odpovídá stylu okna ACS_TRANSPARENT) na TRUE - pozadí animace pak zůstane nedotčeno a bude tedy tvořeno pozadím dialogu. Pokud nechceme mít kolem animace vnořený okraj, nastavíme také vlastnost "Border" na FALSE.

Použití prvku Animate Control

Nyní trocha teorie. Prvek Animation Control patří mezi běžné prvky Windows (Windows Common Controls). Pokud bychom ho vytvářeli programově, museli bychom nejdříve použít makro Animate_Create, které "uvnitř" volá funkci CreateWindow, čímž vytvoří okno příslušné třídy.

HWND Animate_Create(
    HWND hwndP, 
    UINT id, 
    DWORD dwStyle, 
    HINSTANCE hInstance
);
  • hwndP - okno vlastníka prvku Animate Control
  • id - identifikátor prvku animate
  • dwStyle - styly prvku Aninmate (popis neleznete v dokumentaci)
  • hInstance - handle instance

Pokud (jako v našem případě) máme tento prvek na dialogu, máme při inicializaci dialogu již prvek vytvořen, použijeme pro "otevření" animačního prvku makro Animate_Open:

BOOL Animate_Open(
  HWND hwndAnim, // handle okna prvku Animate Control
  LPTSTR lpszName // jméno zdroje 
);

V našem příkladě si prvek IDC_ANIMATE otevřeme při inicializaci, dialogu, tedy v handleru zprávy WM_INITDIALOG:

void OnInitDialog(HWND hwndDlg)
{
  Animate_Open(GetDlgItem(hwndDlg, IDC_ANIMATE), MAKEINTRESOURCE(IDR_SEARCH));
}

Identifikátor IDR_SEARCH představuje importovaný zdroj typu AVI, konkrétně jde o systémovou animaci nazývanou "baterka". Pokud bychom chtěli, aby se animace rozběhla již při vytvoření, mohli bychom nastavit vlastnost "Auto Play" (odpovídající stylu okna ACS_AUTOPLAY) na TRUE. My však budeme animaci spouštět a zastavovat na stisknutí dvou tlačítek na dialogu. Ke spuštění použijeme makro Animate_Play:

BOOL Animate_Play(
    HWND hwndAnim, 
    UINT wFrom, 
    UINT wTo, 
    UINT cRepeat
);
  • hwndAnim - handle prvku Animate Control
  • wFrom - index (číslovaný od nuly) obrázku v animaci, od kterého se má spustit přehrávání
  • wTo - index obrázku, u kterého se má přehrání animace ukončit. Pokud uvedeme hodnotu -1, animace se přehraje k poslednímu obrázku.
  • cRepeat - počet opakování, hodnota -1 znamená neustálé opakování až do ukončení jiným příkazem.

Pro zastavení animace pak slouží makro Animate_Stop:

BOOL Animate_Stop(
    HWND hwndAnim // handle prvku animate control
);

Pokud chceme prvek Animate Control zcela "zavřít", použijeme makro AnimateClose:

BOOL Animate_Close(
    HWND hwnd
);

Toto makro samozřejmě nejprve zastaví animaci, pokud je v tom okamžiku spuštěna, a je zobrazen první obrázek AVI klipu. Pokud bychom chtěli AVI klip zcela vizuálně odstranit, mohli bychom použít již známou funkci ShowWindow s parametrem SW_HIDE, nebo zrušit okno prvku funkcí DestroyWindow. K použití prvku Animate Control ještě doplním, že je nutné před jeho vytvořením, v našem případě tedy před vyvoláním dialogu funkcí DialogBox, inicializovat tzv. běžné prvky Windows (z tohoto seriálu již známou) funkcí InitCommonControls, nebo InitCommonControlsEx.

Přehrávání vlnového zvuku WAVE

Pokud chceme přehrát jednoduchý (nekomprimovaný) vlnový zvuk WAVE, použijeme funkci PlaySound:

BOOL PlaySound(
  LPCSTR pszSound, 
  HMODULE hmod,     
  DWORD fdwSound    
);
  • pszSound - název zvuku. Může to být název zdroje (resource) nebo název externího souboru wav.
  • hmod - handle modulu obsahujícího zdroj. Pokud nejde o zvuk ve zdrojích, musí mít hodnotu NULL.
  • fdwSound - volby přehrávání. Jejich plný výčet s popisem naleznete v dokumentaci. Zmíním se jen o některých:
    • SND_FILENAME - zvuk je přehráván ze souboru
    • SND_RESOURCE - zvuk je ve zdrojích programu.
    • SND_SYNC - funkce je ukončena až po přehrání zvuku do konce
    • SND_ASYNC - funkce je ukončena a program pokračuje hned po spuštění přehrávání a toto přehrávání pokračuje asynchronně za běhu programu, pokud není předčasně ukončeno dalším voláním této funkce nebo samozřejmě například ukončením celého programu.

Vraťme se k našemu ukázkovému příkladu. Na dialogu máme 2 tlačítka, na které budeme spouštět a zastavovat animaci, a při spuštění animace přehrajeme asynchronně zvuk ze zdrojů (s identifikátorem IDR_OK). Handlery stisknutí tlačítek budou tedy vypadat následovně:

void OnPlayAVI(HWND hwndDlg)
{
  PlaySound(MAKEINTRESOURCE(IDR_OK), g_hInst, SND_RESOURCE | SND_ASYNC);
  Animate_Play(GetDlgItem(hwndDlg, IDC_ANIMATE),0,-1,-1);
}

void OnStopAVI(HWND hwndDlg)
{
  Animate_Stop(GetDlgItem(hwndDlg, IDC_ANIMATE));
}

Doprovodný projekt je ke stažení zde: win_api_25.zip

Přehrávání multimédií pomocí MCI.

V tomto pokračování nahlédneme do světa multimédií ve Windows. Naučíme se používat ten nejjednodušší způsob přehrávání multimediálních souborů pomocí tzv. MCI - Media Control Interface. I když v současnosti je již toto rozhraní částečně nahrazováno technologií DirectX, stále má své místo a lze jej s úspěchem používat. Umožňuje nám jednoduchým způsobem přehrávat nejrůznější typy multimediálních souborů, jako zvuky (wav, midi, wma, mp3), videoklipy (avi, wmv, mpeg apod). V podstatě jedinou podmínkou úspěšného přehrání je nainstalovaný kodek, nutný pro soubor daného typu a komprese.

win-api-26

Pro použití MCI musíme přidat hlavičkový soubor Video pro Windows - Vfw.h a odpovídající knihovnu - vfw32.lib. Ukážeme si nyní velice jednoduchý přehrávač, který bude umožňovat vybrat a spustit přehrávání souboru, toto přehrávání kdykoli ukončit a dále během přehrávání jej ručně přerušit a poté ve stejném místě klipu pokračovat v přehrávání. V této ukázce bude samozřejmě použita jen malá část funkcí MCI, takže zájemci o podrobnější proniknutí do této problematiky mohou pokračovat ve studiu dokumentace (nejlépe MSDN).

Pro vytvoření MCI okna přehrávače záznamů použijeme funkci MCIWndCreate, která zaregistruje příslušnou třídu, vytvoří okno a pokud zadáme také jméno souboru, otevře jej a připraví pro přehrávání.

HWND MCIWndCreate(
  HWND hwndParent,      
  HINSTANCE hInstance,  
  DWORD dwStyle,        
  LPSTR szFile          
);

Pokud funkce proběhne úspěšně, vrátí nám handle okna - přehrávače záznamů. Tento handle pak budeme používat jako parametr dalších funkcí, ovládajících přehrávání multimediálního klipu. Když se podíváte do dokumentace, zjistíte že jako parametr dwStyle můžeme použít některý (nebo kombinaci více hodnot) styl specifický pro MCI okno. Můžeme například určit, zda okno má mít vlastní panel s ovládacími prvky (playbar), zda má zobrazovat aktuální pozici v klipu apod. V našem příkladě jsem použil styl MCIWNDF_NOPLAYBAR (bez ovládacího panelu), v kombinaci s klasickými styly okna WS_POPUP a WS_BORDER. Znamená to, že pokud se bude jednat o videoklip, bude tento přehráván v samostatném plovoucím okně (bez toho, aby bylo umístěno na jiném okně jako dětské okno) a navíc bude mít jednoduchý okraj. Přehrávání pak bude možno ovládat pouze z okna aplikace. Samozřejmě můžete experimentovat s různými styly okna. Například není problém přehrávat klip v dětském okně hlavního okna aplikace apod.

Při spuštění dialogu ještě zakážeme tlačítka ovládající klip. Tyto pak budeme povolovat pouze po spuštění přehrávání, kdy naopak zakážeme tlačítko spuštění klipu do té doby, než bude aktuální přehrávání ukončeno. Podívejme se tady jak budeme reagovat na stlačení tlačítka "Přehrát soubor":

BOOL SelectFileDialog(LPTSTR lpFile, HWND hwndOwner)
{
  OPENFILENAME ofn;
  TCHAR szFile[MAX_PATH];
  lstrcpy(szFile, "");
  ZeroMemory(&ofn, sizeof(OPENFILENAME));
  ofn.lStructSize = sizeof(OPENFILENAME);
  ofn.lpstrFile = szFile;
  ofn.hwndOwner = hwndOwner;
  ofn.nMaxFile = sizeof(szFile);
  ofn.lpstrFilter = TEXT("Všechny soubory (*.*)\0*.*\0");
  ofn.nFilterIndex = 1;
  ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
  if ( !GetOpenFileName(&ofn) )
    return FALSE;
  lstrcpy(lpFile, szFile);
  return TRUE;
}

void OnPlayFile(HWND hwndDlg)
{
  if ( IsWindow(g_hwndMCI) )
  {
    MCIWndStop(g_hwndMCI);
    MCIWndDestroy(g_hwndMCI);
  }
  TCHAR szFile[MAX_PATH];
  if ( !SelectFileDialog(szFile, hwndDlg) )
    return;
  g_hwndMCI = MCIWndCreate(NULL, g_hInst, MCIWNDF_NOPLAYBAR | WS_POPUP | WS_BORDER, szFile);
  MCIWndPlay(g_hwndMCI);
  EnableWindow(GetDlgItem(hwndDlg, IDC_STOP), TRUE);
  EnableWindow(GetDlgItem(hwndDlg, IDC_PAUSE), TRUE);
  EnableWindow(GetDlgItem(hwndDlg, IDC_CONTINUE), TRUE);
  EnableWindow(GetDlgItem(hwndDlg, IDC_PLAY), FALSE);
}

Jak jsem se již zmínil, handle MCI okna můžeme nyní použít jako parametr v široké škále MCI funkcí. Zde si ukážeme ty nejjednodušší. Zastavení přehrávání klipu pomocí makra MCIWndStop a zrušení MCI okna bude vypadat následovně:

void OnStop(HWND hwndDlg)
{
  MCIWndStop(g_hwndMCI);
  MCIWndDestroy(g_hwndMCI);
  EnableWindow(GetDlgItem(hwndDlg, IDC_STOP), FALSE);
  EnableWindow(GetDlgItem(hwndDlg, IDC_PAUSE), FALSE);
  EnableWindow(GetDlgItem(hwndDlg, IDC_CONTINUE), FALSE);
  EnableWindow(GetDlgItem(hwndDlg, IDC_PLAY), TRUE);
}

Podobně pro přerušení přehráváni a pokračování od místa přerušení realizujeme pomocí maker MCIWndPause a MCIWndPlay.

void OnPause(HWND hwndDlg)
{
  if ( IsWindow(g_hwndMCI) )
    MCIWndPause(g_hwndMCI);
}

void OnContinue(HWND hwndDlg)
{
  if ( IsWindow(g_hwndMCI) )
    MCIWndPlay(g_hwndMCI);
}

K dalšímu studiu doporučuji podívat se na přehled maker, funkcí a zpráv začínajících na "MCI". Zjistíte například, jak nastavovat intenzitu zvuku - MCIWndSetVolume, jak upravovat rychlost přehrávání - MCIWndSetSpeed, jak zvětšit či zmenšit obraz videoklipu - MCIWndSetZoom a také jak zachytávat zprávy informující o některých událostech při přehrávání.

Doprovodný projekt je ke stažení zde: win_api_26.zip

Začínáme oživovat vzhled aplikace.

win-api-27

V tomto článku se podíváme na problematiku oživení vzhledu aplikace. Z předchozích pokračování tohoto seriálu již víme, že pomocí skriptu zdrojů, a tím tedy samozřejmě také pomocí vizuálního editoru zdrojů (který máme k dispozici například ve Visual C++), nemůžeme u jednotlivých prvků (edit-boxy, list-boxy, tlačítka...) příliš měnit jejich vzhled. Přesně řečeno, ve skriptu zdrojů lze nastavit pouze to, co lze u toho kterého prvku nastavit pomocí stylu okna. Znamená to tedy, že například můžeme nastavit zarovnání textu vpravo u edit-boxu, nebo třeba u list-boxu nebo edit-boxu odstranit vnořené 3D okraje (tj. odebereme vlastnost "client-edge" apod. Ale již nemůžeme například nastavit písmo (font) každému jednotlivému prvku ani barvu textu či pozadí textu.

Existují opravdu vizuální nástroje, které samozřejmě umožňují "programátorovi" naklikat nejrůznější vlastnosti již při návrhu formuláře (tak se většinou nazývá okno aplikace). Toto však je za cenu poměrně výrazného snížení výkonu aplikace a výrazného zvýšení nároků na paměť a další systémové zdroje. Stačí se podívat pomocí Správce úloh (ve Windows NT/2000/XP) na hodnoty jako objekty GDI, popisovače apod. u aplikace vytvořené třeba s pomocí C++ Builderu nebo WinForms. Samozřejmě že tyto nástroje výrazně zrychlují vývoj aplikace, což je v mnoha případech dnes prioritou. Z tohoto hlediska osobně doporučuji používat knihovnu MFC, která je podle mého názoru optimálním "kompromisem" mezi "pohodlím a rychlostí vývoje" a výkonem výsledné aplikace. Zůstávat u čistého API v případě rozsáhlejších programů typu účetnictví apod. je dnes opravdu již nesmysl. Může to být samozřejmě zajímavé "intelektuální cvičení", ale programátor se musí také nějak živit a přizpůsobit volbu vývojových nástrojů "komerčním hlediskům".

Proč tento poměrně rozsáhlý teoretický úvod. Je totiž potřeba vědět, že vše, co si v tomto článku a v dalším pokračování ukážeme, znamená zvýšený nárok na systémové zdroje, který samozřejmě nebude zdaleka tak výrazný, jako když použijeme například celý balík knihovny VCL, ale "bez následků" to zákonitě zůstat nemůže.

Jaké máme tedy možnosti "customizace" běžných prvků Windows. Pokud bychom chtěli mít naprostou kontrolu nad jejich vzhledem, pak bychom u části z těchto prvků mohli použít tzv. "uživatelské kreslení". O něm si však řekneme až příště, nyní se podíváme na jednodušší způsob, kterým je použití zpráv typu WM_CTLCOLORxxx, kde xxx je typ jednoho z prvků, které toto podporují. Tímto "prvkem" může být i samo dialogové okno.

Ukázkový projekt je aplikace založená na hlavním dialogovém okně. Podívejme se, jak nastavit pozadí celého dialogu tak, jak to vidíte na obrázku (aniž bychom cokoli vykreslovali v handleru zprávy WM_PAINT). Zpráva WM_CTLCOLORDLG je posílána do procedury okna dialogu před tím, než jej systém vykreslí. V reakci na tuto zprávu můžeme specifikovat barvu nebo štětec, který bude použit pro vykreslení textu a pozadí dialogu. V parametru wParam této zprávy máme k dispozici kontext zařízení (HDC) dialogu. Pokud chceme specifikovat vlastní štětec, musíme v handleru této zprávy vrátit handle štětce (HBRUSH), který bude použit pro výplň pozadí dialogu. Vytvoříme si tedy při spuštění aplikace štětec (na základě bitmapy načtené ze zdrojů), jehož handle budeme vracet v handleru zprávy WM_CTLCOLORDLG. Podívejme se nyní na úplný výpis zdrojového kódu aplikace:

#define WIN32_LEAN_AND_MEAN
#include 
#include 
#include 
#include "resource.h"

HBRUSH g_hbPozadi;
HINSTANCE g_hInst;
LOGFONT logFont;
HFONT hfNadpis;

INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam,
                            LPARAM lParam)
{
  switch ( uMsg )
  {
    case WM_COMMAND:
      switch ( LOWORD(wParam) )
      {
        case IDOK:
        case IDCANCEL:
          DeleteObject(g_hbPozadi);
          EndDialog(hwndDlg, LOWORD(wParam));
          break;
      }
      break;
    case WM_CTLCOLORDLG:
      SetBkMode((HDC)wParam, TRANSPARENT);
      return (INT_PTR)g_hbPozadi;
      break;
    case WM_CTLCOLORLISTBOX:
      SetBkMode((HDC)wParam, TRANSPARENT);
      SetTextColor((HDC)wParam, 0x00FFA0A0);
      return (INT_PTR)g_hbPozadi;
    case WM_CTLCOLOREDIT:
      SetTextColor((HDC)wParam, 0x00FFFFFF);
      SetBkMode((HDC)wParam, TRANSPARENT);
      if ( (HWND)lParam == GetDlgItem(hwndDlg, IDC_EDIT2))
        SetTextColor((HDC)wParam, 0x00D0A0A0FF);
      return (INT_PTR)g_hbPozadi;
      break;
    case WM_CTLCOLORSTATIC:
      if ( (HWND)lParam == GetDlgItem(hwndDlg, IDC_NADPIS))
        SetTextColor((HDC)wParam, 0x00A0FFFF);
      else
        SetTextColor((HDC)wParam, 0x00FFFFA0);
      SetBkMode((HDC)wParam, TRANSPARENT);
      return (INT_PTR)g_hbPozadi;
      break;
    case WM_INITDIALOG:
      g_hbPozadi = CreatePatternBrush(LoadBitmap(g_hInst, 
                           MAKEINTRESOURCE(IDB_POZADI)));
      GetObject(GetStockObject(DEFAULT_GUI_FONT), sizeof(logFont), &logFont);
      logFont.lfHeight = -22;
      logFont.lfItalic = TRUE;
      lstrcpy(logFont.lfFaceName, TEXT("Times New Roman"));
      hfNadpis = CreateFontIndirect(&logFont);
      SendDlgItemMessage(hwndDlg, IDC_NADPIS, WM_SETFONT, (WPARAM)hfNadpis, 
                                     (LPARAM)TRUE);
      SendMessage(GetDlgItem(hwndDlg, IDC_LIST1), LB_ADDSTRING, 0, 
                          (LPARAM)TEXT("1. řádek list-boxu"));
      SendMessage(GetDlgItem(hwndDlg, IDC_LIST1), LB_ADDSTRING, 0, 
                          (LPARAM)TEXT("2. řádek list-boxu"));
      SendMessage(GetDlgItem(hwndDlg, IDC_LIST1), LB_ADDSTRING, 0, 
                          (LPARAM)TEXT("3. řádek list-boxu"));
      SendMessage(GetDlgItem(hwndDlg, IDC_LIST1), LB_ADDSTRING, 0, 
                          (LPARAM)TEXT("4. řádek list-boxu"));
      SendMessage(GetDlgItem(hwndDlg, IDC_LIST1), LB_ADDSTRING, 0, 
                          (LPARAM)TEXT("5. řádek list-boxu"));
      break;
  }  
  return FALSE;
}

int WINAPI _tWinMain(HINSTANCE hInstance,  HINSTANCE hPrevInstance,
  LPTSTR lpCmdLine, int nCmdShow)
{
  g_hInst = hInstance;
  InitCommonControls();
  return (int)DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG_HLAVNI), NULL,
    (DLGPROC)DialogProc);
}

Z uvedeného je vidět, že štetec g_hbPozadi použijeme jako návratovou hodnotu u všech zpráv typu WM_CTLCOLORxxx. Pro všechny tyto zprávy také platí, že jejich parametr wParam je handle kontextu zařízení (HDC) a lParam je handle (HWND) okna prvku, kterého se tato zpráva týká. V případě prvků jako je edit, static nebo list-box pak můžeme jednoduše použít handle kontextu zařízení (parametr wParam) pro nastavení barvy textu a pozadí textu těchto prvků pomocí funkcí SetTextColor, SetBkColor a SetBkMode - jak vidíte ve výpisu.

Téma vlastního vzhledu tlačítek si necháme na příští pokračování.

V reakci na žádosti některých čtenářů jsem umístil do balíku ke stažení kromě zdrojů také spustitelný exe soubor aplikace (v release konfiguraci).

Doprovodný projekt je ke stažení zde: win_api_27.zip

Uživatelsky kreslená tlačítka.

V minulém článku jsme si slíbili, že úpravu vzhledu tlačítek si necháme do dalšího pokračování. Bylo tomu tak z důvodů, že u tlačítek nelze použít zprávu skupiny WM_CTLCOLORxxx tak, jak jsme si použili u prvků v minulém díle.

win-api-28

U tlačítek máme v zásadě 2 možnosti. Tou jednodušší je možnost nastavit tlačítku ikonu nebo bitmapu. Z toho však plynou leckdy závažná omezení. Nemůžeme například dynamicky měnit text tlačítka (funkcí SetWindowText), neboť text okna se na tlačítku nezobrazí.Celý "obraz" tlačítka totiž je tvořen tlačítkem nakresleným systémem, na kterém je námi nastavená bitmapa. Nic více tam již nedostaneme. Museli bychom tedy mít předem připraveny bitmapy se všemi možnými texty a tyto celé bitmapy pak dynamicky měnit, což by určitě bylo pracné a také náročné na zdroje programu. Druhou možností je použít tzv. uživatelsky kreslená tlačítka. Pak máme možnost plné kontroly nad vzhledem tlačítka a můžeme na něj kreslit vše, co umíme nakreslit do kontextu zařízení (HDC). Na obrázku vidíte vlevo ukázku tlačítka s nastavenou bitmapou. K nastavení bitmapy (popř. ikony) použujeme zprávu BM_SETIMAGE, tak jak vidíte v ukázce kódu doprovodného projektu:
// Při spuštění nastavíme tlačítku bitmapu (handler WM_INITDIALOG)
void On_InitDialog(HWND hWnd)
{
  SendDlgItemMessage(hWnd, IDC_HONZA, BM_SETIMAGE, (WPARAM)IMAGE_BITMAP,
    (LPARAM)LoadBitmap(g_hInst, MAKEINTRESOURCE(IDB_HONZA)));
}
Tato funkce je volána v handleru zprávy WM_INITDIALOG, tedy "těsně" před prvním zobrazením dialogu na obrazovce. Ještě musíme v editoru zdrojů příslušnému tlačítku nastavit vlastnost "Bitmap" na True (styl okna BS_BITMAP). Nyní se podívejme na další 2 tlačítka, která jsou již plně uživatelsky kreslena. V editoru zdrojů musíme takovým tlačítkům nastavit vlastnost "Owner Draw" na True (odpovídá to stylu okna BS_OWNERDRAW). Když nyní dialog spustíme, tyto tlačítka vůbec na dialogu nebudou vidět, musíme je tedy ve správný čas kreslit sami. O tom, kdy nastal ten správný čas, nás informuje systém. Rodičovskému oknu, tedy našemu dialogu je poslána zpráva WM_DRAWITEM, obsahující v parametru lParam adresu struktury DRAWITEMSTRUCT:
typedef struct tagDRAWITEMSTRUCT {
    UINT  CtlType;
    UINT  CtlID;
    UINT  itemID;
    UINT  itemAction;
    UINT  itemState;
    HWND  hwndItem;
    HDC  hDC;
    RECT  rcItem;
    ULONG_PTR  itemData;
} DRAWITEMSTRUCT;
V této struktuře máme všechny potřebné informace k vykreslení buttonu (totéž platí i o dalších prvcích podporující uživatelské kteslení - listbox, combobox apod.). Struktura obsahuje také informace o stavu tlačítka - zda je například právě zamáčknuté, zda má klávesnicový fokus apod. Podívejme se nyní na konkrétní realizaci. V projektu máme 2 tlačítka kreslená uživatelsky. Chceme mít na obou stejné pozadí tvořené bitmapou. Tuto bitmapu si tedy přidáme do zdrojů (IDB_POZADI). Zobrazované ikony jsou systémové ikony (IDI_INFORAMTION a IDI_ERROR) a každé tlačítko má svůj vlastní text. Vše ostatní již realizujeme v handleru zprávy WM_DRAWITEM, ve kterém zavoláme následující vlastní kreslící funkci, které jako parametr předáme lParam procedury dialogu.
// Uživatelské kreslení tlačítek
void On_DrawItem(LPDRAWITEMSTRUCT lpDIS)
{
  HBRUSH hbPozadi = CreatePatternBrush(LoadBitmap(g_hInst, MAKEINTRESOURCE(IDB_POZADI)));
  if ( lpDIS->itemState & ODS_SELECTED )
    DrawFrameControl(lpDIS->hDC, &lpDIS->rcItem, DFC_BUTTON, DFCS_BUTTONPUSH | DFCS_PUSHED);
  else
    DrawFrameControl(lpDIS->hDC, &lpDIS->rcItem, DFC_BUTTON, DFCS_BUTTONPUSH);
  InflateRect(&lpDIS->rcItem, -4,-4);
  FillRect(lpDIS->hDC, &lpDIS->rcItem, hbPozadi);
  SetBkMode(lpDIS->hDC, TRANSPARENT);
  SetTextColor(lpDIS->hDC, 0x00A0FFFF);
  switch ( lpDIS->CtlID )
  {
    case IDC_INFO:
      DrawIcon(lpDIS->hDC,
        (lpDIS->rcItem.right - lpDIS->rcItem.left + 8
          - GetSystemMetrics(SM_CXICON))/2,
        lpDIS->rcItem.top + 5,
        LoadIcon(NULL, IDI_INFORMATION));
        lpDIS->rcItem.top = lpDIS->rcItem.bottom - 20;
      DrawText(lpDIS->hDC, TEXT("O programu"), -1, &lpDIS->rcItem,
        DT_SINGLELINE | DT_CENTER | DT_VCENTER);
      break;
    case IDC_CHYBA:
      DrawIcon(lpDIS->hDC,
        (lpDIS->rcItem.right - lpDIS->rcItem.left + 8
          - GetSystemMetrics(SM_CXICON))/2,
        lpDIS->rcItem.top + 5,
        LoadIcon(NULL, IDI_ERROR));
        lpDIS->rcItem.top = lpDIS->rcItem.bottom - 20;
      DrawText(lpDIS->hDC, TEXT("Bug report"), -1, &lpDIS->rcItem,
        DT_SINGLELINE | DT_CENTER | DT_VCENTER);
      break;
  }
  DeleteObject(hbPozadi);
}

/////////////////////////////////////////////////////////////////////////////
// procedura dialogu

INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch ( uMsg )
  {
    case WM_COMMAND:
      switch ( LOWORD(wParam) )
      {
        case IDOK:
        case IDCANCEL:
          DeleteObject(g_hbPozadi);
          EndDialog(hwndDlg, LOWORD(wParam));
          break;
        case IDC_HONZA:
          ShellExecute(NULL, TEXT("open"),
            TEXT("http://www.rplusj.cz/index.asp?stranka=fotogalerie.htm"),
            NULL, NULL, SW_SHOWMAXIMIZED);
          break;
        case IDC_CHYBA:
          ShellExecute(NULL, TEXT("open"),
            TEXT("http://www.rplusj.cz/obrazek.asp?src=data/foto/sranda/kdyz_usa_prohraje.jpg"),
            NULL, NULL, SW_SHOWMAXIMIZED);
          break;
        case IDC_INFO:
          ShellExecute(NULL, TEXT("open"),
            TEXT("http://www.rcsoft.cz"),
            NULL, NULL, SW_SHOWMAXIMIZED);
          break;
      }
      break;
    case WM_DRAWITEM:
      On_DrawItem((LPDRAWITEMSTRUCT)lParam);
      break;
    case WM_INITDIALOG:
      On_InitDialog(hwndDlg);
      break;
  }  
  return FALSE;
}
Doprovodný projekt je ke stažení zde: win_api_28.zip

Uživatelsky kreslený ListBox.

V tomto pokračování ještě zůstaneme u uživatelsky kreslených prvků. Vytvoříme si ListBox, do kterého budeme jako položky přidávat vybrané soubory. V ListBoxu bude pak zobrazena ikona asociovaná s příslušným souborem a jméno souboru vypsané tak, že v případě delšího názvu bude povolen víceřádkový výpis do zbylého prostoru položky ListBoxu, jak vidíte na obrázku:

win-api-uk-listbox

U uživatelsky kresleného ListBoxu (a podobně některých další ovládacích prvků) musíme kromě zprávy WM_DRAWITEM reagovat na zprávu WM_MEASUREITEM, ve které systému řekneme požadované rozměry (v případě ListBoxu výšku) položky. V našem případě budeme zobrazovat velké ikony, takže výšku položky nastavíme a výšku ikony plus řekněme 4 pixely. Tuto zprávu ošetříme přímo v proceduře dialogu, pro obsluhu zprávy WM_DRAWITEM si vytvoříme vlastní funkci, stejně jako pro zobrazení dialogu výběru souboru a přidání vybraného souboru jako položky ListBoxu. Procedura dialogu bude vypadat následovně:
// Procedura dialogu
INT_PTR CALLBACK DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  LPMEASUREITEMSTRUCT lpMIS;
  switch ( uMsg )
  {
    case WM_COMMAND:
      switch ( LOWORD(wParam) )
      {
        case IDOK:
          EndDialog(hWnd, IDOK);
          break;
        case IDCANCEL:
          EndDialog(hWnd, IDCANCEL);
          break;
        case IDC_PRIDAT:
          PridatSoubor(hWnd);
          break;
      }
      break;
    case WM_INITDIALOG:
      break;
    case WM_DRAWITEM:
      DrawItem((LPDRAWITEMSTRUCT)lParam);
      break;
    case WM_MEASUREITEM:
      lpMIS = (LPMEASUREITEMSTRUCT)lParam;
      lpMIS->itemHeight = GetSystemMetrics(SM_CYICON) + 4;
      break;
  }
  return FALSE;
}
Funkce pro přidání vybraného souboru jako položky je již pro čtenáře seriálu jednoduchá, pro úplnost ji zde uvedu:
void PridatSoubor(HWND hWnd)
{
  TCHAR szSoubor[MAX_PATH];
  lstrcpy(szSoubor, _T(""));
  OPENFILENAME ofn;
  ZeroMemory(&ofn, sizeof(OPENFILENAME));
  ofn.lStructSize = sizeof(OPENFILENAME);
  ofn.hwndOwner = hWnd;
  ofn.lpstrFile = szSoubor;
  ofn.nMaxFile = sizeof(szSoubor);
  ofn.lpstrFilter = TEXT("Všechny soubory\0*.*\0");
  ofn.lpstrFileTitle = NULL;
  ofn.nMaxFileTitle = 0;
  ofn.lpstrInitialDir = NULL;
  ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
  if ( !GetOpenFileName(&ofn) ) 
    return;
  SendDlgItemMessage(hWnd, IDC_LISTBOX, LB_ADDSTRING, 0,
    (LPARAM)szSoubor);
}
Nyní zbývá to nejdůležitější, vykreslit položku ListBoxu při obdržení zprávy WM_DRAWITEM. Identifikátor dané položky dostaneme jako prvek itemID struktury DRAWITELSTRUCT. Z toho je již snadné získat text položky, což je plné jméno souboru. Handle asociované ikony získáme pomocí funkce ExtractAssociatedIcon. Dále zjistíme, zda se jedná o právě vybranou položku otestování přítomnosti hodnoty ODS_SELECTED v prvku itemState. Podle výsledku vyplníme plochu položky jedním z příslušných systémových štětců. Poté vykreslíme ikonu do levé části položky a do zbylého obdélníka vypíšeme název souboru funkcí DrawText, která umožňuje nastavit automatické zalamování řádek v rámci daného obdélníka.
void DrawItem(LPDRAWITEMSTRUCT lpDIS)
{
  TCHAR szSoubor[MAX_PATH];
  HBRUSH hbPozadi;
  SendMessage(lpDIS->hwndItem, LB_GETTEXT, lpDIS->itemID, (LPARAM)szSoubor);
    WORD index = 0;
  HICON hIcon = ExtractAssociatedIcon(g_hInstance, szSoubor, &index);
  if ( lpDIS->itemState & ODS_SELECTED )
  {
    hbPozadi = GetSysColorBrush(COLOR_HIGHLIGHT);
    SetTextColor(lpDIS->hDC, GetSysColor(COLOR_HIGHLIGHTTEXT));
  }
  else
  {
    hbPozadi = GetSysColorBrush(COLOR_WINDOW);
    SetTextColor(lpDIS->hDC, GetSysColor(COLOR_WINDOWTEXT));
  }
  
  SetBkMode(lpDIS->hDC, TRANSPARENT);
  FillRect(lpDIS->hDC, &lpDIS->rcItem, hbPozadi);
  
  DrawIcon(lpDIS->hDC, lpDIS->rcItem.left + 1,
    lpDIS->rcItem.top + 2, hIcon);
  RECT rect;
  CopyMemory(&rect, &lpDIS->rcItem, sizeof(RECT));
  rect.left += 34;
  DrawText(lpDIS->hDC, szSoubor, -1, &rect,
    DT_WORDBREAK | DT_VCENTER);
}
Doprovodný projekt je ke stažení zde: win_api_uk_listbox.zip

Uživatelsky kreslený ListBox - II.

V dnešním pokračování si vylepšíme "souborový" ListBox z minulého článku. Budeme implementovat následující:

  • volbu zobrazení malé/velké ikony
  • umožníme přidávat položky (soubory) přetažením (Drag and Drop)
  • otevírat soubory poklepáním (dvojklikem) na položku
  • zefektivníme (na námět čtenáře) vykreslování.
  • zobrazování pouze názvů souborů bez cesty

win-api-uk-listbox-2

Volba zobrazení malé/velké ikony

Nastavení malé nebo velké ikony si budeme udržovat pro další použití v globální proměnné a dále si vytvoříme funkci pro změnu této volby, která zajistí okamžité překreslení ListBoxu.

BOOL g_VelkeIkony = TRUE;
void NastavVelkeIkony(HWND hListBox, BOOL Velke)
{
  g_VelkeIkony = Velke;
  int vyska;
  if ( g_VelkeIkony )
    vyska = GetSystemMetrics(SM_CYICON)+2;
  else
    vyska = GetSystemMetrics(SM_CYSMICON)+2;
  SendMessage(hListBox, LB_SETITEMHEIGHT, 0, (LPARAM)vyska);
  RedrawWindow(hListBox, NULL, NULL,
    RDW_ERASE | RDW_INVALIDATE | RDW_ERASENOW );
}

Vytvoříme si novou funkci na přidání již vybraného souboru, kterou budeme volat ze dvou míst. Dále si změníme způsob získávání handle asociované ikony při vykreslování. Tento handle si uložíme do dat položky při jejím přidání a při vykreslování pak při jeho získání ušetříme nějaký ten takt procesoru.

void PridatPolozku(HWND hListBox, LPCTSTR lpSoubor)
{
  LRESULT Polozka =
    SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM)lpSoubor);
  WORD index = 0;
  HICON hIcon = ExtractAssociatedIcon(g_hInstance, (LPTSTR)lpSoubor, &index);
  SendMessage(hListBox, LB_SETITEMDATA, Polozka, (LPARAM)hIcon);
}

Vykreslování položek

Upravíme si také obsluhu zprávy WM_DRAWITEM. Nyní budeme zobrazovat pouze názvy souborů bez cesty, musíme vzít v úvahu aktuální nastavení malé-velké ikony a handle ikony nyní získáváme z dat položky, které dostaneme jako prvek itemData struktury DRAWITEMSTRUCT. Celá obsluha bude nyní vypadat takto:

void DrawItem(LPDRAWITEMSTRUCT lpDIS)
{
  TCHAR szSoubor[MAX_PATH];
  HBRUSH hbPozadi;
  SendMessage(lpDIS->hwndItem, LB_GETTEXT, lpDIS->itemID, (LPARAM)szSoubor);
    WORD index = 0;
  int xIkona, yIkona; // rozměry malé nebo velké ikony
  if ( g_VelkeIkony )
  {
    xIkona = GetSystemMetrics(SM_CXICON);
    yIkona = GetSystemMetrics(SM_CYICON);
  }
  else
  {
    xIkona = GetSystemMetrics(SM_CXSMICON);
    yIkona = GetSystemMetrics(SM_CYSMICON);
  }
  HICON hIcon = (HICON)lpDIS->itemData;
  if ( lpDIS->itemState & ODS_SELECTED )
  {
    hbPozadi = GetSysColorBrush(COLOR_HIGHLIGHT);
    SetTextColor(lpDIS->hDC, GetSysColor(COLOR_HIGHLIGHTTEXT));
  }
  else
  {
    hbPozadi = GetSysColorBrush(COLOR_WINDOW);
    SetTextColor(lpDIS->hDC, GetSysColor(COLOR_WINDOWTEXT));
  }
  
  SetBkMode(lpDIS->hDC, TRANSPARENT);
  FillRect(lpDIS->hDC, &lpDIS->rcItem, hbPozadi);
  
  if ( g_VelkeIkony )
    DrawIcon(lpDIS->hDC,
      lpDIS->rcItem.left+1,
      lpDIS->rcItem.top+1, hIcon);
  else
    DrawIconEx(lpDIS->hDC,
      lpDIS->rcItem.left,
      lpDIS->rcItem.top, hIcon,
      xIkona, yIkona,
      0, NULL, DI_NORMAL);
  RECT rect;
  CopyMemory(&rect, &lpDIS->rcItem, sizeof(RECT));
  rect.left += xIkona + 2;
  LPTSTR  lpSoubor = strrchr(szSoubor, '\\');
  if ( !lpSoubor ) // pro jistotu :))
    lpSoubor = szSoubor;
  else
    lpSoubor++;
  DrawText(lpDIS->hDC, lpSoubor, -1, &rect,
    DT_SINGLELINE | DT_VCENTER);
}

Realizace Drag and Drop

Jak nyní na realizaci Drag and Drop způsobu přijímání souborů. Aby okno vůbec mohlo být cílem "puštění", musíme toto povolit pomocí funkce DragAcceptFiles, jejíž 2. parametr povolí nebo zakáže příjem položek pomocí Drag and Drop. Toto v našem případě nastavíme nejlépe v obsluze WM_INITDIALOG.

DragAcceptFiles(GetDlgItem(hWnd, IDC_LISTBOX), TRUE);

Od tohoto okamžiku bude okno dostávat při puštění položky zprávu WM_DROPFILES, jejímž parametrem wParam je handle interní struktury HDROP, ze které získáme potřebná data. Musíme ale nejprve na ListBox aplikovat již dříve probíraný subclassing, tady přesměrování jeho procedury okna na vlastní funkci, což provedeme opět v obsluze WM_INITDIALOG takto:

wndprocLB = (WNDPROC)SetWindowLongPtr(GetDlgItem(hWnd, IDC_LISTBOX),
  GWLP_WNDPROC, (LONG_PTR)WindowProcListBox);

Vlastní procedura okna ListBoxu bude obsluhovat pouze zmíněnou zprávu WM_DROPFILES, při čemž si zavoláme vlastní funkci pro její zpracování. Obě funkce vypadají následovně:

void DropFiles(HWND hWnd, HDROP hDrop)
{
  TCHAR szSoubor[MAX_PATH];
  int Pocet = (int)DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0);
  if ( Pocet <= 0 )
    return;
  for ( int i = 0; i < Pocet; i++ )
  {
    DragQueryFile(hDrop, i, szSoubor, MAX_PATH);
    PridatPolozku(hWnd, szSoubor);
  }
  DragFinish(hDrop);
}

WNDPROC wndprocLB;

LRESULT CALLBACK WindowProcListBox(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch ( uMsg )
  {
    case WM_DROPFILES:
      DropFiles(hWnd,  (HDROP)wParam);
      break;
  }
  return CallWindowProc(wndprocLB,  hWnd, uMsg, wParam, lParam);
}

Otevírání souborů v položkách

Nyní zbývá ještě realizovat schopnost na poklepání (dvojklik) položky příslušný soubor otevřít stejným způsobem jako Průzkumník Windows, tedy v přidruženém programu, pokud takový existuje, nebo přímo spustit pokud jde o spustitelný soubor. Budeme tedy zachytávat v proceduře dialogu oznamovací zprávu LBN_DBLCLK a z ní volat vlastní funkci které předáme handle ListBoxu a která vypadá takto..

void LBNDblClk(HWND hListBox)
{
  TCHAR szSoubor[MAX_PATH];
  LRESULT Vyber = SendMessage(hListBox, LB_GETCURSEL, 0, 0);
  SendMessage(hListBox, LB_GETTEXT, Vyber,
    (LRESULT)szSoubor);
  ShellExecute(NULL, _T("open"), szSoubor,
    NULL, NULL, SW_SHOWNORMAL);
}

Pro úplnost si v části kódu procedury dialogu ukážeme její volání. Z procedury dialogu je ve výpisu pouze obsluha oznamovacích zpráv ListBoxu a CheckBoxu přepínajícího zobrazení na malé/velké ikony.

INT_PTR CALLBACK DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch ( uMsg )
  {
    case WM_COMMAND:
      switch ( LOWORD(wParam) )
      {
        case IDC_LISTBOX:
          if ( HIWORD(wParam) == LBN_DBLCLK )
            LBNDblClk((HWND)lParam);
          break;
        case IDC_VELKE_IKONY:
          if ( HIWORD(wParam) == BN_CLICKED )
          {
            NastavVelkeIkony(GetDlgItem(hWnd, IDC_LISTBOX),
              IsDlgButtonChecked(hWnd, IDC_VELKE_IKONY) == BST_CHECKED);
          }
          break;
      }
      break;
  }
  return FALSE;
}

Doprovodný projekt je ke stažení zde: win_api_uk_listbox_2.zip

Výběr složky a naplnění ListBoxu soubory.

V tomto pokračování se dotkneme 2 témat. Využijeme ListBoxu z minulého dílu, abychom si ukázali, jak jej lze naplnit soubory ze zvolené složky. V té souvislosti se naučíme použít systémový dialog umožňující uživateli vybrat složku.

Dialog výběru složky

Nejprve si ukažme, jak vytvořit dialog pro výběr složky, aniž bychom museli použít běžný dialog pro výběr souboru (což by bylo poněkud křečovité, i když řešitelné), jehož ukázku vidíte na obrázku:

win-api-dir-listbox

Jádrem je použití funkce SHBrowseForFolder, která je deklarovaná v hlavičkovém souboru shlobj.h, který si tedy musíme vložit do zdrojového kódu. Pro použití této funkce musíme vytvořit vlastní callback funkci, která má parametry jako procedura okna a do které jsou posílány určité zprávy v době kdy je zobrazen dialog výběru složky. Zpráva BFFM_SELCHANGED přijde, pokud uživatel změní aktuálně vybranou složku. My pak máme možnost vypsat tuto složku do stavového textu (nad oknem se stromem složek). Při zachycení zprávy BFFM_INITIALIZED, která přijde jednou bezprostředně po inicializaci dialogu, zase můžeme nastavit požadovanou počáteční složku procházení. Pokud bychom chtěli, můžeme také pomocí zprávy BFFM_SETOKTEXT nastavit vlastní text tlačítku OK. Kód této funkce může tedy vypadat následovně:
// callback procedura pro funkci SHBrowseForFolder

TCHAR chStartBrowseDir[MAX_PATH];

int CALLBACK BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData)
{
  TCHAR szDir[MAX_PATH];
  switch ( uMsg )
  {
    case BFFM_INITIALIZED:
      if ( chStartBrowseDir != NULL )
        SendMessage(hwnd, BFFM_SETSELECTION, TRUE, (LPARAM)chStartBrowseDir);
      break;
    case BFFM_SELCHANGED:
      if ( SHGetPathFromIDList((LPITEMIDLIST)lParam, szDir))
        SendMessage(hwnd, BFFM_SETSTATUSTEXT, 0, (LPARAM)szDir);
      break;
  }
  return 0;
}
Nyní jak vypadá vlastní vyvolání dialogu. Vytvoříme si funkci, v jejích parametrech si budeme předávat požadovanou výchozí složku, okno vlastníka dialogu, vlastní titulek dialogu (který mám možnost změnit) a dále textový buffer, do kterého nám funkce naplní uživatelem vybranou složku. Návratová hodnota funkce nám bude říkat, zda uživatel složku skutečně vybral nebo zda ukončil dialog tlačítkem Storno. Celá funkce pak vypadá takto:
// Vyvolá dialog vybrání složky
BOOL VyberSlozku(LPTSTR lpVybranaSlozka, LPCTSTR lpStart, LPCTSTR lpNadpis, HWND hVlastnik)
{
  if ( lpStart != NULL )
    lstrcpy(chStartBrowseDir, lpStart);
  BOOL bResult = TRUE;
  LPITEMIDLIST pidl  = NULL;
  BROWSEINFO bi;
  ZeroMemory(&bi, sizeof(bi));
  LPMALLOC pMalloc = NULL;
  SHGetMalloc(&pMalloc);
  if ( IsWindow(hVlastnik) )
    bi.hwndOwner = hVlastnik;
  else
    bi.hwndOwner = NULL;
  bi.pszDisplayName = 0;
  bi.pidlRoot = 0;
  bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_STATUSTEXT;
  bi.lpfn = BrowseCallbackProc;
  if ( lpNadpis == NULL )
    bi.lpszTitle = _T("Vyberte požadovanou složku");
  else
    bi.lpszTitle = lpNadpis;
  pidl= SHBrowseForFolder(&bi);
  if ( pidl )
  {
    if ( !SHGetPathFromIDList(pidl, lpVybranaSlozka) )
      bResult = FALSE;
    pMalloc->Free(pidl);
  }
  else
    bResult = FALSE;
  pMalloc->Release();
  return bResult;
}

Naplnění ListBoxu obsahem složky

Nyní máme vybranou složku a chceme naplnit náš ListBox všemi soubory obsaženými ve vybrané složce. Vzhledem k tomu, že náš ListBox může být uživatelsky kreslený, musíme postupovat různým způsobem. Abychom měli funkci v run-timu univerzální, otestujeme si za běhu, zda je ListBox nastaven jako uživatelsky kreslený, a podle výsledku použijeme jednu nebo druhou variantu. Rozhodovací funkce bude testovat přítomnost jednoho ze stylu ListBoxu označujícího uživatelsky kreslený ListBox - LBS_OWNERDRAWFIXED a LBS_OWNERDRAWVARIABLE. Funkce bude vypadat takto:
void PridatSlozku(HWND hWnd)
{
  TCHAR szSlozka[MAX_PATH] = {0};
  if ( !VyberSlozku(szSlozka, szSlozka,  _T("Přidat složku do ListBoxu"), hWnd) )
    return;
  // Otestujeme, zda je uživatelsky kreslený
  if ( GetWindowLongPtr(GetDlgItem(hWnd, IDC_LISTBOX), GWL_STYLE)
    & (LBS_OWNERDRAWFIXED | LBS_OWNERDRAWVARIABLE) )
    PridejZeSlozky(hWnd, szSlozka);
  else
  {
    lstrcat(szSlozka, _T("\\*.*"));
    SendDlgItemMessage(hWnd, IDC_LISTBOX, LB_DIR, 0,
      (LPARAM)szSlozka);
  }
}
Jak je vidět z výpisu, pro naplnění ListBoxu soubory z dané složky slouží zpráva LB_DIR. Do ListBoxu jsou jako položky přidána pouze jména souborů bez cesty. V parametru lParam je šablona, ve které s využitím běžně známých takzvaných "divokých znaků" (* a ?) specifikujeme, které soubory mají být přidány. V parametr wParam pak můžeme uvést kombinaci jedné nebo více hodnot, specifikujících, jaké další objekty chceme přidat. Úplný seznam cca 8 hodnot naleznete v popisu této zprávy v dokumentaci, jako příklad uveďme:
  • DDL_DIRECTORY - zahrne také podsložky (jejich názvy jsou uvedeny v hranatých závorkách [] )
  • DDL_HIDDEN - zahrne do seznamu také skryté soubory
V případě našeho uživatelsky kresleného ListBoxu nemůžeme tuto zprávu použít, neboť potřebujeme při každém jednotlivém přidání souboru nastavit data položky. V příštím pokračování si ukážeme, jak prohledávat obsah složky a získat výčet souborů. Doprovodný projekt je ke stažení zde: win_api_dir_listbox.zip

Vyhledávání souborů - zjištění obsahu složky.

V tomto článku si ukážeme, jak získat seznam souborů a podsložek, obsažených v zadané složce. Výsledkem bude program, který vyvolá již probíranou funkci pro výběr složky, upravenou tak, že během procházení jednotlivými složkami uživatelem bude do ListBoxu na hlavním dialogu zobrazován seznam všech souborů v této složce (s odfiltrováním podsložek) s uvedením jejich velikosti a data poslední změny, jak je vidět na následujícím obrázku:

win-api-find-file

Získání obsahu složky nám umožňují funkce FindFirstFile a FindNextFile. Princip použití je následující: nejprve zavoláme funkci FindFirstFile, které zadáme vzor, kterému musí vyhovovat nalezené objekty (soubory či složky). Funkce nám v případě úspěchu naplní strukturu WIN32_FIND_DATA a vrátí handle. Tyto hodnoty pak použijeme v opakovaném hledání dalších objektů pomocí funkce FindNextFile. Nakonec musíme zavřít použitý handle. Struktura WIN32_FIND_DATA obsahuje různé informace o nalezeném objektu. V našem případě použijeme jméno souboru (cFilename) a prvek dwFileAttributes, pomocí kterého zjistíme, zda objektem je složka, které chceme ze seznamu vyloučit. Celý postup je vidět v následujícím výpisu:

void PridejSoubor(HWND hListBox, LPWIN32_FIND_DATA lpWFD)
{
  TCHAR szText[MAX_PATH+60];
  TCHAR szVelikost[25];
  TCHAR szCas[25];
  FILETIME fileTime;
  SYSTEMTIME sysTime;
  StrCpy(szText, lpWFD->cFileName);
  StrFormatByteSizeA(lpWFD->nFileSizeLow, szVelikost, sizeof(szVelikost));
  FileTimeToLocalFileTime(&lpWFD->ftLastAccessTime, &fileTime);
  FileTimeToSystemTime(&fileTime, &sysTime);
  _stprintf(szCas, _T("změněno dne: %d.%d.%d"),
    sysTime.wDay, sysTime.wMonth, sysTime.wYear);
  StrCat(szText, _T(" ( velikost: "));
  StrCat(szText, szVelikost);
  StrCat(szText, _T(", "));
  StrCat(szText, szCas);
  StrCat(szText, _T(" )"));
  SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM)szText);
}

void PridejZeSlozky(HWND hWnd, LPTSTR lpSlozka)
{
  TCHAR szSoubor[MAX_PATH];
  StrCpy(szSoubor, lpSlozka);
  StrCat(szSoubor, _T("\\*.*"));
  HWND hListBox = GetDlgItem(hWnd, IDC_LISTBOX);
  // Nejprve odstraníme případné staré položky ListBoxu
  SendMessage(hListBox, LB_RESETCONTENT, 0, 0);
  WIN32_FIND_DATA wfd;
  ZeroMemory(&wfd, sizeof(wfd));
  // Nalezneme 1. objekt
  HANDLE hFind = FindFirstFile(szSoubor, &wfd);
  if ( hFind == INVALID_HANDLE_VALUE )
    return;
  // Vyloučit složky
  if ( !(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) )
    PridejSoubor(hListBox, &wfd);
  // Procházíme všemi objekty ve složce
  while ( FindNextFile(hFind, &wfd) )
  {
    if ( !(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) )
      PridejSoubor(hListBox, &wfd);
  }
  // Zavřít handle vyhledávání
  FindClose(hFind);
}

Ve funkci PridejSoubor je dále vidět, jak lze pomocí funkce StrFormatByteSize získat textové vyjádření zadané velikosti (v bytech), přičemž funkce sama zvolí použití jednotky (kB, MB..) podle aktuální velikosti. Dále je zde ukázán převod a výpis časového údaje v souboru (typ FILETIME) na místní čas.

Nyní si ukážeme, jak zařídit, aby uvedená funkce byla volaná vždy při změně složky v dialogu výběru složky. Ve funkci BrowseCallbackProc využijeme parametr lpData, který představuje prvek lParam struktury BROWSEINFO, kterou plníme před voláním funkce SHBrowseForFolder. Využijeme ho pro předání handle dialogu, potřebné při volání funkce PridejZeSlozky, kterou si tak můžeme zavolat přímo zevnitř této callback funkce, jak je vidět na jejím výpisu:

int CALLBACK BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData)
{
  TCHAR szDir[MAX_PATH];
  switch ( uMsg )
  {
    case BFFM_INITIALIZED:
      if ( chStartBrowseDir != NULL )
        SendMessage(hwnd, BFFM_SETSELECTION, TRUE, (LPARAM)chStartBrowseDir);
      break;
    case BFFM_SELCHANGED:
      if ( SHGetPathFromIDList((LPITEMIDLIST)lParam, szDir))
      {
        SendMessage(hwnd, BFFM_SETSTATUSTEXT, 0, (LPARAM)szDir);
        PridejZeSlozky((HWND)lpData, szDir);
      }
      break;
  }
  return 0;
}

Pro úplnost uvedeme také její volání:

BOOL VyberSlozku(LPTSTR lpVybranaSlozka, LPCTSTR lpStart, LPCTSTR lpNadpis, HWND hVlastnik)
{
  if ( lpStart != NULL )
    lstrcpy(chStartBrowseDir, lpStart);
  BOOL bResult = TRUE;
  LPITEMIDLIST pidl  = NULL;
  BROWSEINFO bi;
  ZeroMemory(&bi, sizeof(bi));
  LPMALLOC pMalloc = NULL;
  SHGetMalloc(&pMalloc);
  if ( IsWindow(hVlastnik) )
  {
    bi.hwndOwner = hVlastnik;
    bi.lParam = (LPARAM)hVlastnik;
  }
  else
    bi.hwndOwner = NULL;
  bi.pszDisplayName = 0;
  bi.pidlRoot = 0;
  bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_STATUSTEXT;
  bi.lpfn = BrowseCallbackProc;
  if ( lpNadpis == NULL )
    bi.lpszTitle = _T("Vyberte požadovanou složku");
  else
    bi.lpszTitle = lpNadpis;
  pidl= SHBrowseForFolder(&bi);
  if ( pidl )
  {
    if ( !SHGetPathFromIDList(pidl, lpVybranaSlozka) )
      bResult = FALSE;
    pMalloc->Free(pidl);
  }
  else
    bResult = FALSE;
  pMalloc->Release();
  return bResult;
}

Doprovodný projekt je ke stažení zde: win_api_find_file.zip

Pracujeme s ComboBoxem.

V tomto článku poznáme trochu blíže ovládací prvek ComboBox, česky překládaný jako rozbalovací seznam. Podobně jako ListBox umožňuje uživateli výběr z existujících položek. Navíc umožňuje také uživateli přímo zapisovat text do editačního pole, pokud má nastaven příslušný styl.

win-api-combobox

Řekneme si nejdříve o 3 typech ComboBoxu:

Simple ComboBox

Simple ComboBox má ve stylu okna hodnotu CBS_SIMPLE, kterou uvedeme při jeho ručním vytváření nebo ve skriptu prostředků. V editoru prostředků pak jednoduše nastavíme vlastnost Type na hodnotu Simple. Tento typ ComboBoxu má oba prvky (Edit i ListBox) trvale zobrazené. Při změně výběru v ListBoxu se text vybraného řádku automaticky zobrazí v editačním poli ComboBoxu. Uživatel může do editačního pole také zapisovat vlastní text. Pozor! V editoru prostředků musíme po nastavení typu na Simple roztáhnout ComboBox do příslušné požadované výšky. V opačném případě by se zobrazoval pouze ve formě jednoho řádku, ve které by sice bylo možné klávesami šipka nahoru nebo dolů listovat, ale nedosáhli bychom výsledku, který jsme pravděpodobně zamýšleli.

Drop down

Tento typ ComboBoxu (ve stylu okna má hodnotu CBS_DROPDOWN) je představován editačním polem (prvek Edit) s rozbalovacím tlačítkem, přičemž rozbalovací část (prvek ListBox) může být rozbalena pouze v době kdy má ComboBox fokus. Jakmile uživatel přeskočí (například klávesou Tab) na jiné okno, ComboBox se automaticky zabalí, aniž by uživatel proti tomu mohl cokoli dělat. Také v tomto typu ComboBoxu je uživateli umožněno zapisovat do editačního pole. Pozor! Častým problémem začátečníků bývá nastavení dostatečné výšky rozbalovacího seznamu. Pokud totiž necháte ComboBox tak, jak jej umístíte na dialog v editoru prostředků bude i při větším počtu položek "rozbalen" pouze jeden řádek vzhledu prvku Edit s malými skrolovacími šipkami, kterými je možné procházet položky ComboBoxu. Správné nastavení provedeme v editoru prostředků kliknutím na rozbalovací šipku, po kterém se nám zobrazí roztahovací obdélník, který pak můžeme tažením za jeho spodní okraj roztáhnout do velikosti, která představuje maximální výšku, do které se rozbalí ComboBox za běhu programu.

Drop List

Tento typ ComboBoxu (styl okna CBS_DROPDOWNLIST) je podobný předchozímu s tím rozdílem, že uživatel nemůže zapisovat vlastní text. Jeho nastavení v editoru prostředků je stejné jako u předchozího typu Drop down.

Naplnění položek ComboBoxu

Položky ComboBoxu můžeme programově přidávat jednou ze dvou zpráv CB_ADDSTRINGa CB_INSERTSTRING. Zpráva CB_ADDSTRING přidá položku na konec a v případě, že má ComboBox nastavenu vlastnost CBS_SORT, je položka automaticky zatříděna podle abecedy. Vlastnost (styl okna) CBS_SORT nastavíme v editoru prostředků jako vlastnost Sort, která má ve Visual C++ výchozí hodnotu true, i když osobně si myslím, že v praxi u většiny ComboBoxů, které jsem použil, jsem toto automatické třídění zrušil. Druhou zprávou pro přidání položky ComboBoxu je CB_INSERTSTRING, jejímž parametrem lParam je text přidávané položky a parametr wParam určuje index, který má nově přidaná položka mít. Na rozdíl od zprávy CB_ADDSTRING namá nastavení stylu CBS_SORT (automatické třídění) žádný vliv a položka je přidána tam, kam řekneme. Pokud chceme touto zprávou přidat položku na konec, uvedeme jako parametr wParam hodnotu -1. Ukázku přidání položek ComboBoxu funkci voláme při zachycení zprávy WM_INITDIALOG vidíte v následujícím výpisu:
void NaInitDialog(HWND hWnd)
{
  SendDlgItemMessage(hWnd, IDC_DROPDOWN, CB_ADDSTRING, 0, (LPARAM)_T("1. řádek"));
  SendDlgItemMessage(hWnd, IDC_DROPDOWN, CB_ADDSTRING, 0, (LPARAM)_T("2. řádek"));
  SendDlgItemMessage(hWnd, IDC_DROPDOWN, CB_ADDSTRING, 0, (LPARAM)_T("3. řádek"));
  SendDlgItemMessage(hWnd, IDC_DROPDOWN, CB_ADDSTRING, 0, (LPARAM)_T("4. řádek"));
  // přidání dalších položek ...
}

Detekce změny výběru, zjištění a nastavení výběru v programu

V následujícím kódu si ukážeme jak detekovat změnu výběru položky ComboBoxu. Budeme zachytávat změnu jednoho z rozbalovacích ComboBoxů, zjistíme index aktuálně vybrané položky a stejnou položku (index) nastavíme jako vybranou ComboBoxu typu Simple. Funkce kterou budeme volat při zjištění změny výběru uživatelem, vypadá takto:
void NaZmenaVyberu(HWND hCombo)
{
  LRESULT vyber = SendMessage(hCombo, CB_GETCURSEL, 0, 0);
  SendDlgItemMessage(GetParent(hCombo), IDC_SIMPLE, CB_SETCURSEL, vyber, 0);
}
Jak a kdy volat tuto zprávu? Při změně výběru dostane vlastník ComboBoxu, tedy naše dialogové okno oznamovací zprávu CBN_SELCHANGE, která přijde prostřednictvím zprávy WM_COMMAND. V následujícím výseku z kódu procedury dialogu je vidět její obsluha:
INT_PTR CALLBACK DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch ( uMsg )
  {
    case WM_COMMAND:
      switch ( LOWORD(wParam) )
      {
        case IDC_DROPDOWN:
        case IDC_DROPLIST:
          if ( HIWORD(wParam) == CBN_SELCHANGE)
            NaZmenaVyberu((HWND)lParam);
          break;
        case IDC_SIMPLE:
          ZjistiTextPolozky((HWND)lParam);
          break;
        // další příkazy ...
      }
      break;
    // Další zprávy ...
  }
  return FALSE;
}
Využíváme parametru lParam, který u zprávy WM_COMMAND představuje handle okna ovládacího prvku, kterého se zpráva týká. Jak je vidět z dosavadního výkladu, práce s položkami ComboBoxu je obdobná práci s ListBoxem, který jsme již poznali. Rozdílem je pouze první písmeno v názvech identifikátorů příslušných zpráv. Ukažme si ještě spíše pro připomenutí, jak načíst text aktuálně vybrané položky ComboBoxu. Budeme tentokrát detekovat změnu ComboBoxu typu Simple a vypisovat do prvku Static její text. Při detekci změny výběru budeme vola následující funkci, jejíž volání také vidíte ve výpisu nad sebou:
void ZjistiTextPolozky(HWND hCombo)
{
  LPTSTR lpText;
  LRESULT vyber = SendMessage(hCombo, CB_GETCURSEL, 0, 0);
  LRESULT delkaTextu = SendMessage(hCombo, CB_GETLBTEXTLEN, vyber, 0);
  lpText = (LPTSTR)HeapAlloc(GetProcessHeap(), 0,
    (delkaTextu+1) * sizeof(TCHAR));
  SendMessage(hCombo, CB_GETLBTEXT, vyber, (LPARAM)lpText);
  SetDlgItemText(GetParent(hCombo), IDC_COMBO_TEXT, lpText);
  HeapFree(GetProcessHeap(), 0, lpText);
}
Na závěr si ještě ukážeme jak lze programově rozbalit položky ComboBoxu. Na dialog si přidáme tlačítko a v obsluze jeho stisknutí (to jistě již pravidelný čtenář zná) zavoláme tuto funkci, která rozbalí jeden z ComboBoxů na dialogu a navíc nastaví výběr na 5. položku:
void NaRozbalit(HWND hWnd)
{
  SendDlgItemMessage(hWnd, IDC_DROPDOWN, CB_SHOWDROPDOWN, (WPARAM)TRUE, 0);
  SendDlgItemMessage(hWnd, IDC_DROPDOWN, CB_SETCURSEL, (WPARAM)4, 0);
}
Doprovodný projekt je ke stažení zde: win_api_combobox.zip

Pracujeme s ComboBoxem - II.

V tomto článku zůstaneme ještě u ovládacího prvku ComboBox. Ukážeme si některé další techniky jeho použití a ovládání.

win-api-combobox-2

Nastavení šířky rozbalovacího seznamu

Pokud je předpoklad, že ComboBox obsahuje nebo může obsahovat položky, jejichž šíře textu je větší než šířka vlastního ComboBoxu (v nerozbaleném stavu), je žádoucí přiměřeným způsobem nastavit (zvětšit) šířku rozbalovacího seznamu. Provedeme to pomocí zprávy CB_SETDROPPEDWIDTH, jak je vidět v následujícím příkladu, který nastaví tuto šířku na 350 pixelů:
SendDlgItemMessage(hWnd, IDC_COMBO, CB_SETDROPPEDWIDTH, 350, 0);

Jak urychlit naplnění větším množstvím položek

Pokud naplňujeme programově ComboBox najednou velkým množstvím položek, většinou v nějakém cyklu, můžeme urychlit tento proces použitím zprávy WM_SETREDRAW. Touto zprávou totiž můžeme zakázat a nebo opět povolit překreslení prvku (nejen ComboBoxu) při změně jeho obsahu, čímž je přidání jedné položky. Nejprve tedy překreslování zakážeme, provedeme celý cyklus přidání položek a poté překreslení opět povolíme, takže překreslen je až konečný stav po naplnění. Následující kód ukazuje přidání necelých 10000 položek tak, že v položce je vypsáno číslo řádku a hodnota vrácená funkcí GetTickCount, což je počet milisekund od startu systému (samozřejmě její "citlivost" není 1 ms, ale přibývá po určitých skocích -ale to je už jiné téma).
  TCHAR szText[100];
  // Zakážeme překreslování
  SendDlgItemMessage(hWnd, IDC_COMBO, WM_SETREDRAW, FALSE, 0);
  for ( int i = 4; i < 10000; i++ )
  {
    _stprintf(szText, _T("řádek %d. přidán ve %d ms"), i, (int)GetTickCount());
    SendDlgItemMessage(hWnd, IDC_COMBO, CB_ADDSTRING, 0, (LPARAM)szText);
  }
  SendDlgItemMessage(hWnd, IDC_COMBO, CB_SETCURSEL, 2, 0);
  // Povolíme překreslení
  SendDlgItemMessage(hWnd, IDC_COMBO, WM_SETREDRAW, TRUE, 0);
Poznámka pro přemýšlivé: v doprovodném projektu je tento kód pro zjednodušení umístěn v obsluze zprávy WM_INITDIALOG, která přijde ještě před vlastním zobrazením celého dialogu, takže lze předpokládat, že zde je použití této techniky zbytečné. Je zde pouze z demonstračních důvodů.

Zajištění viditelnosti určité položky při programovém rozbalení

V některých případech můžeme chtít, aby po programovém rozbalení ComboBoxu bylo zajištěno, že určitá položka bude ve viditelné části rozbalovacího seznamu bez nutnosti k ní odskrolovat. Toto zajistíme pomocí zprávy CB_SETTOPINDEX, jejímž parametrem wParam je právě index položky, která má být viditelná. Postup vidíte v následující funkci, volané na stisk tlačítka.
void NaRozbalit(HWND hWnd)
{
  SendDlgItemMessage(hWnd, IDC_COMBO, CB_SHOWDROPDOWN, TRUE, 0);
  SendDlgItemMessage(hWnd, IDC_COMBO, CB_SETTOPINDEX,  (WPARAM)200, 0);
}
Výsledek vidíte na úvodním obrázku. Možná vám připadá nějaké divné písmo, kterým jsou vypsány položky v rozbalené části ComboBoxu. Proč je tomu tak, si řekneme nyní.

Jednoduché zjištění dalších informací o ComboBoxu ve Windows XP

Technika, použitá v tomto odstavci je omezena na použití v operačním systému Windows XP. Pro přeložení a vytvoření aplikace není samozřejmě nutné mít Windows XP. Ty jsou nutné pouze pro její spuštění. Naopak pro překlad i ve Windows XP musíte mít dostatečně aktuální verzi Windows SDK (Software Development Kit). Tento "balík" obsahuje (kromě jiného) nutné knihovny a hlavičkové soubory, potřebné pro překlad a sestavení aplikace. Pokud máte Visual C++ .NET, máte již tyto soubory dostatečně aktuální. V případě starší verze Visual C++ lze poslední verzi (v okamžiku kdy píšu tyto řádky je poslední verze z října 2002) stáhnout (samozřejmě zdarma) ze stránek Microsoftu, nebo je součástí balíku, který dostávají předplatitelé vyšších sad MSDN, takže pokud pracujete pro nějakou větší (bohatší) firmu, máte asi toto k dispozici. Tuto aktualizaci lze samozřejmě vřele doporučit (také pro aktualizovanou a stále rozšiřovanou dokumentaci) také vlastníkům Visual C++ .NET. Takže: Ve Windows XP je zavedena nová zpráva CB_GETCOMBOBOXINFO,která nám naplní strukturu COMBOBOXINFO.
typedef struct tagCOMBOBOXINFO {
    DWORD cbSize;
    RECT rcItem;
    RECT rcButton;
    DWORD stateButton;
    HWND hwndCombo;
    HWND hwndItem;
    HWND hwndList;
} COMBOBOXINFO, *PCOMBOBOXINFO, *LPCOMBOBOXINFO;
Význam jednotlivých položek již napovídají jejich názvy:
  • rcItem - obsahuje souřadnice (obdélníka) editačního pole (prvku Edit)
  • stateButton - obsahuje stav rozbalovacího tlačítka ComboBoxu.
  • hwndCombo - handle (okna) ComboBoxu
  • hwndItem - handle (okna) prvku Edit
  • hwndList - handle rozbalovacího pole (ListBoxu)
Podrobný popis této zprávy a struktury naleznete na MSDN, zde si ukážeme příklad jejího použití. Na stisknutí tlačítka zavoláme následující funkci, která nastaví text editačního pole ComboBoxu a dále nastaví vlastní písmo (font) rozbalovacímu poli (ListBoxu). Proto tedy to "divné" písmo na úvodním obrázku. Pro zjednodušení je použit jeden ze systémových fontů, získaných funkcí GetStockObject.
void NaComboInfo(HWND hWnd)
{
  COMBOBOXINFO cbi;
  ZeroMemory(&cbi, sizeof(cbi));
  cbi.cbSize = sizeof(cbi);
  SendDlgItemMessage(hWnd, IDC_COMBO, CB_GETCOMBOBOXINFO, 0, (LPARAM)&cbi);
  SetWindowText((HWND)cbi.hwndItem, _T("Vlastní text"));
  SendMessage((HWND)cbi.hwndList, WM_SETFONT,
    (WPARAM)GetStockObject(ANSI_FIXED_FONT), TRUE);
}
Doprovodný projekt je ke stažení zde: win_api_combobox_2.zip

Rozšířený prvek ComboBoxEx.

V tomto článku si řekneme o rozšířeném prvku ComboBoxEx, který patří stejně jako ComboBox mezi standardní ovládací prvky Windows.

win-api-combobox-ex

Ovládací prvek ComboBoxEx má některé další rozšiřující vlastnosti a schopnosti oproti běžnému prvku ComboBox. Tato výhoda však současně vyžaduje od programátora "trochu více práce" při jeho vytvoření a použití. Dejme se tedy do práce. Prvním krokem, který musíme učinit před vlastním vytvořením tohoto prvku je správná inicializace knihovny běžných ovládacích prvků, comctl32. Provedeme ji pomocí funkce InitCommonControlsEx s použitím hodnoty ICC_USEREX_CLASSES, jak vidíte v následujícím kódu, umístěném před vlastním vytvořením okna nebo dialogového okna obsahujícího ComboBoxEx.
  INITCOMMONCONTROLSEX icc;
  icc.dwSize = sizeof(INITCOMMONCONTROLSEX);
  icc.dwICC = ICC_WIN95_CLASSES  | ICC_USEREX_CLASSES;
  if ( !InitCommonControlsEx(&icc) )
    return -1;
Dalším rozdílem je, že prvek ComboBoxEx nemůžeme definovat přímo ve skriptu prostředků, tedy nemůžeme jej vytvořit vizuálním editorem dialogu. Vytvoříme jej tedy v obsluze zprávy WM_INITDIALOG pomocí funkce CreateWindowEx. Tento prvek dále umožňuje jednoduchým způsobem (bez uživatelského kreslení, které samozřejmě není nijak složité, ale..) zobrazovat u položek obrázky. Tyto obrázky musí být obsaženy v prvku ImageList, což je sada jednotlivých obrázků stejné velikosti. Každé položce pak přiřadíme index obrázku z tohoto ImageListu, který má být zobrazován v levé části položky. ImageList nastavíme prvku ComboBoxEx pomocí zprávy CBEM_SETIMAGELIST. Vše uvidíte v dalším výpisu kódu, v kterém si také ukážeme, jak lze nastavit prvku ComboBoxEx vlastní kurzor a dále jiný kurzor editačnímu prvku, obsaženém v ComboBoxu. V té souvislosti si musíme říci, z čeho se vlastně rozšířený ComboBoxEx skládá. Vlastní okno prvku ComboBoxEx totiž v sobě obsahuje ještě (jako dětské okno) běžný prvek ComboBox, a ten jak jsme mohli poznat v minulých dílech obsahuje prvek Edit, představující editační pole v případě, že ComboBox má nastaven potřebný styl CBS_DROPDOWN). Kromě tohoto Editu je pak při rozbalení ComboBoxu dynamicky tvořen prvek ListBox. Položky do prvku ComboBoxEx přidáváme pomocí zprávy CBEM_SETIMAGELIST, jejímž parametrem je adresa struktury COMBOBOXEXITEM, obsahující vlastnosti položky. Nyní se tedy již podívejme na výpis obsluhy zprávy WM_INITDIALOG:
BOOL NaInitDialog(HWND hWnd)
{
  HWND hCombo = CreateWindowEx(0,
    WC_COMBOBOXEX,
    NULL,
    WS_BORDER | WS_VISIBLE | WS_CHILD | CBS_DROPDOWN,
    0, 0,  0, 100, // polohu a rozměry nastvíme později
    hWnd,
    (HMENU)IDC_COMBOEX, // nastavíme ID pro snadnější použití
    GetModuleHandle(NULL),
    NULL);
  g_hImageList = ImageList_LoadBitmap(GetModuleHandle(NULL), MAKEINTRESOURCE(IDB_IMAGELIST),
    16, 3, 0x0000FF00);
  if ( !g_hImageList )
    return FALSE;
  SendMessage(hCombo, CBEM_SETIMAGELIST, 0, (LPARAM)g_hImageList);
  SetWindowPos(hCombo, NULL,10,32,250,120,SWP_NOACTIVATE | SWP_NOZORDER);
  COMBOBOXEXITEM cbei;
  // Naplníme ComboBox 4 položkami.
  ZeroMemory(&cbei, sizeof(cbei));
  cbei.mask = CBEIF_IMAGE | CBEIF_TEXT | CBEIF_SELECTEDIMAGE;
  cbei.pszText = _T("první položka");
  cbei.iImage = 0;
  cbei.iItem = 0;
  cbei.iSelectedImage = 0;
  SendMessage(hCombo, CBEM_INSERTITEM, 0, (LPARAM)&cbei);
  cbei.pszText = _T("druhá položka");
  cbei.iImage = 1;
  cbei.iItem = 1;
  cbei.iSelectedImage = 1;
  SendMessage(hCombo, CBEM_INSERTITEM, 0, (LPARAM)&cbei);
  cbei.mask = CBEIF_IMAGE | CBEIF_TEXT | CBEIF_SELECTEDIMAGE;
  cbei.pszText = _T("třetí položka");
  cbei.iImage = 2;
  cbei.iItem = 2;
  SendMessage(hCombo, CBEM_INSERTITEM, 0, (LPARAM)&cbei);
  cbei.mask = CBEIF_IMAGE | CBEIF_TEXT | CBEIF_SELECTEDIMAGE;
  cbei.pszText = _T("čtvrtá položka");
  cbei.iImage = 3;
  cbei.iItem = 3;
  cbei.iSelectedImage = 3;
  SendMessage(hCombo, CBEM_INSERTITEM, 0, (LPARAM)&cbei);
  // Nastavíme výchozí výběr
  SendMessage(hCombo, CB_SETCURSEL, 1, 0);
  
  // Zjistíme "dětský" ComboBox a prvek Edit
  HWND hChildCombo = (HWND)SendMessage(hCombo, CBEM_GETCOMBOCONTROL, 0,0);
  HWND hEdit = (HWND)SendMessage(hCombo, CBEM_GETEDITCONTROL, 0,0);
  // Nastavíme vlastní kurzory
  SetClassLongPtr(hChildCombo, GCLP_HCURSOR,
    (LONG_PTR)LoadCursor(GetModuleHandle(NULL), MAKEINTRESOURCE(IDC_CURSOR1)));
  SetClassLongPtr(hEdit, GCLP_HCURSOR,
    (LONG_PTR)LoadCursor(GetModuleHandle(NULL), MAKEINTRESOURCE(IDC_CURSOR2)));
  return TRUE;
}
Dále si ještě ukažme, jak v případě rozšířeného ComboBoxu reagovat na změnu výběru položky a následně získat a nějakým způsobem zobrazit některá data položky. Konkrétně budeme zobrazovat text vybrané položky do prvku Static a obrázek vybrané položky ve formě ikony v prvku Static typu STM_ICON - který v editoru dialogu vložíme jako Picture Control. V proceduře dialogu budeme reagovat na oznamovací zprávu CBN_SELCHANGE. Vypíšeme si pro úplnost kompletní kód procedury dialogu z doprovodného příkladu:
// Procedura dialogu
INT_PTR CALLBACK DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch ( uMsg )
  {
    case WM_COMMAND:
      switch ( LOWORD(wParam) )
      {
        case IDOK:
          EndDialog(hWnd, IDOK);
          break;
        case IDCANCEL:
          EndDialog(hWnd, IDCANCEL);
          break;
        case IDC_COMBOEX:
          if ( HIWORD(wParam) == CBN_SELCHANGE )
            NaSelChange(hWnd);
          break;
      }
      break;
      
    case WM_INITDIALOG:
      NaInitDialog(hWnd);
      NaSelChange(hWnd);
      break;
  }
  return FALSE;
}
Obsluha oznamovací zprávy CBN_SELCHANGE vypadá pak takto:
void NaSelChange(HWND hWnd)
{
  TCHAR szText[100];
  COMBOBOXEXITEM cbei;
  ZeroMemory(&cbei, sizeof(cbei));
  cbei.mask = CBEIF_IMAGE | CBEIF_TEXT;
  cbei.pszText = szText;
  cbei.cchTextMax = sizeof(szText);
  // index vybrané položky
  cbei.iItem = SendDlgItemMessage(hWnd, IDC_COMBOEX, CB_GETCURSEL, 0, 0);
  SendDlgItemMessage(hWnd, IDC_COMBOEX, CBEM_GETITEM,  0, (LPARAM)&cbei);
  SetDlgItemText(hWnd, IDC_COMBO_TEXT, szText);
  SendDlgItemMessage(hWnd, IDC_IMAGE, STM_SETIMAGE, IMAGE_ICON,
    (LPARAM)ImageList_GetIcon(g_hImageList, cbei.iImage, ILD_NORMAL));
}
Doprovodný projekt je ke stažení zde: win_api_comboboxex.zip

Ovládací prvek TrackBar.

V tomto článku se zaměříme na další ze standardních ovládacích prvků Windows, kterým je Trackbar Control.

win-api-trackbar

Aby byla aplikace používající prvek Trackbar funkční, musíme na jejím  začátku - přesněji řečeno před vytvořením okna (nebo více oken) prvku Trackbar použít funkci InitCommonControlsEx podobně jako u dalších prvků obsažených v knihovně comctl32.dll. S tím také souvisí nutnost přidat pro linkování do projektu odpovídající knihovnu comctl32.lib a vložit hlavičkový soubor commctrl.h.
// Běžné hlavičkové soubory 
#include <commctrl.h>	

// linkeru přidáme knihovnu přímo ve zdrojovém kódu
#pragma comment (lib, "comctl32.lib")
Ukázkový projekt je aplikace založená na hlavním okně dialogu. V editoru prostředků přidáme na dialog prvek Trackbar, který v okně Toolbox nalezneme pod poněkud matoucím označení Slider Control.

Nastavení vlastností v editoru prostředků

Přímo v editoru prostředků máme možnost nastavit různé vlastnosti (styly okna) Trackbaru. Řekněme si o významu některých z nich.
  • Orientation - určuje orientaci, která může být ve vodorovném nebo svislém směru. Vlastnost může nabývat hodnot Horizontal nebo Vertical, což odpovídá stylům okna (které použijeme při "ručním" vytvoření) TBS_HORZ (výchozí vlatsnost) a TBS_VERT.
  • Tick Marks - určuje zda je povoleno zobrazovat značky v bodech odpovídajících jednotlivým polohám nebo jejich násobkům (hustotu - krokování značek lze nastavit programově). Odpovídající stylu okna je TBS_NOTICKS, který značky zakazuje!
  • Auto Ticks - pokud je nastavena na True, značky jsou zobrazeny systémem automaticky.
  • Point - určuje zda se mají značky zobarzovat na obou stranách (hodnota Both) nebo pouze na levé/horní (podle orientace Trackbaru) - hodnota Top/Left či pravé/spodní - hodnota bottom/Right. Odpovídající styly okna jsou TBS_LEFT, TBS_TOP, TBS_RIGHT a TBS_BOTTOM. Nastavení zobrazování pouze na jedné straně také způsobí změnu vzhledu jezdce, který bude na příslušné straně zobrazen do špičky.
  • Tooltips - určuje zda se při tažení jezdce má zobrazovat aktuální poloha v okně tooltipu, vytvářeného a obsluhovaného systémem. Na obrázku jde o to žluté plovoucí okénko zobrazující polohu 23.

Programové ovládání Trackbaru

Jednou z prvních věcí která nás napadne, je nastavení rozsahu (v počtu logických jednotek - kroků) a dále třeba nastavení výchozí pozice. Následující funkce volaná v obsluze zprávy WM_INIDIALOG nastaví pomocí zpráv TBM_SETRANGE a TBM_SETPOS rozsah 30 logických jednotek a umístí jezdce do výchozí polohy 10.
void NastavRozsah(HWND hWnd)
{
  SendDlgItemMessage(hWnd, IDC_TRACKBAR, TBM_SETRANGE,
    TRUE, MAKELONG(0,30));
  SendDlgItemMessage(hWnd, IDC_TRACKBAR, TBM_SETPOS,
    TRUE, 10);
}
Pokud povolíme zobrazování značek, máme možnost kromě automatického zobrazení zobrazit vlastní značky v libovolných místech. Systém pak automaticky zobrazí značky počátku a konce. V našem příkladě takto nastavíme dvě vlastní značky na pozice 5 a 25 pomocí funkce TBM_SETTIC.
#define DOLNI_MEZ 5
#define HORNI_MEZ 25

void NastavZnacky(HWND hWnd)
{
  SendDlgItemMessage(hWnd, IDC_TRACKBAR, TBM_SETTIC, 0, (WPARAM)DOLNI_MEZ);
  SendDlgItemMessage(hWnd, IDC_TRACKBAR, TBM_SETTIC, 0, (WPARAM)HORNI_MEZ);
}
Mezi základní akce s Trackbarem dále patří programové zjištění aktuálně nastavené polohy a dále okamžitá detekce změny polohy. Touto změnou polohy je zde myšlena změna aktuální hodnoty logické souřadnice která většinou neodpovídá grafickému posunutí jezdce o každý pixel, ale dojde k ní v závislosti na poměru rozsahu a velikosti okna Trackbaru obvykle skokově při posunu o určitý počet pixelů. Logickou souřadnici získáme pomocí zprávy TBM_GETPOS, jak si ukážeme za chvíli v příkladu. Pokud jde o detekci změny souřadnice, musíme zachytávat zprávu WM_HSCROLL v případě horizontálního Trackbaru nebo WM_VSCROLL u svislého Trackbaru. Ukážeme si to na následujícím příkladě. Cílem bude detekovat změnu polohy a v případě že se uživatel dostane mimo rozsah dříve nastavených bodů (pozice 5 a 25) změníme ikonu na dialogu na systémovou ikonu "chyba" (IDI_ERROR). Při opětném návratu do "správných mezí" opět vrátíme ikonu na vlastní ikonu aplikace. Nastavení ikony provedeme pomocí zprávy STM_SETIMAGE prvku static, což je v editoru prostředků prvek Picture Control typu Icon. Navíc při pohybu v zakázané oblasti přehrajeme varovný zvuk - zde pro jednoduchost pomocí funkce MessageBeep. Takto bude tedy vypadat funkce realizující uvedenou kontrolu mezí a procedura dialogu, ze které jsou také volány ostatní výše uvedené funkce.
// Obsluha zprávy WM_HSCROLL
void ZmenaPolohy(HWND hWnd)
{
  LRESULT pozice =
    SendDlgItemMessage(hWnd, IDC_TRACKBAR, TBM_GETPOS, 0, 0);
  SetDlgItemInt(hWnd, IDC_POLOHA, pozice, FALSE);
  if ( pozice < DOLNI_MEZ || pozice > HORNI_MEZ )
  {
    if ( g_OK )
    {
      g_OK = FALSE;
      SendDlgItemMessage(hWnd, IDC_IKONA, STM_SETIMAGE,
        IMAGE_ICON, (LPARAM)LoadIcon(NULL, IDI_ERROR));
    }
    MessageBeep(MB_ICONERROR);
  }
  else
  {
    if ( !g_OK )
    {
      g_OK = TRUE;
      SendDlgItemMessage(hWnd, IDC_IKONA, STM_SETIMAGE, IMAGE_ICON,
        (LPARAM)LoadIcon(GetModuleHandle(0), MAKEINTRESOURCE(IDI_HLAVNI)));
    }
  }
}

/////////////////////////////////////////////////////////////////////////////
// Procedura dialogu

INT_PTR CALLBACK DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch ( uMsg )
  {
    case WM_COMMAND:
      switch ( LOWORD(wParam) )
      {
        case IDOK:
          EndDialog(hWnd, IDOK);
          break;
        case IDCANCEL:
          EndDialog(hWnd, IDCANCEL);
          break;
      }
      break;
      
    case WM_INITDIALOG:
      NastavRozsah(hWnd);
      NastavZnacky(hWnd);
      ZmenaPolohy(hWnd);
      break;
    case WM_HSCROLL:
      ZmenaPolohy(hWnd);
      break;
  }
  return FALSE;
}
Doprovodný projekt je ke stažení zde: win_api_trackbar.zip

Ovládací prvek ProgressBar.

V tomto článku se podíváme na práci s ovládacím prvkem Progress Bar Control.

win-api-progressbar

Ovládací prvek Progress Bar Control se běžně používá jako indikátor průběhu nějaké obvykle  déletrvající akce. K běžnému uživatelskému folklóru sice patří vtipkování na téma "plynulosti" tohoto zobrazení při kopírování velkého množství souborů nebo různých instalacích. Zde však není na vině Progress Bar, ale obtížnost nebo praktické nemožnost správného algoritmu výpočtu aktuálního řekněme procenta prováděné akce.

K základním úkonům, které se musíme naučit s tímto prvkem provádět, patří následující akce a jim odpovídající zprávy Windows:
  • nastavení logického rozsahu - PBM_SETRANGE32
  • nastavení aktuální logické pozice - PBM_SETPOS
  • zjištění aktuální logické pozice - PBM_GETPOS
V ukázkovém projektu máme ukazatel průběh (Progress Bar). Na stisk tlačítka nastavíme logický rozsah na 20. Znamená to, že indikátor průběhu bude moci nabývat dvacet různých logických pozic. Spustíme časovač (Timer), na který budeme každou sekundu zvětšovat logickou pozici Progress Baru a při zjištění konce zastavíme časovač a pozici vrátíme na počátek (hodnota 0). Spouštění průběhu budeme volat v obsluze příkazu tlačítka IDC_SPUSTIT (zpráva WM_COMMAND, jak čtenáři seriálu již určitě vědí):
void Spustit(HWND hWnd)
{
  // Nastavíme rozsah
  SendDlgItemMessage(hWnd, IDC_PROGRESSBAR, PBM_SETRANGE32, 0,20);
  // Nastavíme (pro pořádek:-)) pozici na počátek
  SendDlgItemMessage(hWnd, IDC_PROGRESSBAR, PBM_SETPOS, 0,0);
  SetTimer(hWnd, 1, 1000, NULL);
}
V obsluze zprávy WM_TIMER budeme pak volat následující funkci, která zvýší pozici o 1 logickou jednotku a bude hlídat dosažení konce (pozice 20):
void NaTimer(HWND hWnd)
{
  MessageBeep(0); // zvuková signalizace
  // Zjistíme aktuální log. pozici
  LRESULT Pozice = SendDlgItemMessage(hWnd, IDC_PROGRESSBAR, PBM_GETPOS, 0,0);
  if ( Pozice < 20 )
  {
    Pozice++;
    // Nastavíme novou pozici
    SendDlgItemMessage(hWnd, IDC_PROGRESSBAR, PBM_SETPOS, Pozice,0);
  }
  else
  {
    KillTimer(hWnd, 1);
    // Vrátíme se na začátek
    SendDlgItemMessage(hWnd, IDC_PROGRESSBAR, PBM_SETPOS, 0,0);
  }
}
Dále si ukážeme, jak lze "animovat" prvek Progress Bar ve Windows XP. Abychom mohli  následující kód přeložit, musíme nastavit verzi cílové platformy na Windows XP a mít k dispozici dostatečně aktuální verzi Windows SDK. Pokud vám nepůjde z uvedeného důvodu přeložit doprovodný projekt, musíte kód který si nyní ukážeme zapoznámkovat nebo odstranit. Před vložením hlavičkových souborů nastavíme cílovou verzi Windows.
#define WINVER 0x0501
#define _WIN32_WINNT 0x0501

#include <windows.h>
// ....
Samozřejmě že tento kód můžete přeložit a sestavit na jakékoli platformě, ale vlastní spuštění musíte provést pouze ve Windows XP (nebo vyšších...). Animaci budeme spouštět a zastavovat na změnu zaškrtnutí Check Boxu (IDC_ANIMOVAT), kterou zachytíme v proceduře dialogu, kterou si nyní ukažme celou i s dříve uvedenými obsluhami zpráv.
// Procedura dialogu
INT_PTR CALLBACK DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch ( uMsg )
  {
    case WM_COMMAND:
      switch ( LOWORD(wParam) )
      {
        case IDOK:
          EndDialog(hWnd, IDOK);
          break;
        case IDCANCEL:
          EndDialog(hWnd, IDCANCEL);
          break;
        case IDC_SPUSTIT:
          Spustit(hWnd);
          break;
        case IDC_ANIMOVAT:
          if ( IsDlgButtonChecked(hWnd, IDC_ANIMOVAT) )
            SpustitAnimaci(hWnd);
          else
            ZastavitAnimaci(hWnd);
          break;
      }
      break;
      
    case WM_INITDIALOG:
      SendMessage(hWnd, WM_SETICON, ICON_SMALL | ICON_BIG,
        (LPARAM)LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_HLAVNI)));
      break;
    case WM_TIMER:
      NaTimer(hWnd);
      break;
  }
  return FALSE;
}
Funkce pro spuštění a zastavení animace vypadají takto:
// Spustí animaci ve Widnows XP
void SpustitAnimaci(HWND hWnd)
{
  // Nejdříve musíme přidat styl PBS_MARQUEE
  SetWindowLongPtr(GetDlgItem(hWnd, IDC_PROGRESSBAR), GWL_STYLE,
    GetWindowLongPtr(GetDlgItem(hWnd, IDC_PROGRESSBAR), GWL_STYLE) | PBS_MARQUEE);
  SendDlgItemMessage(hWnd, IDC_PROGRESSBAR, PBM_SETMARQUEE, TRUE, 100);
}

/////////////////////////////////////////////////////////////////////////////
// Zastaví animaci
void ZastavitAnimaci(HWND hWnd)
{
  SendDlgItemMessage(hWnd, IDC_PROGRESSBAR, PBM_SETMARQUEE, FALSE, 0);
  DWORD dwStyl = (DWORD)
    GetWindowLongPtr(GetDlgItem(hWnd, IDC_PROGRESSBAR), GWL_STYLE);
  dwStyl &=~ PBS_MARQUEE;
  SetWindowLongPtr(GetDlgItem(hWnd, IDC_PROGRESSBAR), GWL_STYLE, dwStyl);
}
Jak vidíte v kódu, před spuštěním animace musíme oknu prvku Progress Bar přidat styl PBM_SETMARQUEE, který po zastavení animace opět odebereme - vše pomocí funkce SetWindowLongPtr. Doprovodný projekt je ke stažení zde: win_api_progressbar.zip

Copyright © Radek Chalupa, 2018