Programování pro Windows v C++ – úvod
 x   TIP: Přetáhni ikonu na hlavní panel pro připnutí webu

Programování pro Windows v C++ – úvodProgramování pro Windows v C++ – úvod

 

Programování pro Windows v C++ – úvod

Google       Google       18. 6. 2014       26 492×

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ředtí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 níž 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;
}

V dalších pokračováních si vytvoříme další třídy, které nám výrazně zjednoduší programování ve WinAPI 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.

Zde je ukázkový projekt (ve Visual Studiu 2008 Professional) a  kód knihovny.

×Odeslání článku na tvůj Kindle

Zadej svůj Kindle e-mail a my ti pošleme článek na tvůj Kindle.
Musíš mít povolený příjem obsahu do svého Kindle z naší e-mailové adresy kindle@programujte.com.

E-mailová adresa (např. novak@kindle.com):

TIP: Pokud chceš dostávat naše články každé ráno do svého Kindle, koukni do sekce Články do Kindle.

3 názory  —  3 nové  
Hlasování bylo ukončeno    
15 hlasů
Google
(fotka) Radek ChalupaRadek je na volné noze jako vývojář software, konzultant a lektor v oblasti vývoje software.
Web     Twitter    

Nové články

Obrázek ke článku Hybridní inteligentní systémy 2

Hybridní inteligentní systémy 2

V technické praxi využíváme často kombinaci různých disciplín umělé inteligence a klasických výpočtů. Takovým systémům říkáme hybridní systémy. V tomto článku se zmíním o určitém typu hybridního systému, který je užitečný ve velmi složitých výrobních procesech.

Obrázek ke článku Jak vést kvalitně tým v IT oboru: Naprogramujte si ty správné manažerské kvality

Jak vést kvalitně tým v IT oboru: Naprogramujte si ty správné manažerské kvality

Vedení týmu v oboru informačních technologií se nijak zvlášť neliší od jiných oborů. Přesto však IT manažeři čelí výzvě v podobě velmi rychlého rozvoje a tím i rostoucími nároky na své lidi. Udržet pozornost, motivaci a efektivitu týmu vyžaduje opravdu pevné manažerské základy a zároveň otevřenost a flexibilitu pro stále nové výzvy.

Obrázek ke článku Síla týmů se na home office může vytrácet. Odborníci radí, jak z pracovních omezení vytěžit maximum

Síla týmů se na home office může vytrácet. Odborníci radí, jak z pracovních omezení vytěžit maximum

Za poslední rok se podoba práce zaměstnanců změnila k nepoznání. Především plošné zavedení home office, které mělo být zpočátku jen dočasným opatřením, je pro mnohé už více než rok každodenní realitou. Co ale dělat, když se při práci z domova ztrácí motivace, zaměstnanci přestávají komunikovat a dříve fungující tým se rozpadá na skupinu solitérů? Odborníci na personalistiku dali dohromady několik rad, jak udržet tým v chodu, i když pracovní podmínky nejsou ideální.

Hostujeme u Českého hostingu       ISSN 1801-1586       ⇡ Nahoru Webtea.cz logo © 20032024 Programujte.com
Zasadilo a pěstuje Webtea.cz, šéfredaktor Lukáš Churý