Co je nového?

 · Nová verze webu

 · Aktualizace aplikace "Net Rádio" (2.1.0.1).

 · Nová verze aplikace "Ovládání CD" (3.0.0.1).

Programování v C++ pro Windows - okna v třídě

Ukážeme si, že psát aplikace pro Windows bez použití hotových knihoven či frameworků nemusí být tak frustrující, jak se zdá. V tomto a návazných článcích si ukážeme, jak efektivně vytvářet rychlé a relativně nenáročné aplikace v C++ a Win API.

Pár řádků na úvod

Těm, kteří čtou tento článek, je asi zbytečné připomínat sílu a výkon jazyka C/C++ (samozřejmě při vývoji nativních aplikací). V následujícím seriálu se přesvědčíme o tom, že ani pro vývoj standardních aplikací pro Windows s „okenním” uživatelským rozhraním není v mnoha případech třeba snižovat tento výkon a zvyšovat nároky aplikace používáním rozsáhlých knihoven (nadstaveb nad Win API), jako jsou například MFC, VCL nebo .NET Framework. O těch multiplatformních ani nemluvě, ty jsou z hlediska nároků na prostředky a výkonu kapitolou samy pro sebe.

Na úvod předesílám, že půjde o programování aplikací výhradně pro Windows, takže ve zdrojovém kódu nehodlám řešit otázky „přenositelnosti“ a dále budu využívat některá „Microsoft specifika“, například pohodlnou deklaraci (a současně definici) globálních proměnných pomocí __declspec(selectany).

Pokud jde o ukázku jakési knihovny, kterou si postupně vytvoříme, bude kompletně v hlavičkovém souboru (tento přístup používá například knihovna ATL).

Co by měl čtenář znát

Pro pochopení dále uvedeného by měl čtenář tohoto a budoucích návazných článků mít alespoň základní znalosti jazyka C++ a základních principů programování ve Win API (zejména co jsou to zprávy Windows, smyčka zpráv, procedura okna). Na téma „výuky“ Win API jsem před pár lety napsal sérii článků Učíme se Win API, popř. zde je  výpis celého seriálu v jednom dokumentu.

Vytvoření kostry aplikace ve Win API

V tomto úvodním článku si vytvoříme základ aplikace s jedním oknem, přičemž ta „frustrující“ část kódu (C++ a WinAPI) bude ve výše zmíněné knihovně. Odpovíme také na (dost často v diskusích kladenou) otázku, zda a jak je možné mít proceduru okna jako členskou funkci třídy a jak obsloužit více instancí této třídy (zapouzdřující okno Windows - tj. handle typu HWND).

Aplikaci založíme (v MS Visual Studiu) jako Win32 Windows aplikaci (pozor, nikoliv konzolovou). Protože chceme začínat s čistým štítem, odstraníme (v Solution exploreru) z projektu všechny soubory kromě stdafx.h, stdafx.cpp, targetver.h a souboru nazev_pojektu.cpp (ve kterém je vstupní bod aplikace, tj. funkce WinMain). V tomto souboru smažeme veškerý wizardem vygenerovaný zdrojový kód a ponecháme jen následující:

#include "stdafx.h"
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, LPTSTR, int) throw()
{
	return 0;
}

Dále si založíme základ knihovny. Do projektu přidáme nový hlavičkový soubor (já jsem ho nazval winapi.h), který umístíme nejlépe do nějaké složky vedle složky s projektem. Do tohoto souboru si vložíme hlavičkové soubory z Windows SDK a také přímo ve zdrojovém kódu přidáme do projektu příslušné statické knihovny (lib). Většinu dále uvedených hlaviček nebudeme zatím potřebovat, avšak připravíme si je pro další projekty.

#include <windows.h>
#include <comdef.h>
#include <commctrl.h>
#include <gdiplus.h>
#include <shlobj.h>
#include <strsafe.h>
#include <shlwapi.h>
#include <uxtheme.h>
#include <vssym32.h>
#include <process.h>
#include <time.h>
#include <lm.h>

#pragma warning(push)
#pragma warning(disable: 4995)
#pragma warning(disable: 4996)
#include <string>
#include <algorithm>
#include <vector>
#pragma warning(pop)

#pragma comment (lib, "comctl32.lib")
#pragma comment (lib, "gdiplus.lib")
#pragma comment (lib, "shlwapi.lib")
#pragma comment (lib, "UxTheme.lib")
#pragma comment (lib, "version.lib")
#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Wininet.lib

Tento hlavičkový soubor pak vložíme do souboru stdafx.h s následující strukturou:

#pragma once

#include "targetver.h"

#include "..\\knihovna\\winapi.h"
using namespace winapi;

#ifdef _UNICODE
#if defined _M_IX86
#pragma comment(linker,"/manifestdependency:\"type='win32'
	name='Microsoft.Windows.Common-Controls' version='6.0.0.0'
	processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"")
