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

Okna dialogowe, cz. 7

[lekcja]
Nasze dotychczasowe rozważania o oknach dialogowych skupiały się wokół plików zasobów (RC). W nich musieliśmy definiować okienka i znajdujące się w nich kontrolki. Użycie plików RC wiąże się z wieloma niedogodnościami, na przykład:

  • konieczność opanowania (dosyć dziwnej, nieelastycznej i słabo udokumentowanej) składni stosowanie jednostek miary innych niż piksele
  • jedyny w miarę działający edytor WYSIWYG do plików RC to Visual Studio (tylko wersje komercyjne) – w dodatku i ten pozostawia sporo do życzenia
  • czasami potrzeba dostosować dialog do danych już w czasie wykonania programu
  • pliki RC muszą znajdować się w tym samym module, co używający ich kod albo w pliku DLL

Najbardziej doskwierają nam zapewne trzy ostatnie punkty. Tym bardziej, że nawet Visual Studio dość topornie zarządza plikami RC (zwłaszcza identyfikatorami zasobów) i ma wiele innych wad jeśli chodzi o ich edycję. Wymaganie, żeby zasoby znalazły się w tym samym module również nie ułatwia nam życia – wystarczy sobie wyobrazić, że budujemy program z kilkoma bibliotekami LIB – jeśli chcemy, by współdzieliły one z głównym EXE pliki RC, musimy te ostatnie wpakować do oddzielnej biblioteki DLL...

Istnieją rozwiązania, które pozwalają utworzyć dialog bez plików RC – bezpośrednio w kodzie. Nie są to rozwiązania łatwe i przyjemne, ale w pewnych sytuacjach mogą się przydać.

Pierwszy sposób nazwijmy "oficjalnym", ponieważ można o nim poczytać w dokumentacji Windows API od Microsoftu. Polega on na utworzeniu wzorca okna dialogowego wraz z kontrolkami w pamięci.

Wzorzec w pamięci

Ta "oficjalna" sztuczka polega na utworzeniu w pamięci najpierw wzorca okna, a następnie kolejno wzorców wszystkich kontrolek. Jak zwykle w WinAPI, wszystko to sprowadza się do wypełniania odpowiednich struktur. Tym razem struktura nazywa się DLGTEMPLATE i wygląda tak:

C/C++
struct DLGTEMPLATE
{
    DWORD style;
    DWORD dwExtendedStyle;
    WORD cdit;
    short x;
    short y;
    short cx;
    short cy;
};

Wystarczy rzut oka by stwierdzić, że dość małe możliwości daje nam ta struktura. Nie możemy sobie nawet podać tytułu okna, nie wspominając o takich detalach, jak ustawienie czcionki (domyślna jest dość paskudna, jak już wiemy). Poza tym niniejszy kurs nie jest po to, by się zajmować rzeczami tak banalnymi, jak wypełnianie prostych struktur :-). Dlatego skorzystamy z "rozszerzonej" wersji tej struktury, która ma postać:

C/C++
struct DLGTEMPLATEEX
{
    WORD dlgVer;
    WORD signature;
    DWORD helpID;
    DWORD exStyle;
    DWORD style;
    WORD cDlgItems;
    short x;
    short y;
    short cx;
    short cy;
    sz_Or_Ord menu;
    sz_Or_Ord windowClass;
    WCHAR title[ titleLen ];
    WORD pointsize;
    WORD weight;
    BYTE italic;
    BYTE charset;
    WCHAR typeface[ stringLen ];
};

Są tu już wszystkie interesujące nas rzeczy, ale uważny czytelnik spostrzeże też haczyk (nie licząc "podejrzanego" typu sz_Or_Ord): tak, tablice znaków są tu zmiennej długości! Oczywiście nie da się zadeklarować takiej tablicy w języku C++, dlatego też powyższa deklaracja nie jest poprawna i nie znajdziemy jej w żadnym pliku nagłówkowym Windows API. Tak więc struktura taka po prostu nie istnieje, a w dokumentacji podano ją tylko i wyłącznie po to, by mniej więcej zobrazować sposób, w jaki musimy poukładać dane w pamięci...

Wyrażenie "mniej więcej" nie jest, niestety, przypadkowe. Mimo iż wspomniana dokumentacja bardzo się stara, sposób tworzenia wzorców w pamięci jest tak zawikłany, że na pewno żadne problemy nie zostaną nam oszczędzone :-).

Postraszyliśmy się, a teraz do roboty. Najpierw zadeklarujemy kilka pomocniczych zmiennych i zaalokujemy bufor, do którego będziemy wpychać kolejne dane naszego wzorca.

C/C++
HGLOBAL hgbl;
DLGTEMPLATE * pdt;
DLGITEMTEMPLATE * pdit;
WORD * pw;
LPWSTR pws;
int nchar;

hgbl = GlobalAlloc( GMEM_ZEROINIT, 1024 );
pdt =( DLGTEMPLATE * ) GlobalLock( hgbl );

