Jeśli czytałeś już o
kolorach kontrolek i opanowała cię żądza zmieniania "kastomizacji" wyglądu wszystkiego, co się da, to być może natrafiłeś gdzieś na nieudokumentowaną stałą
WM_CTLCOLORMSGBOX... Mogłoby się wydawać, że oto mamy świetny sposób na ustawienie kolorków w MessageBox-ie. Wystarczyłoby obsłuży komunikat analogicznie do
WM_CTLCOLORSTATIC i całej reszty z tej rodzinki i...
Niestety, nic z tego. Komunikat ten nie jest nigdy wysyłany do naszej aplikacji. Ale jest inny sposób, by dobrać się do MessageBox-a. I dokładnie jak można się było tego spodziewać, jest on zupełnie niespodziewany.
Być może miałeś już okazję przeczytać [[Haki|artykuł o hakach[/div]. Jest tam wspomniany jeden wyjątkowo ciekawy hak –
WH_CBT. Jedną z jego rozlicznych funkcji jest kontrola tworzenia i niszczenia okienek. Skoro możemy "uchwycić" moment, gdy MessageBox jest tworzony (i mamy jego
HWND), to możemy mu też podmienić procedurę dialogową. A jeśli mamy procedurę dialogową, to możemy też obsłużyć
WM_CTLCOLORDLG i
WM_CTLCOLORSTATIC! W ten sposób możemy nie tylko zmienić kolor tła i tekstu, ale również zmienić ikonkę czy dodać nowe przyciski. Na razie jednak skupimy się na tym pierwszym zadaniu.
Nowa funkcja do MessageBox-a
Ponieważ hak
WH_CBT ma działanie globalne, a możemy nie życzyć sobie podmiany MessageBox-ów w całym systemie, warto byłoby napisać w tym celu własną funkcję pokazującą okno z wiadomością. Funkcja ta ustawiałaby hak, uruchamiała MessageBox-a z "podstawioną" procedurą dialogową, a następnie usuwała hak, aby inne MessageBox-y wyglądały normalnie.
Zacznijmy od kilku zmiennych globalnych:
HHOOK g_hHook;
HWND g_hWndMsgBox;
WNDPROC g_MsgBoxProc;
HBRUSH g_hBrush;
HBITMAP g_hBitmap;
Mamy tu uchwyt do haka i wskaźnik do nowej procedury dialogowej dla MessageBox-a, by wykonać wspomniane wyżej kroki. Mamy też uchwyt do samego MessageBox-a, który przyda się nam w procedurze haka – zaraz powiemy do czego dokładnie. Pozostałe dwa uchwyty wykorzystamy już do malowania.
int CoolMsgBox( HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType )
{
g_hHook = NULL;
g_hWndMsgBox = NULL;
g_MsgBoxProc = NULL;
HINSTANCE hInstance = GetModuleHandle( NULL );
g_hHook = SetWindowsHookEx( WH_CBT, HookProc, hInstance, GetCurrentThreadId() );
int nRet = MessageBox( hWnd, lpText, lpCaption, uType );
UnhookWindowsHookEx( g_hHook );
return nRet;
}
Sygnatura funkcji przypomina jako żywo funkcję
MessageBox, ale po co miałaby się różnić? Na początek zerujemy na wszelki wypadek nasze kluczowe zmienne. Dalej pobieramy uchwyt aktualnej instancji, by w następnej linii móc ustawić hak. Następnie wywołujemy standardową funkcję
MessageBox. W tym właśnie momencie procedura hakowa (którą za chwilę napiszemy) zostanie wywołana po raz pierwszy, ponieważ utworzy się okno MessageBox-a. Po wyjściu z funkcji
MessageBox (tj. gdy użytkownik zamknie MessageBox-a) procedura hakowa woła się po raz drugi, ponieważ niszczymy okno. Teraz możemy już usunąć hak i zwrócić wartość, którą dostaliśmy od MessageBox-a.
Procedura hakowa
Jak już wspomnieliśmy, procedura hakowa zostanie wywołana dwukrotnie: przy tworzeniu okienka MessageBox-a i przy jego niszczeniu. Tak naprawdę woła się ona znacznie częściej, ale nas interesują akurat te dwa zdarzenia. Ponadto hak zareaguje również na tworzenie okienek potomnych MessageBox-a, czyli kontrolek: tej wyświetlającej tekst, tej wyświetlającej ikonę i przycisku. Podobnie z niszczeniem okna. Konkretne zdarzenie rozpoznamy, sprawdzając wartość parametru
nCode w procedurze hakowej. Natomiast z rozróżnianiem poszczególnych okienek sprawa jest nieco bardziej skomplikowana:
LRESULT CALLBACK HookProc( int nCode, WPARAM wParam, LPARAM lParam )
{
if( nCode < 0 )
return CallNextHookEx( g_hHook, nCode, wParam, lParam );
switch( nCode )
{
case HCBT_CREATEWND:
{
LPCBT_CREATEWND lpCbtCreate =( LPCBT_CREATEWND ) lParam;
if( lpCbtCreate->lpcs->lpszClass == WC_DIALOG )
{
g_hWndMsgBox =( HWND ) wParam;
}
else
{
if( g_MsgBoxProc == ] NULL )
g_MsgBoxProc =( WNDPROC ) SetWindowLongPtrA( g_hWndMsgBox,
GWLP_WNDPROC,( LONG_PTR ) CoolMsgBoxProc );
}
}
break;
case HCBT_DESTROYWND:
{
if(( HWND ) wParam == g_hWndMsgBox )
SetWindowLongPtr( g_hWndMsgBox, GWLP_WNDPROC,( LONG_PTR ) g_MsgBoxProc );
}
}
return 0;
}
Funkcja
HookProc najpierw wywoła się z kodem
HCBT_CREATEWND, gdy będzie tworzone okno MessageBox-a. Rozpoznamy je dość łatwo. Korzystając ze struktury "ukrytej" w
lParam możemy sprawdzić jego klasę. Ponieważ MessageBox to zwykły dialog, jego klasą będzie
WC_DIALOG. Jednak kryje się tu pewna pułapka: w tym momencie okno będzie już utworzone, ale procedura dialogowa nie będzie jeszcze do niego przypisana. Tak więc jej podmiana nic nie da. Dlatego też na razie nic nie robimy z oknem, zapamiętujemy tylko jego uchwyt (dostajemy go w
wParam) w zmiennej
g_hWndMsgBox.
Następne wywołania
HookProc, jak już powiedzieliśmy, dotyczyć będą kontrolek. Właściwie to nie chcemy z tymi kontrolkami nic robić, ale tym razem procedura dialogowa będzie już na swoim miejscu, toteż możemy skorzystać z okazji, aby ją sobie podmienić, wywołując
SetWindowLongPtr.
Od razu piszemy też obsługę niszczenia okna, czyli przywracamy "starą" procedurę dialogową, którą zapamiętaliśmy wcześniej w zmiennej
g_hWndMsgBox. Oczywiście robimy to tylko raz – dla samego okna, nie dla jego kontrolek, stąd ten
if.
Procedura dialogowa
Ostatni (i najważniejszy) etap zabawy to nowa procedura dialogowa dla MessageBox-a – ta, którą mu ustawiliśmy przed chwilą. To ona będzie zmieniała kolory kontrolkom, a przy okazji również stworzy dla nich nowe, piękne tło z bitmapy, którą sobie wcześniej narysujemy ;-).
LRESULT CALLBACK CoolMsgBoxProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
switch( uMsg )
{
case WM_INITDIALOG:
{
HINSTANCE hInstance = GetModuleHandle( NULL );
HBITMAP hBitmap =( HBITMAP ) LoadImage( hInstance,
"msgbox.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE );
g_hBrush = CreatePatternBrush( hBitmap );
DeleteObject( hBitmap );
}
break;
case WM_CTLCOLORDLG:
case WM_CTLCOLORSTATIC:
{
HDC hDC =( HDC ) wParam;
SetBkMode( hDC, TRANSPARENT );
SetTextColor( hDC, RGB( 0, 128, 0 ) );
return( LRESULT ) g_hBrush;
}
break;
case WM_COMMAND:
{
DeleteObject( g_hBrush );
}
break;
}
return CallWindowProc( g_MsgBoxProc, hWnd, uMsg, wParam, lParam );
}
Jak widać, bitmapę wczytujemy podczas inicjalizacji dialogu (MessageBox-a), po czym od razu tworzymy z niej pędzelek i zwalniamy. Następnie, tak jak w artykule
Kolory kontrolek, odpowiadamy na komunikaty
WM_CTLCOLORDLG i
WM_CTLCOLORSTATIC, by zmienić kolor tekstu i tło.
Gdy użytkownik zamknie okienko, dostaniemy komunikat
WM_COMMAND i to będzie dla nas sygnał, że można już skasować nasz pędzel.
To wszystko, zostało nam jeszcze tylko wywołać naszą funkcję i sprawdzić, czy faktycznie działa:
CoolMsgBox( hwnd, "To jest właśnie nasz nowy, lepszy MessageBox w akcji.",
"Test", MB_ICONINFORMATION );