Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: 'Złośliwiec'
Biblioteki C++

Tuning MessageBox-a

[lekcja] Artykuł opisuje w jaki sposób można zmienić wygląd standardowych okienek dialogowych.
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.

MessageBox po tuningu (Windows XP)
MessageBox po tuningu (Windows XP)
 
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:
C/C++
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.
C/C++
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:
C/C++
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 ;-).
C/C++
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 » Kurs WinAPI, C++ » KontrolkiKolory kontrolek lekcja, 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:
C/C++
CoolMsgBox( hwnd, "To jest właśnie nasz nowy, lepszy MessageBox w akcji.",
"Test", MB_ICONINFORMATION );
Poprzedni dokument Następny dokument
Okna o nieregularnym kształcie Kurs WinSock, C++