1024 bajty powinny nam wystarczyć na mały dialog. Wzorzec musi mieć określony układ w pamięci; najpierw umieszczamy "strukturę" DLGTEMPLATEEX, a następnie odpowiednią liczbę struktur struktur DLGITEMTEMPLATEEX (o nich za chwilę). Wskaźniki pdt, pdit, pw i pws pomogą nam poruszać się po buforze. Pierwszym zadaniem jest ustawienie stylu okienka dialogowego i liczby kontrolek. Liczba kontrolek określa zarazem, ile struktur typu DLGITEMTEMPLATE lub DLGITEMTEMPLATEEX umieściliśmy w buforze:

C/C++
pdt->style = WS_POPUP | WS_BORDER | WS_SYSMENU | DS_MODALFRAME | WS_CAPTION;
pdt->cdit = 2;

Teraz musimy zmierzyć się z faktem, że należałoby podać współrzędne i wymiary dialogu. Oczywiście w owych arcyniewygodnych jednostkach "dialogowych". Autorzy WinAPI w swojej hojności dostarczyli nam funkcję MapDialogRect, która przelicza jednostki dialogowe na ekranowe (piksele), ale o funkcji, która robiłaby to w drugą stroną "zapomniano". Dlatego musimy sobie radzić sami. Funkcja MapDialogRect działa mniej więcej w ten sposób:

C/C++
left = MulDiv( left, baseunitX, 4 );
right = MulDiv( right, baseunitX, 4 );
top = MulDiv( top, baseunitY, 8 );
bottom = MulDiv( bottom, baseunitY, 8 );

...gdzie baseunitX i baseunitY to bazowe jednostki, które możemy sobie pobrać za pomocą funkcji GetDialogBaseUnits. Przez analogię możemy sobie napisać coś takiego:

C/C++
void PixelsToDialogUnits( RECT & rc )
{
    LONG base = GetDialogBaseUnits();
    int baseX = LOWORD( base );
    int baseY = HIWORD( base );
   
    rc.left = MulDiv( rc.left, 4, baseX );
    rc.right = MulDiv( rc.right, 4, baseX );
    rc.top = MulDiv( rc.top, 8, baseY );
    rc.bottom = MulDiv( rc.bottom, 8, baseY );
}

Teraz żadne jednostki nie są nam straszne, możemy sobie wszystko podawać w pikselach, co też i uczynimy:

C/C++
RECT rcDlg = { 10, 10, 200, 150 };
PixelsToDialogUnits( rcDlg );

pdt->x =( short ) rcDlg.left;
pdt->y =( short ) rcDlg.top;
pdt->cx =( short ) rcDlg.right;
pdt->cy =( short ) rcDlg.bottom;

Dialog zyskał pozycję na ekranie i właściwy rozmiar. Pora przejść do tajemniczych pól menu i windowClass. Ponieważ w gruncie rzeczy doskonale znamy te pola z funkcji CreateWindowEx, więc nie będziemy tu po raz setny odkrywać Ameryki. Menu nie potrzebujemy, zaś jako klasę wykorzystamy domyślną, dlatego też obydwa pola wystarczy ustawić na 0. Najpierw jednak należy zadbać o odpowiedni offset w buforze:

C/C++
pw =( WORD * )( pdt + 1 );
* pw++ = 0x0; // brak menu
* pw++ = 0x0; // predefiniowana klasa dialogu

Pozostał tylko jeden mały problem: tytuł okna. Widzimy, że jest to tablica elementów typu WCHAR, a więc wygląda na to, że panowie z Microsoftu poczuli przez moment tchnienie nowoczesności i postanowili użyć Unikodu. Tak więc podając nasz tytuł powinniśmy poprzedzić go prefiksem L, po czym umieścić w buforze np. za pomocą lstrcpyW. Można też nieco wygodniejszym sposobem: zastosować konwertującą funkcję MultiByteToWideChar, która przy okazji policzy za nas długość stringa, co będzie nam znów potrzebne do offsetu:

C/C++
pws =( LPWSTR ) pw;
nchar = 1 + MultiByteToWideChar( CP_ACP, 0, "Mój dialog", - 1, pws, 50 );
pw += nchar;

Ostatnie 5 pól pomijamy. Teoretycznie powinny one służyć do ustawiania domyślnej czcionki dialogu, ale niestety nie udało mi się w żaden sposób zmusić dialogu z wypełnionymi tymi polami do działania. Na szczęście bez flagi DS_SETFONT w polu style te felerne pola nie są wymagane.

Skończyliśmy z samym dialogiem, pora teraz na kontrolki. Jak już mimochodem wspomnieliśmy, tym razem wypełniamy je według DLGITEMTEMPLATEEX – kolejnej struktury, której nie ma ;-). Gdyby jednak istniała, miałaby z grubsza taką postać:

