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

WinForms a C++ - překreslování bez problikávání pozadí

3.5.2017

Pokud na okně formuláře máme panel do kterého chceme vykreslovat bitmapu nebo nějakou grafiku která má svoji velikost automaticky přizpůsobovat velikosti okna (resp. panelu), musíme provést její překreslení v reakci na událost "Resize", tj. při každé změně velikosti byť o jediný pixel během uživatelského roztahování okna. A zde narazíme na problém problikávání barvy pozadí panelu. Čím je to způsobeno? Ve Windows API (na kterém je samozřejmě .NET postaveno) při požadavku na překreslení okna přijde oknu nejprve zpráva WM_ERASEBKGND, jejímž výchozím zpracováním (pokud ji nezadržíme) je vyplnění oblasti okna barvou štětce (HBRUSH) který je nastaven odpovídající třídě okna. Poté přijde zpráva WM_PAINT, v jejíž obsluze uživatel (tj. programátor) vykreslí to co chce mít "trvale" na okně. V .NET je obsluha zprávy WM_PAINT implementována v události "Paint", kterou jistě zná a často využívá každý .NET programátor. Bohužel událost implementující obsluhu WM_ERASEBKGND mezi událostmi není.

Při programování ve Windows API (nebo MFC, ATL/WTL apod.) je řešení jednoduché a máme dvě možnosti. Buď nastavit při registraci třídy okna "nulový štětec", tj. prvek typu HBRUSH příslušné struktury (WNDCLASSEX) na NULL (0, nullptr). Druhou možností je v proceduře okna zachytit zprávu WM_ERASEBKGND a nepustit ji k výše zmíněnému výchozímu zpracování, tj. vrátit 0 místo volání funkce DefWidowProc.

Ve WinForms bychom mohli vytvořit v projektu typu "Class Library" vlastní komponentu odvozenou od třídy Panel a přepsat virtuální metodu CreateParams a v ní nastavit výše zmíněný nulový štětec. Pak bychom museli tuto komponentu přidat na panel nástrojů a každý projekt by musel být distribuován s touto knihovnou tříd. Pro masochisticky založeného vývojáře je to možná dobrá zábava...

Ale (samozřejmě předpokládáme projekt v C++) řešení je i ve WinForms velice jednoduché. Stačí se "napíchnout" na proceduru okna panelu pomocí tzv. subclassingu a v ní provést výše zmíněné zadržení zprávy WM_ERASEBKGND. Jak na to? Nejprve si musíme nadeklarovat proměnnou ve které budeme mít uloženou adresu původní procedury okna a napsat vlastní funkci na kterou bude přesměrována procedura okna. Toto bude v našem případě vypadat následovně.

__declspec(selectany) WNDPROC wndProcPanelObrOrig = nullptr;

inline LRESULT CALLBACK PanelObrWindowProc(HWND hwnd,
	UINT zprava, WPARAM wparam, LPARAM lparam) noexcept
{
	if (zprava == WM_ERASEBKGND)
		return 0;
	else
		return CallWindowProc(wndProcPanelObrOrig,
			hwnd, zprava, wparam, lparam);
}

Pak už zbývá v obsluze události Load formuláře provést zmíněný subclassing:

System::Void OnLoad(System::Object^  sender, System::EventArgs^  e)
{
	wndProcPanelObrOrig =
		(WNDPROC)SetWindowLongPtrW(reinterpret_cast(
			this->panelObrazek->Handle.ToPointer()),
			GWLP_WNDPROC, (LONG_PTR)PanelObrWindowProc);
}

a před zrušením okna vrátit vše do původního stavu:

System::Void OnClosed(System::Object^  sender,
	System::Windows::Forms::FormClosedEventArgs^  e)
{
	SetWindowLongPtrW(reinterpret_cast(
		this->panelObrazek->Handle.ToPointer()),
		GWLP_WNDPROC, (LONG_PTR)wndProcPanelObrOrig);
}

Samozřejmě pokud zadržíme zprávu WM_ERASEBKGND, musíme v obsluze zprávy WM_PAINT vyplnit kreslením celou (klientskou) oblast okna panelu, to znamená pokud kreslíme nějakou bitmapu se zachováním poměru stran, musíme také vyplnit příslušné okraje.