#elif defined _M_IA64
#pragma comment(linker,"/manifestdependency:\"type='win32'
	name='Microsoft.Windows.Common-Controls' version='6.0.0.0'
	processorArchitecture='ia64' publicKeyToken='6595b64144ccf1df' language='*'\"")
#elif defined _M_X64
#pragma comment(linker,"/manifestdependency:\"type='win32'
	name='Microsoft.Windows.Common-Controls' version='6.0.0.0'
	processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"")
#else
#pragma comment(linker,"/manifestdependency:\"type='win32'
	name='Microsoft.Windows.Common-Controls' version='6.0.0.0'
	processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
#endif
#endif

Základ třídy zapouzdřující okno

V naší „knihovně“ (hlavičkovém souboru winapi.h) si vytvoříme základ třídy zapouzdřující okno Windows, tj. handle typu HWND. Prozatím bude obsahovat pouze minimum členských funkcí, které nás oprostí od nutnosti rozepisovat opakující se či nevyužité parametry volání WinAPI funkcí a také v DEBUG režimu pomocí _ASSERT pomohou zachytit případné chyby.

####################################

Ještě před tím si do knihovny přidáme globální funkci (resp. její prozatím 2 přetížené varianty), kterou budeme volat v případě výskytu takové chyby, při které je záhodno upozornit uživatele chybovou hláškou a ukončit běžící aplikaci, např. z důvodu možné kumulace neuvolněných objektů či paměti.

Také si přímo do knihovny přidáme globální proměnnou typu HINSTANCE, ve které si budeme po celou dobu běhu udržovat handle instance (tj. 1. parametr funkce WinMain), které budeme často v kódu potřebovat, například při načítání dat z prostředků (resource).

Úvodní část knihovny bude prozatím vypadat následovně:

__declspec(selectany) HINSTANCE _hinstance = NULL;

#pragma region globalni_funkce

// Volaná při kritické chybě, ukončí běžící exe aplikaci
__declspec(noinline) inline void __declspec(noreturn) _kriticka_chyba(const wchar_t* sz_text) throw()
{
	FatalAppExitW(0, sz_text);
}


// Volaná při kritické chybě, ukončí běžící exe aplikaci
__declspec(noinline) inline void __declspec(noreturn) _kriticka_chyba() throw()
{
	HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
	if (SUCCEEDED(hr))
		hr = E_UNEXPECTED;
	_kriticka_chyba(_com_error(hr).ErrorMessage());
}
#pragma endregion globalni_funkce

takto vypadá kód třídy okna:

#pragma region okno
//-------------------------------------------------------------------------------------------------
//	class okno
//-------------------------------------------------------------------------------------------------

class okno
{
protected:
	HWND m_hwnd;
	bool m_b_vytvorene_okno;

public:
	okno(HWND hwnd = NULL) throw() :
		m_b_vytvorene_okno(false),
		m_hwnd(NULL)
	{
		if (hwnd != NULL)
			nastavit(hwnd);
	}

public:
	~okno() throw()
	{
		uvolnit();
	}

public:
	const HWND hwnd() const throw()
	{
		return m_hwnd;
	}

public:
	operator HWND() const throw()
	{ 
		return m_hwnd; 
	}

public:
	void uvolnit() throw()
	{
		if (m_hwnd)
		{
			if (m_b_vytvorene_okno)
				if (IsWindow(m_hwnd))
					if (!DestroyWindow(m_hwnd))
						_kriticka_chyba();
			m_hwnd = NULL;
		}
		m_b_vytvorene_okno = false;
	}

public:
	void zrusit_okno() throw()
	{
		if (IsWindow(m_hwnd))
			if (!DestroyWindow(m_hwnd))
				_kriticka_chyba();
		m_b_vytvorene_okno = false;
	}

public:
	// V případě neplatného parametru vyvolá kritickou chybu
	void nastavit(HWND hwnd) throw()
	{
		_ASSERTE(IsWindow(hwnd));
		if (!IsWindow(hwnd))
			_kriticka_chyba();
		uvolnit();
		m_hwnd = hwnd;
		m_b_vytvorene_okno = false;
	}

public:
	__forceinline LRESULT send_message(UINT zprava, WPARAM wparam = 0, LPARAM lparam = 0) throw()
	{
		_ASSERTE(IsWindow(m_hwnd));
		return SendMessageW(m_hwnd, zprava, wparam, lparam);
	}

public:
	__forceinline BOOL post_message(UINT zprava, WPARAM wparam = 0, LPARAM lparam = 0) throw()
	{
		_ASSERTE(IsWindow(m_hwnd));
		return PostMessageW(m_hwnd, zprava, wparam, lparam);
	}

public:
	void zobrazit(int cmd_show = SW_SHOW) throw()
	{
		_ASSERTE(IsWindow(m_hwnd));
		ShowWindow(m_hwnd, cmd_show);
	}
};	// class okno
#pragma endregion okno

