Dostosowywanie predefiniowanych dialogów
Predefiniowane dialogi z pewnością oszczędzają programistom wiele pracy, ale są sytuacje, kiedy okazują się one trochę za mało rozbudowane. Przykładowo: robimy program graficzny i chcemy, żeby w dialogu wyboru pliku pojawiała się dodatkowo opcja podglądu zawartości pliku w miniaturze. Czy musimy wtedy cały dialog robić sami od zera? Oczywiście nie. Wystarczy deczko zmodyfikować ten systemowy...
Jeśli zainteresowałeś się już subclassingiem, to wiesz, że zmiana wyglądu i zachowania kontrolek odbywa się poprzez "dobranie się" do ich procedury okna. Podobnie mają się sprawy z predefiniowanymi dialogami systemowymi. Możemy sobie napisać własną procedurę, która będzie obsługiwała wszelkie komunikaty wysyłane do dialogu, a następnie przekazywała je (lub nie) do właściwej (domyślnej) procedury dialogu. Taka "fałszywa" procedura dialogu nazywa się z angielska ''hook procedure'' (czyli, hmmm, procedura hakowa - z braku lepszej nazwy tak będę na nią tutaj mówił).
Nasz przykładowy program będzie poszerzał możliwości standardowego dialogu wyboru pliku o możliwość podglądu zawartości bitmap. Innymi słowy, jeśli użytkownik zaznaczy plik *.bmp, to obrazek z tego pliku zostanie wyświetlony na dialogu.
Zacznijmy od deklaracji procedury hakowej - w tym punkcie nie różni się ona zbytnio od "zwykłej" procedury okna czy dialogu:
UINT APIENTRY OFNHookProc( HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam );
Oczywiście w miejsce OFNHookProc możemy wstawić dowolną nazwę, jaka nam odpowiada, dowolne są też nazwy poszczególnych argumentów. Jedyne, co musi się zgadzać z powyższym przykładem, to oczywiście typy argumentów oraz typ zwracany, ewentualnie konwencja wywołania APIENTRY (dla dociekliwych: APIENTRY to synonim słowa APIPRIVATE albo WINAPI albo PASCAL albo CALLBACK albo STDCALL, i wszystkie one oznaczają konwencję wywołania __stdcall, czyli argumenty są wrzucane na stos od prawej do lewej i funkcja wołająca jest odpowiedzialna za wyczyszczenie stosu).
Na razie nasza procedura hakowa jest pusta, ale już możemy ją podpiąć pod nasz dialog. Robimy to w dwóch krokach: po pierwsze do flag struktury OPENFILENAME dodajemy OFN_ENABLEHOOK, po drugie podajemy adres naszej procedury hakowej do pola lpfnHook wspomnianej struktury:
OPENFILENAME ofn;
char sNazwaPliku[ MAX_PATH ] = "";
ZeroMemory( & ofn, sizeof( ofn ) );
ofn.lStructSize = sizeof( ofn );
ofn.lpstrFilter = "Bitmapy (*.bmp)\0*.bmp\0Wszystkie pliki\0*.*\0";
ofn.nMaxFile = MAX_PATH;
ofn.lpstrFile = sNazwaPliku;
ofn.lpstrDefExt = "bmp";
ofn.lpfnHook = & OFNHookProc; ofn.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_ENABLEHOOK | OFN_EXPLORER;
Możesz zauważyć, że dorzuciliśmy jeszcze jedną flagę, o której nic dotąd nie wspominaliśmy - OFN_EXPLORER. Oznacza ona, że dialog wyboru pliku ma być w "nowym" stylu (jeśli pominiesz teraz tę flagę, to przekonasz się, jak wyglądał "stary" styl ;-) ). Domyślnie wszystkie dialogi wyświetlane są w "nowym" stylu, ale w przypadku gdy korzystamy z procedury hakowej (bądź z flagi OFN_ALLOWMULTISELECT), musimy podać OFN_EXPLORER jawnie.
Od tej pory wszystkie komunikaty przeznaczone dla dialogu wyboru pliku kierowane będą najpierw do naszej procedury hakowej (wyjątkiem jest komunikat WM_INITDIALOG, który kierowany jest najpierw do domyślnej procedury dialogu, a dopiero potem do hakowej). Jeśli procedura hakowa odpowie na odebrany komunikat, zwracając 0, to komunikat ten zostanie przesłany dalej do domyślnej procedury dialogu. Jeśli natomiast zwróci wartość niezerową, to komunikat zostaje (po ewentualnym obsłużeniu wewnątrz procedury hakowej) zignorowany.
Sprawdźmy, czy to wszystko działa, obsługując komunikat WM_INITDIALOG. Na razie zareagujemy na niego tylko "bipnięciem":
UINT APIENTRY OFNHookProc( HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam )
{
if( uiMsg == WM_INITDIALOG )
Beep( 0, 0 );
return 0;
}
Jeśli wpisaliśmy wszystko w dobrym miejscu i wywołamy teraz na scenę nasz dialog wyboru pliku, to powinna się zawołać nasza procedura hakowa:
GetOpenFileName( & ofn );
Faktycznie, przy pokazywaniu dialogu słychać teraz jakiś dźwięk (o ile nie wyłączyliśmy go w Panelu Sterowania), a więc udało się - mamy komunikaty dialogu i nie zawahamy się ich użyć ;-). Ma się rozumieć, że wydawanie głupich dźwięków nie jest najciekawszą rzeczą, jaką chcemy dodać do naszego dialogu. Chcieliśmy przecież podgląd bitmap...
Żeby dodać do dialogu własne elementy, musimy najpierw napisać odpowiedni skrypt *.rc, zawierający definicję nowego dialogu ze wszystkimi kontrolkami, jakie chcemy umieścić na dialogu wyboru pliku. Kontrolki te będą domyślnie umieszczone pod spodem całego dialogu (jest jednakże sposób aby umieścić je gdzie indziej - pokażemy go później). Nasz przykładowy skrypt będzie wyglądał tak:
IDD_DODATEK DIALOG 6, 18, 186, 72
STYLE WS_CHILD | WS_CLIPSIBLINGS | DS_3DLOOK | DS_CONTROL
CAPTION "Dialog Title"
FONT 8, "MS Sans Serif"
{
CONTROL "", IDC_RAMKA, "Static", SS_BLACKFRAME, 6, 6, 54, 60
AUTOCHECKBOX "Podgl¹d w³¹czony", IDC_PREVIEW, 66, 12, 108, 10
LTEXT "", IDC_IMGSIZE, 66, 48, 90, 12
}
Jak widać, umieściliśmy w tym skrypcie jednego checkboxa (którym będzie można włączać lub wyłączać podgląd), jedną kontrolkę typu STATIC (będzie pełnić rolę ramki) oraz jedną kontrolkę do wyświetlenia wymiarów obrazka. Zwróć uwagę na style dialogu - jest ważne, żeby były one właśnie takie, gdyż w przeciwnym wypadku doświadczymy bardzo niemiłych efektów :-).
Oczywiście musimy jeszcze gdzieś zdefiniować użyte identyfikatory:
#define IDD_DODATEK 800
#define IDC_PREVIEW 801
#define IDC_IMGSIZE 802
#define IDC_RAMKA 803
Teraz musimy sprawić, żeby wzorzec ze skryptu został skojarzony z dialogiem wyboru plików. W tym celu do flag dołączamy OFN_ENABLETEMPLATE, musimy również podać uchwyt do naszego programu w polu hInstance oraz oczywiście identyfikator naszego dialogu-wzorca (tutaj IDD_DODATEK). Wszystko to, ma się rozumieć, powinno znaleźć się w tym samym miejscu, co nasze wcześniejsze dłubanie przy strukturze ofn:
ofn.hInstance = GetModuleHandle( NULL ); ofn.lpTemplateName = MAKEINTRESOURCE( IDD_DODATEK ); ofn.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_ENABLEHOOK | OFN_EXPLORER | OFN_ENABLETEMPLATE;
Gdybyś w tym miejscu skompilował program, to mógłbyś już zobaczyć dialog poszerzony o nasze dodatkowe kontrolki. Tyle, że nie będą one jeszcze działały. Dlatego należałoby teraz zabrać się za procedurę hakową i obsłużyć w niej wszystkie potrzebne komunikaty.
Wszystkie interesujące nas komunikaty będą miały formę powiadomienia WM_NOTIFY, z którym prawdopodobnie jesteśmy już obeznani. UWAGA - powiadomienia WM_NOTIFY od dialogu dostanie nasza procedura hakowa OFNHookProc, a nie procedura głównego okna! Zmienna lParam tego komunikatu zawiera wskaźnik na strukturę, która mówi nam, co dalej. Na razie najbardziej potrzebujemy powiadomienia CDN_SELCHANGE, które dostaniemy wtedy, gdy użytkownik zaznaczy jakiś plik - wtedy będziemy mogli sobie sprawdzić, czy jest to bitmapa i jeśli tak, to wyświetlimy jej podgląd:
case WM_NOTIFY:
{
NMHDR * pnmhdr =( NMHDR * ) lParam;
if( pnmhdr->code == CDN_SELCHANGE )
{
OFNOTIFY * ofnot =( OFNOTIFY * ) lParam;
}
}
break;
Jak widzimy, w przypadku powiadomień od dialogu wyboru pliku lParam zawiera wskaźnik na większą strukturę typu OFNOTIFY. Siedzi w niej standardowa struktura NMHDR, wskaźnik na strukturę OPENFILENAME oraz wskaźnik na stringa pszFile. Wbrew pozorom, ten ostatni nie zawiera interesującej nas nazwy pliku i możemy go używać tylko w przypadku błędu sieci. A więc nic nas on nie obchodzi. Nazwę pliku odczytamy ze znanego nam już pola struktury OPENFILENAME, do której, jako się rzekło, dostaliśmy wskaźnik. Nie pozostaje zatem nic innego, jak tylko zrobić to, po czym wczytać bitmapę:
UINT APIENTRY OFNHookProc( HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam )
{
switch( uiMsg )
{
case WM_NOTIFY:
{
NMHDR * pnmhdr =( NMHDR * ) lParam;
OFNOTIFY * ofnot =( OFNOTIFY * ) lParam;
if( pnmhdr->code == CDN_SELCHANGE )
{
SendMessage( hdlg, CDM_GETFILEPATH, sizeof( buffer ),( LPARAM ) & buffer );
LPSTR filename = buffer;
FreeBitmap();
g_hBitmap =( HBITMAP ) LoadImage( GetModuleHandle( NULL ), filename, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE );
}
else if( pnmhdr->code == CDN_FILEOK )
{
FreeBitmap();
}
}
break;
case WM_DESTROY:
{
FreeBitmap();
}
break;
}
return 0;
}
Jak widać, zadbaliśmy o prawidłowe zwalnianie bitmapy, gdy nie jest już potrzebna. Robimy to zarówno kiedy zamyka się dialog, jak i podczas zaznaczania kolejnych plików przez użytkownika (aby skasować poprzednią bitmapę z podglądem). Oczywiście funkcja FreeBitmap() zawoła się nam aż dwa razy w przypadku wciśnięcia OK, ale nic nie szkodzi - zabezpieczymy się przed tym, a chciałem pokazać, że można obsłużyć również powiadomienie CDN_FILEOK.
Napisanie funkcji FreeBitmap nie jest oczywiście trudne, ale na wszelki wypadek:
void FreeBitmap()
{
if( g_hBitmap )
DeleteObject( g_hBitmap );
g_hBitmap = NULL;
}
Teraz musimy już tylko bitmapkę wyświetlić. Potrzebujemy do tego kontekst pozaekranowy kompatybilny z kontekstem okna, więc stwórzmy go sobie:
HDC hDC = GetDC( hwnd );
g_hDCMem = CreateCompatibleDC( hDC );
assert( g_hDCMem );
ReleaseDC( hwnd, hDC );
Następnie w procedurze hakowej obsługujemy komunikat WM_COMMAND. Wcześniej deklarujemy zmienną g_Preview typu bool oznaczającą, czy podgląd jest włączony czy też nie. W obsłudze WM_COMMAND ustawiamy tę zmienną na podstawie stanu checkbox-a:
case WM_COMMAND:
{
g_Preview =( IsDlgButtonChecked( hdlg, IDC_BMPPREVIEW ) == BST_CHECKED );
InvalidateRect( hdlg, NULL, true );
}
break;
Jak widać, kliknięcie na checkbox-a (i w sumie nie tylko na niego, ale nie chce nam się sprawdzać dodatkowych warunków ;-)) spowoduje też odświeżenie okna poprzez wywołanie WM_PAINT dialogu. Musimy zatem dopisać jego obsługę i tam też narysujemy naszą bitmapę z podglądem:
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint( hdlg, & ps );
if( g_Preview && g_hBitmap && g_hDCMem )
{
HWND hFrame = GetDlgItem( hdlg, IDC_RAMKA );
RECT rcFrame;
GetWindowRect( hFrame, & rcFrame );
int w = rcFrame.right - rcFrame.left;
int h = rcFrame.bottom - rcFrame.top;
BITMAP bm;
GetObject( g_hBitmap, sizeof( BITMAP ), & bm );
ScreenToClient( hdlg,( LPPOINT ) & rcFrame );
ScreenToClient( hdlg,( LPPOINT ) & rcFrame.right );
HBITMAP hbmDefault = SelectBitmap( g_hDCMem, g_hBitmap );
StretchBlt( hdc, rcFrame.left, rcFrame.top, w, h, g_hDCMem, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY );
SelectBitmap( g_hDCMem, hbmDefault );
}
EndPaint( hdlg, & ps );
}
break;
Jeśli podgląd został włączony, to pobieramy sobie współrzędne naszej ramki. Są podane względem ekranu, więc potrzebujemy konwersji poprzez ScreenToClient(). Pobieramy też właściwe rozmiary bitmapy. Musimy są przeskalować, aby zmieściła się dokładnie w ramce bez względu na to, jak duża (lub jak mała) jest w rzeczywistości. Dlatego też używamy StretchBlt zamiast standardowego BitBlt. StretchBlt ma trochę więcej parametrów - podajemy dodatkowo wymiary źrodłowe obrazu. Dlatego właśnie pobraliśmy je przez GetObject(). Więcej wytłumaczeń, mam nadzieję, nie potrzeba - oto mamy podgląd zrobiony i działający :-).
Ma się rozumieć, że nie wykorzystaliśmy jeszcze wszystkich możliwości "customizacji" tego dialogu. Jest jeszcze szereg innych powiadomień do obsłużenia. Mamy też dostęp do wszystkich kontrolek, które domyślnie są umieszczane w takim dialogu. Po co się jednak o tym rozpisywać, skoro wszystko to jest w dokumentacji? :-)