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

Jak používat program Záznam zvuku

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

Win32 aplikace s využitím ATL

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

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

Buffering v GDI

ATL jako alternativa MFC

Ovládací prvek Progress Bar

Ovládací prvek Trackbar

Rozšířený prvek ComboBoxEx

Pracujeme s ComboBoxem II.

Pracujeme s ComboBoxem I.

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

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

Uživatelsky kreslený ListBox II.

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