Procedura okna jako členská funkce třídy C++

Velice častou otázkou na diskusních fórech bývá, jak mít proceduru okna jako členskou funkci třídy C++, která zapouzdřuje handle okna (HWND). Problém je v tom, že adresa této funkce (tzv. procedury okna) musí být známa již v době sestavení programu, neboť ji zadáváme při registraci třídy okna jako prvek lpfnWndProc struktury WNDCLASSEX.

Protože adresu "normální" členské funkce třídy kompilátor znát nemůže, znamená to že procedurou okna může být buď globální funkce (tj. mimo jakoukoliv třídu) nebo statická funkce třídy C++.

Pokud tedy použijeme jednu z uvedených možností (v našem případě statickou funkci třídy, kterou si podědíme od výše uvedené třídy okno) vyvstane otázka jak obsloužit více současně existujících oken, tj. instancí této třídy. Možných řešení je více a různé knihovny (ATL, MFC, VCL to řeší po svém).

Já osobně využívám způsobu uložení ukazatele na konkrétní instanci třídy do tzv. uživatelských dat okna, což je poslední parametr funkce CreateWindowEx, která okno vytvoří.

Do naší knihovny si tedy přidáme třídu "okno_impl" odvozenou od třídy okno, která bude implementovat proceduru okna a zajistí volání (virtuální) funkce window_proc, ve které budeme mít obsluhu zpráv Windows. Celý kód této třídy vypadá následovně:

#pragma region okno_impl
//-------------------------------------------------------------------------------------------------
// class okno_impl
//-------------------------------------------------------------------------------------------------
// Při vytvoření pomocí CreateWindowEx je nutné nastavit jako poslední parametr this!
//-------------------------------------------------------------------------------------------------

class okno_impl : public okno
{
protected:
	virtual const wchar_t* trida() const throw() = 0;

protected:
	static LRESULT CALLBACK okno_impl_window_proc(HWND hwnd, UINT zprava, WPARAM wparam, LPARAM lparam) throw()
	{
		okno_impl* p_objekt = (okno_impl*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
		if (zprava == WM_CREATE)
			SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)((LPCREATESTRUCTW)lparam)->lpCreateParams);
		if (p_objekt)
			return p_objekt->window_proc(zprava, wparam, lparam);
		else
			return DefWindowProc(hwnd, zprava, wparam, lparam);
	}

public:
	// Vrátí true pokud byla neexistující třída nově zaregistrovaná
	// Pokud už byla zaregistrovaná před voláním funkce, neudělá nic a vrátí false
	// Při neúspěchu vyvolá kritickou chybu -> havarijní ikončení aplikace
	bool zaregistrovat_tridu() throw()
	{
		_ASSERTE(trida() != NULL);
		WNDCLASSEX wc;
		memset(&wc, 0, sizeof(wc));
		wc.cbSize = sizeof(wc);
		if (GetClassInfoEx(_hinstance, trida(), &wc))
			return false;
		memset(&wc, 0, sizeof(wc));
		wc.cbSize = sizeof(WNDCLASSEX);
		wc.style = CS_HREDRAW | CS_VREDRAW;
		wc.lpfnWndProc = okno_impl_window_proc;
		wc.hInstance = _hinstance;
		wc.hIcon = NULL;
		wc.hCursor = LoadCursor(NULL, IDC_ARROW);
		wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
		wc.lpszClassName = trida();
		wc.hIconSm = NULL;
		if (!RegisterClassEx(&wc))
			_kriticka_chyba();
		return true;
	}

public:
	// Vrátí true pokud byla existující třída odregistrovaná
	// Pokud třída nebyla zaregistrovaná před voláním funkce, neudělá nic a vrátí false
	bool odregistrovat_tridu() throw()
	{
		_ASSERTE(trida() != NULL);
		WNDCLASSEX wc;
		memset(&wc, 0, sizeof(wc));
		wc.cbSize = sizeof(wc);
		if (!GetClassInfoEx(_hinstance, trida(), &wc))
			return false;
		if (!UnregisterClassW(trida(), _hinstance))
			_kriticka_chyba();
		return true;
	}

public:
	virtual void vytvorit() throw()
	{
		_ASSERTE(m_hwnd == NULL);
		_ASSERTE(trida() != NULL);
		zaregistrovat_tridu();
		m_hwnd = CreateWindowEx(0, trida(), L"Implementace okna", WS_OVERLAPPEDWINDOW,
			150, 120, 640, 480, NULL, NULL, _hinstance, this);
		if (m_hwnd == NULL)
			_kriticka_chyba();
		zobrazit();
	}

