Radek Chalupa   konzultace a školení programování, vývoj software na zakázku

Buffering v GDI (Win API)

1.1.2016

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.

Řekněme že chceme kreslit myší (při stisknutém levém tlačítku) do okna jednoduchou čáru. Výsledný program musí splňovat 2 podmínky:

  • čára (chcete-li) stopa po myši se musí objevit v okně hned po akci uživatele (samozřejmě zanedbáváme zpoždění v řádu zlomků sekund) způsobené fyzickou rychlostí hardware počítače:-))
  • veškerý nakreslený obsah musí v okně zůstat i po jeho částečném nebo úplném překreslení vyvolaném například jeho minimalizací a následnou obnovou

Ukázkový projekt je pro urychlení vygenerován jako Win32 aplikace ve Visual Studiu .NET 2003. První úpravou (a dalším zjednodušením problému:-)) je změna stylu okna z WS_OVERLAPED na kombinaci stylů WS_OVERLAPPED | WS_SYSMENU | WS_MINIMIZEBOX, čímž zabráníme změně rozměrů okna a zjednodušíme si náš úkol (jak uvidíme dále) - jde zde o vysvětlení nejjednoduššího principu, další "sofistikaci" již nechám na čtenáři popřípadě na další článek.

hWnd = CreateWindow(szWindowClass,
szTitle,
WS_OVERLAPPED | WS_SYSMENU | WS_MINIMIZEBOX,
200, 180,
640, 480,
NULL, NULL, hInstance, NULL);
Dále si vytvoříme dvě globální proměnné: pro "paměťový" kontext zařízení a  paměťovou" bitmapu:
// Globální proměnné pro buffering
HDC g_hdcMem = NULL;
HBITMAP g_hBitmapMem = NULL;

Při vytvoření okna, tj. v obsluze zprávy WM_CREATE tyto proměnné inicializujeme. Obslužná funkce zprávy WM_CREATE bude vypadat takto:

// Obsluha zprávy WM_CREATE
void OnCreate(HWND hWnd)
{
RECT rect;
HDC hdcScreen = GetDC(NULL);
GetClientRect(hWnd, &rect);
g_hdcMem = CreateCompatibleDC(hdcScreen);
g_hBitmapMem = CreateCompatibleBitmap(hdcScreen, rect.right, rect.bottom);
SelectObject(g_hdcMem, g_hBitmapMem);
FillRect(g_hdcMem, &rect, GetSysColorBrush(COLOR_WINDOW));
ReleaseDC(NULL, hdcScreen);
}

O co zde jde? Nejprve musíme vytvořit tzv. "paměťový" kontext zařízení (HDC). K tomu použijeme funkci CreateCompatibleDC, která jako parametr vyžaduje handle nějakého existujícího kontextu, na základě jehož vlastností je nový kontext zařízení vytvořen. Proto si "vypůjčíme" například kontext celé obrazovky, který získáme pomocí funkce GetDC s parametrem NULL. Stejně tak bychom samozřejmě mohli zadat handle existujícího okna. Dále potřebujeme vytvořit bitmapu (HBITMAP) kterou vybereme do našeho kontextu. Tuto bitmapu vytvoříme pomocí funkce CreateCompatibleBitmap, která ve svých parametrech vyžaduje kromě platného handle kontextu zařízení rozměry této bitmapy. Z pochopitelných důvodů vytvoříme tuto bitmapu o velikosti odpovídající klientské (a tedy námi překreslované) oblasti okna. Tu získáme pomocí funkce GetClientRect. Po vybrání bitmapy do kontextu zařízení už můžeme do kontextu začít kreslit. Můžeme si jej například vyplnit nějakou výchozí barvou - v našem případě jednoduše použijeme barvu pozadí okna - máme k disposici odpovídající systémový štětec který získáme pomocí funkce GetSysColorBrush. Když máme vytvořený paměťový kontext zařízení, bude dalším krokem vytvoření obsluhy zprávy WM_PAINT, která přijde při požadavku na překreslení okna nebo jeho části. Vzhledem k tomu že (jak uvidíme dále) budeme veškeré naše kreslení do okna realizovat kreslením do našeho paměťového kontextu zařízení, budeme v obsluze zprávy WM_PAINT pouze kopírovat náš paměťový kontext do kontextu zařízení okna. Kód obslužné funkce bude tedy jednoduchý:

// Obsluha zprávy WM_PAINT
void OnPaint(HWND hWnd)
{
HDC hDC;
RECT rect;
PAINTSTRUCT ps;
GetClientRect(hWnd, &rect); hDC = BeginPaint(hWnd, &ps); BitBlt(hDC, 0,0, rect.right, rect.bottom, g_hdcMem, 0,0, SRCCOPY); EndPaint(hWnd, &ps); }

Nyní se podíváme na vlastní "kreslení" do okna. Budeme ho realizovat tím nejjednodušším způsobem. Při detekci pohybu myši (zachycení zprávy WM_MOUSEMOVE budeme s využitím parametru wParam testovat, zda je stisknuté levé tlačítko myši a v tom případě nakreslíme "čáru" do nového cílového bodu, zjištěného z parametru lParam zprávy WM_MOUSEMOVE. Poku není levé tlačítko stisknuté, přemístíme do stejného bodu aktuální pozici v kontextu zařízení (která pak bude výchozím bodem pro další kreslení). Funkce vypadá následovně:

// Obsluha zprávy WM_MOUSEMOVE
void OnMouseMove(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
if ( wParam & MK_LBUTTON )
LineTo(g_hdcMem, LOWORD(lParam), HIWORD(lParam));
else
MoveToEx(g_hdcMem, LOWORD(lParam), HIWORD(lParam), NULL);
RedrawWindow(hWnd, NULL, NULL, RDW_UPDATENOW | RDW_INVALIDATE);
}

A to je skoro vše. Zbývá ještě jeden krok, který sice není nutný, ale v mnohých případech výrazně zlepší překreslování okna. Pokud totiž v obsluze WM_PAINT překreslujeme celé okno (přesněji řečeno jeho klientskou oblast) je zcela zbytečné nechávat před tím systém tuto oblast "překreslit" štětcem pozadí, jak to dělá výchozí obsluha zprávy WM_ERASEBKGND. Zamezíme tím nežádoucímu problikávání, které byv našem případě nebylo tak výrazné, neboť naše okno nelze roztahovat a tudíž nedochází k jeho rychle se opakujícímu překreslování při roztahování, ale není na škodu vždy tuto "optimalizaci" provést. Existují 2 používané způsoby. Jedním z nich je zabránit výchozímu (DefWindowProc) zpracování zprávy WM_ERASEBKGND. Druhou možností je nastavit na začátku handle štětce okna na hodnotu NULL. V našem příkladě je použit první způsob. Do procedury okna přidáme následující kód:

// přepínač na hodnotu identifikátoru zprávy
switch (message)
{
case WM_ERASEBKGND:
return 0;
break;
// obsluhy dalších zpráv ...
}