C/C++
struct DLGITEMTEMPLATEEX
{
    DWORD helpID;
    DWORD exStyle;
    DWORD style;
    short x;
    short y;
    short cx;
    short cy;
    WORD id;
    sz_Or_Ord windowClass;
    sz_Or_Ord title;
    WORD extraCount;
};

Ponownie przesuwamy się z naszymi wskaźnikami w buforze, ale uwaga. Adres opisu każdej kontrolki musi być wyrównany do WORD-a – tako rzecze dokumentacja. Nie musimy się zastanawiać, co oznacza ten enigmatyczny wymóg, ponieważ dokumentacja dostarcza nam również przykładowej funkcji, która robi, co trzeba:

C/C++
WORD * lpwAlign( WORD * in )
{
    ULONG_PTR ul =( ULONG_PTR ) in;
    ul += 3;
    ul >>= 2;
    ul <<= 2;
    return( WORD * ) ul;
}

Mając takie coś możemy zrobić porządek ze wskaźnikami i jechać dalej:

C/C++
pw = lpwAlign( pw );
pdit =( DLGITEMTEMPLATE * ) pw;

Pierwszą z dwóch zaplanowanych kontrolek będzie przycisk. Najpierw ustalamy jego wielkość i pozycję – to już znamy:

C/C++
RECT rcBtn = { 50, 100, 100, 25 };
PixelsToDialogUnits( rcBtn );

pdit->x =( short ) rcBtn.left;
pdit->y =( short ) rcBtn.top;
pdit->cx =( short ) rcBtn.right;
pdit->cy =( short ) rcBtn.bottom;

Kolejnym krokiem jest nadanie identyfikatora oraz ustawienie odpowiedniej kombinacji stylów. Tu również bez wielkich niespodzianek:

C/C++
pdit->id = IDOK;
pdit->style = WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON;

Wypełniliśmy wszystko, czym dysponuje "istniejąca" część struktury, czyli DLGITEMTEMPLATE. Dalej mamy klasę okna i tytuł. Jako klasę możemy podać stringa z nazwą, ale możemy również podać też liczbę atomową klasy. W tym akurat przypadku prościej będzie nam podać liczbę. Wartość 0x0080 oznacza przycisk. Poprzedzamy ją w buforze specjalnym kodem 0xFFFF, co oznacza, że podajemy atom, a nie stringa:

C/C++
pw =( WORD * )( pdit + 1 );
* pw++ = 0xffff;
* pw++ = 0x0080; // przycisk

Pozostaje tylko ustawienie napisu na przycisku. To już właściwie przerobiliśmy wcześniej z oknem dialogowym. Ostatnie pole olewamy sobie, wpisując 0:

C/C++
pws =( LPWSTR ) pw;
nchar = 1 + MultiByteToWideChar( CP_ACP, 0, "OK", - 1, pws, 50 );
pw += nchar;
* pw++ = 0; // brak dodatkowych danych

Drugą kontrolkę (pole tekstowe) tworzymy bardzo podobnie:

C/C++
pw = lpwAlign( pw );
pdit =( DLGITEMTEMPLATE * ) pw;

RECT rcEdit = { 25, 10, 150, 25 };
PixelsToDialogUnits( rcEdit );

pdit->x =( short ) rcEdit.left;
pdit->y =( short ) rcEdit.top;
pdit->cx =( short ) rcEdit.right;
pdit->cy =( short ) rcEdit.bottom;

pdit->id = 1000;
pdit->style = WS_CHILD | WS_VISIBLE | WS_BORDER;

pw =( WORD * )( pdit + 1 );
* pw++ = 0xffff;
* pw++ = 0x0081; // edit box

pws =( LPWSTR ) pw;
nchar = 1 + MultiByteToWideChar( CP_ACP, 0, "Tekst", - 1, pws, 50 );
pw += nchar;
* pw++ = 0; // brak dodatkowych danych

Wzorzec w pamięci jest już gotowy. Stwórzmy sobie teraz szybko procedurę dialogową dla naszego okienka – najprostszą z możliwych, nie robiącą zupełnie nic:

C/C++
BOOL CALLBACK DialogProc( HWND hwnd, UINT Msg, WPARAM wParam, LPARAM lParam )
{
    return false;
}

Możemy już użyć naszego wzorca i wywołać okienko dialogowe na ekran. Do dyspozycji mamy: CreateDialogIndirect, CreateDialogIndirectParam, DialogBoxIndirect, DialogBoxIndirectParam. Pierwsze dwie funkcje tylko tworzą dialog, pozostałe dwie również wyświetlają go i czekają na jego zakończenie. Do dzieła:

C/C++
GlobalUnlock( hgbl );
DialogBoxIndirect( hInst,( DLGTEMPLATE * ) hgbl, hWndOwner,( DLGPROC ) DialogProc );
GlobalFree( hgbl );
Poprzedni dokument Następny dokument
Okna dialogowe, cz. 6 Okna dialogowe, cz. 8