protected:
	virtual LRESULT window_proc(UINT zprava, WPARAM wparam, LPARAM lparam) throw()
	{
		return DefWindowProc(m_hwnd, zprava, wparam, lparam);
	}
}; // class okno_impl
#pragma endregion okno_impl

Když máme toto vše připravené, můžeme snadno vytvořit základní aplikaci s jedním oknem. V kódu aplikace si vytvoříme třídu odvozenou od třídy okno_impl, jednu její instanci jako globální proměnnou. Pak stačí jen přepsat virtuální funkci window_proc ve které musíme (protože jde o hlavní okno aplikace) obsloužit zprávu WM_DESTROY, při které musíme zajistit aby po zrušení hlavního okna byla ukončena smyčka zpráv a tím celá aplikace.

S využitím naší knihovny je pak celý kód aplikace velmi jednoduchý - pro zjednodušení jsem v případě takto jednoduchého kódu vše umístil do jediného zdrojového souboru, ve kterém je vstupní funkce WinMain:

#include "stdafx.h"

class okno_hlavni : public winapi::okno_impl
{
private:
	const wchar_t* trida() const throw()
	{
		return L"moje_okno";
	}

private:
	LRESULT window_proc(UINT zprava, WPARAM wparam, LPARAM lparam) throw()
	{
		switch (zprava)
		{
		case WM_DESTROY:
			PostQuitMessage(0);
			break;
		}
		return DefWindowProc(m_hwnd, zprava, wparam, lparam);
	}
};

__declspec(selectany) okno_hlavni _okno_hlavni;


int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, LPTSTR, int) throw()
{
	MSG msg;
	_hinstance = hInstance;
	_okno_hlavni.vytvorit();
	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return (int)msg.wParam;
}

To je prozatím vše a v dalších pokračováních si vytvoříme další třídy které nám výrazně zjednoduší programování ve Win API a také se dostaneme k některým technikám, na které se programátoři ptají v různých diskusních fórech.

Další články

Nová verze webových stránek

4.5.2014 byla spuštěna nová verze těchto stránek.  (5.5.2014)

Jak používat program Záznam zvuku

Program Záznam zvuku umožňuje nahrávat zvuk který právě přehráváte v reproduktorech. Zde zjistíte jak na to.  (1.4.2014)

Vlastní prvky v .NET: Možná nestandardní ale jednoduchý postup.

Ukázka jednoduchého ale fungujícího postupu jak nastavit TextBoxu vlastnost 'vstup pouze čísel'  (11.3.2006)

Win32 aplikace s využitím ATL

Win32 aplikace s využitím ATL aneb jak jednoduché je použití ActiveX prvku.  (22.9.2005)

Odpověď na 3 otázky z diskusního fóra v praktické ukázce

Ukážeme si na malé "předsilvestrovské ptákovině" praktickou realizaci odpovědí na 3 občas kladené otázky na programátorských diskusních fórech  (27.12.2004)

Stručný úvod do bitových operací - masky, příznaky a posun

Krátký článek určený především těm, kteří při studiu C++ nějak "přeskočili" základy probírané při výkladu jazyka C  (28.7.2004)

Buffering v GDI

Odpověď na mnoho různých dotazů (zejména na programátorských fórech) týkajících se kreslení a překreslování okna (pomocí standardního GDI) směřuje k použití paměťového kontextu zařízení. Ukážeme si velice jednoduchý příklad, jak na to.  (20.4.2004)

ATL jako alternativa MFC

I když knihovna ATL (Active Template Library) byla primárně navržena pro zjednodušení vytváření objektů COM, obsahuje také několik tříd urychlujících a zjednodušujících vývoj "běžných" aplikací typu win32 (exe). Na této knihovně je dále postavena knihovna WTL (Windows Template Library) také z dílny Microsoftu, která mimo jiné obsahuje další třídy silně zjednodušující vytváření uživatelského rozhraní podobně jako knihovna MFC.  (30.7.2003)

Ovládací prvek Progress Bar

V tomto článku se podíváme na práci s ovládacím prvkem Progress Bar Control, neboli česky indikátor průběhu.  (22.3.2003)

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.  (28.2.2003)

Rozšířený prvek ComboBoxEx

Řekneme si o rozšířeném prvku ComboBoxEx, který patří stejně jako ComboBox mezi standardní ovládací prvky Windows, ale má některé další možnosti.  (14.2.2003)

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í.  (5.2.2003)

Pracujeme s ComboBoxem I.

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.  (16.1.2003)

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

Ukážeme si 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.  (8.1.2003)

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

Tentokrát 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.  (21.12.2002)

 © 2014 Radek Chalupa || tel. 739 219 991 | Kontakt | Twitter | Poslat e-mail || Radek Chalupa