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

MDI

[lekcja]
MDI to skrót od angielskiego Multiple Document Interface i jest to po prostu takie coś, gdzie mamy okienka w okienku. W MSDN możemy przeczytać, że MDI jest sposobem tworzenia aplikacji zorientowanych pod kątem pracy z wieloma dokumentami jednocześnie. Jednak nie ma co rozgryzać takie definicje, najlepiej zobaczyć przykład takiej aplikacji:

To jest aplikacja wykorzystująca MDI (Windows Vista)
To jest aplikacja wykorzystująca MDI (Windows Vista)

Podstawy MDI

Jak to zwykle bywa w WinApi, żeby korzystać z MDI trzeba nieco zachodu. Na początek trzeba dodać nową klasę okna. Standardowo aplikacja MDI ma zdefiniowane dwie klasy okna. Klasę okna głównego i klasę okna dziecka. My musimy dodać tą drugą. Okno dziecko jest właśnie tym okienkiem w okienku. Trzeba więc zmodyfikować tworzenie klasy okna w funkcji WinMain:

C/C++
WNDCLASSEX wincl;

// Klasa okna głównego

wincl.hInstance = hInstance;
wincl.lpszClassName = "KlasaOkna";
wincl.lpfnWndProc = WindowProcedure;
wincl.style = 0;
wincl.cbSize = sizeof( WNDCLASSEX );
wincl.hIcon = LoadIcon( hInstance, IDI_APPLICATION );
wincl.hIconSm = LoadIcon( hInstance, IDI_APPLICATION );
wincl.hCursor = LoadCursor( NULL, IDC_ARROW );
wincl.lpszMenuName = NULL;
wincl.cbClsExtra = 0;
wincl.cbWndExtra = 0;
wincl.hbrBackground =( HBRUSH ) COLOR_WINDOW;

if( !RegisterClassEx( & wincl ) )
   
 return FALSE;

// Klasa okna dziecka

wincl.lpfnWndProc = ChildWindowProcedure;
wincl.lpszMenuName =( LPCTSTR ) NULL;
wincl.lpszClassName = "KlasaOknaDziecka";

if( !RegisterClassEx( & wincl ) )
   
 return FALSE;

Początkującym może się wydać dość dziwne, że używamy tej samej zmiennej wincl do utworzenia obydwu klas. Zamiast powtarzać ten sam kod po prostu zmieniamy kilka niezbędnych pól. Kolor okna czy kursor po zostają bez zmian, więc po co pisać dwa razy ten sam kod? Funkcji RegisterClassEx nie będzie to przeszkadzać, gdyż klasy mają inne nazwy (pole lpszClassName).

Oczywiście funkcję ChildWindowProcedure trzeba zadeklarować. Deklaracja jest taka sama jak w przypadku "normalnej" procedury okna:

C/C++
// "normalna" procedura
LRESULT CALLBACK WindowProcedure( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam );
// procedura okna-dziecka
LRESULT CALLBACK ChildWindowProcedure( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam );

Pętla Komunikatów

W programie używającym MDI, trzeba także zmodyfikować pętlę komunikatów, tak by rozróżnić komunikaty idące do okna głównego od tych idących do okna-dziecka MDI. Tak więc standardową pętlę komunikatów zastępujemy tym:

C/C++
while( GetMessage( & messages, NULL, 0, 0 ) ) {
   
if( !TranslateMDISysAccel( hMDIClient, & messages ) ) {
       
TranslateMessage( & messages );
       
DispatchMessage( & messages );
   
}
}

Funkcja TranslateMDISysAccel tłumaczy (przekształca) komunikaty WM_KEYDOWN na WM_SYSCOMMAND i wysyła je do aktywnego okna-dziecka MDI. Jeżeli jest to komunikat do okna głównego ("normalnego") to funkcja zwraca FALSE.

Użyliśmy tutaj uchwytu hMDIClient. Dobrze będzie go mieć w zmiennej globalnej, dlatego na początku programu (poza funkcją WinMain dorzuć tą deklarację:

C/C++
HWND hMDIClient;

// to się przyda później:
#define ID_MDI_FIRSTCHILD  50000

Klient MDI

W poprzednim podrozdziale zadeklarowaliśmy uchwyt hMDIClient. Jak sama nazwa wskazuje, będzie w nim siedział klient MDI. Tylko co to za ustrojstwo jest? Klient MDI to po prostu kontrolka, wewnątrz której wyświetlają się okna-dzieci MDI. Jak praktycznie do wszystkiego w WinApi, przy tworzeniu klienta potrzebna będzie struktura. Nazwy się ona CLIENTCREATESTRUCT. Oto pola tej struktury, które będą nas interesować:

Pole Znaczenie
hWindowMenu uchwyt do menu, w którym będą przechowywane informacje o poszczególnych oknach-dzieciach MDI. Może być NULL
idFirstChild ID pierwszego okna-dziecka MDI. (tu podamy zdefiniowaną wcześniej stałą ID_MDI_FIRSTCHILD

Ponieważ klient jest zwyczajną kontrolką, tworzymy go funkcją CreateWindowEx. Jako jej ostatni parametr podajemy wskaźnik do struktury CLIENTCREATESTRUCT. Poniższy kod należy umieścić po instrukcji tworzącej główne okno:

C/C++
CLIENTCREATESTRUCT ccs;

// na razie obejdziemy się bez menu
ccs.hWindowMenu = NULL;
ccs.idFirstChild = ID_MDI_FIRSTCHILD;

hMDIClient = CreateWindowEx( 0, "MDICLIENT",( LPCTSTR ) NULL,
WS_CHILD | WS_CLIPCHILDREN | WS_VSCROLL | WS_HSCROLL,
5, 5, 640, 480, hwnd,( HMENU ) 0xCAC, hInstance,( LPSTR ) & ccs );

ShowWindow( hMDIClient, SW_SHOW );

Omówmy ten kod po kolei. Najpierw definiujemy nowy obiekt struktury CLIENTCREATESTRUCT. Potem przypisujemy polom wartości. Do pola hWindowMenu podajemy na razie NULL. Menu w MDI zajmiemy się w dalszej części kursu. Do pola idFirstChild wstawiamy zdefiniowaną wcześniej stałą ID_MDI_FIRSTCHILD. To właśnie od tej liczby włącznie Windows będzie nadawał ID kolejnym oknom-dzieciom MDI. Następnie tworzymy klienta MDI. Nazwa klasy to MDICLIENTM. Dorzuciliśmy flagę WS_CLIPCHILDREN, która uniemożliwi zamalowywanie okien-dzieci przez okno główne. Jako ostatni argument podajemy wskaźnik do struktury CLIENTCREATESTRUCT. Na koniec pokazujemy klienta MDI funkcją ShowWindow.

Procedura zdarzeniowa okna-dziecka

Ważną rzeczą, na którą trzeba zwrócić uwagę pisząc aplikacje MDI jest właśnie procedura zdarzeniowa okna-dziecka (u nas ChildWindowProcedure). Różni się ona nieco od procedury zdarzeniowej "normalnego" okna. Przede wszystkim, komunikaty, z których nie korzystamy odsyłamy za pomocą funkcji DefMDIChildProc. Wywołujemy ją z takimi samymi argumentami jak DefWindowProc - różnica praktycznie tylko w nazwie.

Jak zwykle Microsoft zadbał o to by wkurzyć programistów - i oto mamy: W procedurze zdarzeniowej okna-dziecka jeżeli chcemy obsłużyć komunikat WM_CHILDACTIVATE (jest on wysyłany gdy okno-dziecko dostanie focusa, gdy jest aktywowane, przenoszone lub zmieniamy jego rozmiar) to musimy go i tak zwrócić funkcją DefMDIChildProc do systemu. Jeżeli tego nie zrobimy, okienka nie będą mogły mieć focusa.

C/C++
LRESULT CALLBACK ChildWindowProcedure( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) {
   
switch( message ) {
       
       
// tu możesz dać różne case'y
       
       
default:
       
return DefMDIChildProc( hwnd, message, wParam, lParam );
   
}
   
return 0;
}

Procedura zdarzeniowa okna głównego

W procedurze zdarzeniowej okna głównego też zachodzą zmiany. Zamiast funkcji DefWindowProc do zwracania nieobsłużonych komunikatów używamy funkcji DefFrameProc w taki sposób:

C/C++
DefFrameProc( hwnd, hMDIClient, message, wParam, lParam );

Jest jeszcze jeden szkopuł. Mianowicie gdy używamy funkcji DefFrameProc przy próbie zmiany rozmiaru okna, klient MDI jest rozciągany na całe okno. Czasem jest to sytuacja pożądana, czasem nie. Na szczęście można łatwo rozwiązać ten problem, dodając obsługę komunikatu WM_SIZE (nawet pustą). W przykładzie który zostanie omówiony w tym artykule nie będziemy potrzebować rozciągać klienta MDI na cały ekran, dlatego zastosujemy wspomniany trick.

Gotowy Szablon

Najwyższy czas poskładać nasz kod do kupy. Można tego używać jako szablonu, do tworzenia własnych aplikacji MDI:

C/C++
#include <windows.h>

#define ID_MDI_FIRSTCHILD  50000

LRESULT CALLBACK WindowProcedure( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam );
LRESULT CALLBACK ChildWindowProcedure( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam );

char szClassName[ ] = "KlasaOkna";
char szChildName[ ] = "KlasaOknaDziecka";

HWND hMDIClient;

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) {
   
   
HWND hwnd;
   
MSG messages;
   
WNDCLASSEX wincl;
   
   
// Klasa okna głównego
   
   
wincl.hInstance = hInstance;
   
wincl.lpszClassName = szClassName;
   
wincl.lpfnWndProc = WindowProcedure;
   
wincl.style = 0;
   
wincl.cbSize = sizeof( WNDCLASSEX );
   
wincl.hIcon = LoadIcon( hInstance, IDI_APPLICATION );
   
wincl.hIconSm = LoadIcon( hInstance, IDI_APPLICATION );
   
wincl.hCursor = LoadCursor( NULL, IDC_ARROW );
   
wincl.lpszMenuName = NULL;
   
wincl.cbClsExtra = 0;
   
wincl.cbWndExtra = 0;
   
wincl.hbrBackground =( HBRUSH ) COLOR_WINDOW;
   
   
if( !RegisterClassEx( & wincl ) )
       
 return FALSE;
   
   
// Klasa okna dziecka
   
   
wincl.lpfnWndProc = ChildWindowProcedure;
   
wincl.lpszMenuName =( LPCTSTR ) NULL;
   
wincl.lpszClassName = "KlasaOknaDziecka";
   
   
if( !RegisterClassEx( & wincl ) )
       
 return FALSE;
   
   
hwnd = CreateWindowEx(
   
0,
   
szClassName,
   
"Aplikacja MDI",
   
WS_OVERLAPPEDWINDOW,
   
CW_USEDEFAULT,
   
CW_USEDEFAULT,
   
400,
   
300,
   
NULL,
   
NULL,
   
hInstance,
   
NULL
    );
   
   
ShowWindow( hwnd, nCmdShow );
   
   
CLIENTCREATESTRUCT ccs;
   
   
ccs.hWindowMenu = NULL;
   
ccs.idFirstChild = ID_MDI_FIRSTCHILD;
   
   
hMDIClient = CreateWindowEx( 0, "MDICLIENT",( LPCTSTR ) NULL,
   
WS_CHILD | WS_CLIPCHILDREN | WS_VSCROLL | WS_HSCROLL,
   
5, 5, 400, 300, hwnd,( HMENU ) 0xCAC, hInstance,( LPSTR ) & ccs );
   
   
ShowWindow( hMDIClient, SW_SHOW );
   
   
while( GetMessage( & messages, NULL, 0, 0 ) ) {
       
if( !TranslateMDISysAccel( hMDIClient, & messages ) ) {
           
TranslateMessage( & messages );
           
DispatchMessage( & messages );
       
}
    }
   
   
return messages.wParam;
   
}

LRESULT CALLBACK WindowProcedure( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) {
   
switch( message ) {
   
case WM_DESTROY:
       
PostQuitMessage( 0 );
       
break;
   
case WM_SIZE:
       
       
break;
   
default:
       
return DefFrameProc( hwnd, hMDIClient, message, wParam, lParam );
   
}
   
return 0;
}

LRESULT CALLBACK ChildWindowProcedure( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) {
   
switch( message ) {
       
       
// tu możesz dać różne case'y
       
       
default:
       
return DefMDIChildProc( hwnd, message, wParam, lParam );
   
}
   
return 0;
}

Dodawanie okienek

W poprzednim rozdziale dużo się napracowaliśmy, a osiągnęliśmy niewiele (jak to w WinApi ;-)). Otrzymaliśmy zaledwie pusty obszar klienta MDI. Nadszedł czas, by do zmienić. Zajmiemy się dodawaniem okienek. Żeby nie robić tego na bezdurno, przygotujemy prosty edytor plików tekstowych wykorzystujący MDI. Na początku dorzucimy kilka stałych na początku programu:

C/C++
HWND hMDIClient;

HWND hNew, hOpen, hSave, hMDIEdit; // to dodaliśmy

#define ID_MDI_FIRSTCHILD  50000
#define ID_NEW 30222      
//
#define ID_OPEN 30223     //
#define ID_SAVE 30224     //
#define ID_MDI_EDIT 2000  // to też dodaliśmy

Mamy tam identyfikatory przycisków nowy, otwórz, zapisz oraz pola do edycji tekstu. Teraz dodamy do naszego okna przycisk "Nowy":

C/C++
hNew = CreateWindowEx( 0, "BUTTON", "Nowy", WS_CHILD | WS_VISIBLE,
660, 5, 150, 30, hwnd,( HMENU ) ID_NEW, hInstance, NULL );

Nowe okno-dziecko MDI

Nadszedł czas na obsłużenie kliknięcia takiego przycisku. Teraz dochodzimy do sedna sprawy - dodawania nowych okienek MDI. Wykorzystamy do tego strukturę MDICREATESTRUCT, do której wpiszemy nieco informacji o naszym nowym okienku. Omówmy niektóre pola tej struktury:

Pole Znaczenie
szClass klasa okna MDI.
szTitle tytuł okna MDI.
hOwner uchwyt do instancji aplikacji.
x pozycja nowego okna od lewego boku klienta MDI.
y pozycja nowego okna od górnego boku klienta MDI.
cx szerokość okna MDI.
cy wysokość okna MDI.
style różne flagi. Zalecane jest podanie MDIS_ALLCHILDSTYLES].
lParam wskaźnik na dowolny obiekt (string, strukturę, klasę, funkcję) do użytku programisty.

W polach x, y, cx, cy można podać wartośc CW_USEDEFAULT. Wtedy Windows sam wybierze pozycje i rozmiar dla naszych okienek. W parametrze lParam możemy przechowywać wskaźnik na dowolne, wybrane przez nas dane.

Samo dodanie okienka odbywa się poprzez wysłanie komunikatu WM_MDICREATE do klienta MDI. Jako parametr lParam podajemy adres struktury MDICREATESTRUCT. Komunikat zwraca uchwyt do nowo utworzonego okna. Skoro już to wiemy, możemy obsłużyć kliknięcie przycisku "Nowy":

C/C++
case WM_COMMAND: {
   
switch( LOWORD( wParam ) ) {
   
case ID_NEW: {
           
MDICREATESTRUCT mcs;
           
HWND hChild;
           
           
mcs.szTitle = "Nowe okno MDI";
           
mcs.szClass = szChildName; // zakładam, że korzystasz z szablonu (punkt 1.5)
           
mcs.hOwner = GetModuleHandle( NULL ); // pobieramy instancję aplikacji
           
mcs.x = mcs.cx = CW_USEDEFAULT; // domyślne wymiary
           
mcs.y = mcs.cy = CW_USEDEFAULT; // i pozycja
           
mcs.style = MDIS_ALLCHILDSTYLES;
           
           
hChild =( HWND ) SendMessage( hMDIClient, WM_MDICREATE, 0,( LONG ) & mcs );
           
           
if( !hChild ) {
               
MessageBox( hwnd, "Nie udało się utworzyć okienka MDI!", "A to szkoda...",
               
MB_ICONEXCLAMATION | MB_OK );
           
}
        }
       
break;
   
}
}
break;

W powyższym kodzie tworzymy i wypełniamy strukturę MDICREATESTRUCT i wysyłamy komunikat do klienta MDI, by dodać okno. Funkcja GetModuleHandle służy do pobrania instancji aplikacji.

Najwyższy czas skompilować program, poklikać parę razy przycisk i podziwiać rezultat:

Nasza prosta aplikacja MDI (Windows Vista)
Nasza prosta aplikacja MDI (Windows Vista)

Wypełniamy kontrolkami okienka MDI

Prędzej czy później znudzą ci się puste okna MDI, które dotychczas udało nam się zrobić. Przydało by się wypełnić okno kontrolkami. Nasuwa się jednak pytanie: w którym miejscu programu to zrobić? Przecież nasze okno jest tworzone naprędce po kliknięciu przycisku. Odpowiedź brzmi: W procedurze zdarzeniowej okna-dziecka MDI! A konkretniej, należy obsłużyć komunikat WM_CREATE. Jest on wysyłany, gdy okno jest tworzone. Tego właśnie szukaliśmy! Bierzmy się wiec do roboty.

Do naszego okna dodamy na razie dwie kontrolki:

  • EDIT do wpisywania tekstu
  • BUTTON do zapisywania tekstu do pliku

Dodawanie kontrolek w procedurze zdarzeniowej nie różni się praktycznie niczym od tych dodawanych w WinMain. No może poza jednym. Jeżeli nie mamy gdzieś globalnego uchwytu do instancji naszego programu, to musimy skorzystać z funkcji GetModuleHandle. Możemy teraz przystąpić do kodzenia. Procedura zdarzeniowa okna-dziecka MDI będzie wyglądać teraz tak:

C/C++
LRESULT CALLBACK ChildWindowProcedure( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) {
   
switch( message ) {
   
case WM_CREATE: {
           
// przypisujemy do globalnych uchwytów
           
hMDIEdit = CreateWindowEx( WS_EX_CLIENTEDGE, "EDIT", NULL, WS_CHILD | WS_VISIBLE |
           
WS_BORDER | WS_VSCROLL | ES_MULTILINE | ES_AUTOVSCROLL, 0, 0, 400, 150,
           
hwnd,( HMENU ) ID_MDI_EDIT, GetModuleHandle( NULL ), NULL );
           
hSave = CreateWindowEx( 0, "BUTTON", "Zapisz", WS_CHILD | WS_VISIBLE,
           
5, 160, 150, 30, hwnd,( HMENU ) ID_SAVE, GetModuleHandle( NULL ), NULL );
           
           
SetFocus( GetDlgItem( hwnd, ID_MDI_EDIT ) );
       
}
       
break;
       
default:
       
return DefMDIChildProc( hwnd, message, wParam, lParam );
   
}
   
return 0;
}

W powyższym kodzie mamy zdefiniowaną obsługę komunikatu WM_CREATE. Dodajemy tam dwie kontrolki: EDIT'a i przycisk. Następnie ręcznie ustawiamy focusa na edicie. Użyliśmy funkcji SetFocus. Użyliśmy też funkcji GetDlgItem do pobrania naszego EDIT'a. Dlaczego? Ano dlatego, żeby odróżnić go od innych EDIT'ów z innych okien-dzieci MDI. Zauważ, że pierwszym argumentem tej funkcji jest uchwyt do okna (w tym przypadku okna-dziecka MDI).

No to teraz kompilujemy nasz program i cieszymy się naszym nowym dziełem:

Aplikacja MDI z polem tekstowym i przyciskiem (Windows Vista)
Aplikacja MDI z polem tekstowym i przyciskiem (Windows Vista)

Komunikaty od okien-dzieci MDI

Najwyższy czas sprawić, by przycisk zapisz zapisywał plik. Jeżeli nie przeczytałeś jeszcze o » Biblioteki C++» Kurs WinAPI, C++» Podstawydialogach zapisu pliku lekcja proponuje zrobić to teraz. Wracając do naszego problemu - komunikaty od kontrolek w oknach MDI wysyłane są do ich procedury zdarzeniowej, tak jak w przypadku "normalnych" okien. W takim razie do naszej procedury dialogowej okna-dziecka MDI dodajemy taki case:

C/C++
case WM_COMMAND: {
   
switch( LOWORD( wParam ) ) {
   
case ID_SAVE:
       
OPENFILENAME ofn;
       
char szNazwaPliku[ MAX_PATH ];
       
       
ZeroMemory( & ofn, sizeof( OPENFILENAME ) );
       
ofn.lStructSize = sizeof( ofn );
       
ofn.hwndOwner = HWND_DESKTOP;
       
ofn.lpstrFilter = "Pliki tekstowe (*.txt)\0*.txt\0Wszystkie pliki (*.*)\0*.*\0";
       
ofn.lpstrFile = szNazwaPliku;
       
ofn.nMaxFile = MAX_PATH;
       
ofn.lpstrDefExt = "txt";
       
ofn.Flags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_HIDEREADONLY |
       
OFN_OVERWRITEPROMPT;
       
       
if( GetSaveFileName( & ofn ) ) {
           
           
DWORD dlugosc = GetWindowTextLength( GetDlgItem( hwnd, ID_MDI_EDIT ) );
           
           
if( !dlugosc ) break;
           
           
LPSTR szTextZEdita =( LPSTR ) GlobalAlloc( GPTR, dlugosc + 1 );
           
GetWindowText( GetDlgItem( hwnd, ID_MDI_EDIT ), szTextZEdita, dlugosc + 1 );
           
           
HANDLE hFile;
           
bool bSuccess = false;
           
           
hFile = CreateFile( szNazwaPliku, GENERIC_WRITE, 0, NULL,
           
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0 );
           
           
if( hFile != INVALID_HANDLE_VALUE ) {
               
DWORD dwWritten;
               
if( WriteFile( hFile, szTextZEdita, dlugosc, & dwWritten, NULL ) )
                   
 bSuccess = true;
               
           
}
           
           
CloseHandle( hFile );
           
GlobalFree( szTextZEdita );
           
           
if( !bSuccess ) {
               
MessageBox( hwnd, "Nie udało się zapisać pliku!",
               
"A to szkoda...", MB_ICONEXCLAMATION | MB_OK );
           
}
           
        }
       
       
break;
   
}
}
break;

Przede wszystkim dodaliśmy instrukcje dla przycisku "Zapisz" (ID_SAVE). Korzystamy ze struktury OPENFILENAME i funkcji GetSaveFileName by użyć dialogu zapisu pliku. Szczegółowo opisane jest to » Biblioteki C++» Kurs WinAPI, C++» Podstawytutaj lekcja. Następnie sprawdzamy długość tekstu z pola tekstowego (funkcja GetWindowTextLength) i pobieramy ten tekst (funkcja GetWindowText). Szczegółowo opisane jest to » Biblioteki C++» Kurs WinAPI, C++» Podstawytutaj lekcja. Potem otwieramy plik funkcją CreateFile i zapisujemy do niego zawartość EDIT'a funkcją WriteFile. Szczegółowo opisane jest to » Biblioteki C++» Kurs WinAPI, C++» Podstawytutaj lekcja.

Układanie okien

W kontrolce klienta MDI okienka-dzieci można ustawiać (a to ci dopiero nowość ;-)). Wystarczy wziąć naszego gryzonia i trochę się naklikać. Co ciekawe Windows umożliwia samopoukładanie się okien MDI. Wszystko sprowadza się do wysłania odpowiednich komunikatów do klienta MDI. Oto kilka najczęściej używanych ustawień:

Kaskada

Aby poukładać okna w kaskadę należy wysłać komunikat  do klienta MDI:

C/C++
SendMessage( hMDIClient, WM_MDICASCADE, 0, 0 );

Oto jak wygląda taka kaskada:

Układ kaskadowy w MDI (Windows Vista)
Układ kaskadowy w MDI (Windows Vista)

Aranżuj ikony

Nazwa zupełnie nietrafiona. Ta opcja spowoduje poukładanie zminimalizowanych okienek, jeżeli wystąpiły jakieś "dziury". Aby zaaranżować ikony należy wysłać do klienta MDI komunikat WM_MDIICONARRANGE:

C/C++
SendMessage( hMDIClient, WM_MDIICONARRANGE, 0, 0 );

Dopasuj w poziomie

Po angielsku nazywa się to "tile" co oznacza w dosłownym tłumaczeniu płytkę ceramiczną. Słowo "dopasowanie" lepiej tu pasuje. Żeby dopasować okienka w poziomie należy wysłać do klienta MDI komunikat WM_MDITILE, a w parametrze wParam podać wartość MDITILE_HORIZONTAL:

C/C++
SendMessage( hMDIClient, WM_MDITILE, MDITILE_HORIZONTAL, 0 );

Oto jak wyglądają tak dopasowane okna:

Dopasowanie w poziomie w MDI (Windows Vista)
Dopasowanie w poziomie w MDI (Windows Vista)

Dopasuj w pionie

Brat bliźniak poprzedniego. Gdy jako parametr wParam podamy wartość MDITILE_VERTICAL:

C/C++
SendMessage( hMDIClient, WM_MDITILE, MDITILE_VERTICAL, 0 );

Osiągniemy taki efekt

Dopasowanie w pionie w MDI (Windows Vista)
Dopasowanie w pionie w MDI (Windows Vista)

Wiele okien - różne klasy

W naszym programie używaliśmy takie same okienka MDI. Nic nie stoi jednak na przeszkodzie, by tworzyć różne okna czyli z różnych klas okna. Posłuży nam do tego prosty przykład: zrobimy okno MDI z narzędziami, w którym będą zawarte opcje układania okien z poprzedniego rozdziału. Tak więc do dzieła! Zaczniemy od dorzucenia dodatkowej klasy okna na nasze okno:

C/C++
WNDCLASSEX wincl;

// tutaj inne klasy  

// nasza klasa
wincl.lpfnWndProc = ToolChildWindowProcedure;
wincl.lpszMenuName =( LPCTSTR ) NULL;
wincl.lpszClassName = "KlasaOknaZNarzedziami";

if( !RegisterClassEx( & wincl ) )
   
 return FALSE;

Nasze okno będzie mieć swoją procedurę zdarzeniową, więc dorzućmy jej deklarację na początku programu:

C/C++
LRESULT CALLBACK ToolChildWindowProcedure( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam );

#define ID_CASCADE 32888
#define ID_TILEHOR 32889
#define ID_TILEVER 32887

Przy okazji dodaliśmy identyfikatory przycisków do naszego okienka. Teraz możemy napisać jego procedurę zdarzeniową. Mamy tu zebraną większość rzeczy, których dotychczas się nauczyliśmy:

C/C++
LRESULT CALLBACK ToolChildWindowProcedure( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) {
   
switch( message ) {
   
case WM_CREATE: {
           
CreateWindowEx( 0, "BUTTON", "Kaskada", WS_CHILD | WS_VISIBLE,
           
5, 5, 150, 30, hwnd,( HMENU ) ID_CASCADE, GetModuleHandle( NULL ), NULL );
           
CreateWindowEx( 0, "BUTTON", "Dopasuj poziomo", WS_CHILD | WS_VISIBLE,
           
5, 40, 150, 30, hwnd,( HMENU ) ID_TILEHOR, GetModuleHandle( NULL ), NULL );
           
CreateWindowEx( 0, "BUTTON", "Dopasuj pionowo", WS_CHILD | WS_VISIBLE,
           
5, 75, 150, 30, hwnd,( HMENU ) ID_TILEVER, GetModuleHandle( NULL ), NULL );
       
}
       
break;
   
case WM_COMMAND: {
           
switch( LOWORD( wParam ) ) {
           
case ID_CASCADE:
               
SendMessage( hMDIClient, WM_MDICASCADE, 0, 0 );
               
break;
           
case ID_TILEHOR:
               
SendMessage( hMDIClient, WM_MDITILE, MDITILE_HORIZONTAL, 0 );
               
break;
           
case ID_TILEVER:
               
SendMessage( hMDIClient, WM_MDITILE, MDITILE_VERTICAL, 0 );
               
break;
           
}
        }
       
break;
       
default:
       
return DefMDIChildProc( hwnd, message, wParam, lParam );
   
}
   
return 0;
}

Kodu nie będę tłumaczył, bo praktycznie nie zawiera niczego nowego. Najwyższy czas dodać nasze okno do klienta MDI. Ten kod umieszczamy za instrukcją tworzącą klienta MDI:

C/C++
MDICREATESTRUCT mcs;
HWND hChild;

mcs.szTitle = "Narzędzia";
mcs.szClass = "KlasaOknaZNarzedziami"; // tu jest sedno sprawy
mcs.hOwner = GetModuleHandle( NULL );
mcs.x = mcs.cx = CW_USEDEFAULT;
mcs.y = mcs.cy = CW_USEDEFAULT;
mcs.style = MDIS_ALLCHILDSTYLES;

hChild =( HWND ) SendMessage( hMDIClient, WM_MDICREATE, 0,( LONG ) & mcs );

if( !hChild ) {
   
MessageBox( hwnd, "Nie udało się utworzyć okienka z narzędziami!",
   
"A to szkoda...", MB_ICONEXCLAMATION | MB_OK );
}

W polu szClass struktury MDICREATESTRUCT wprowadzamy nazwę klasy. Nie musi ona być taka sama dla wszystkich okien. W naszym przypadku, to "KlasaOknaZNarzedziami". Wszystko inne wygląda tak samo jak w przypadku tworzenia innych okien MDI.

Teraz kompilujemy nasz kod i podziwiamy rezultaty:
Aplikacja MDI z wieloma oknami różnych klas (Windows Vista)
Aplikacja MDI z wieloma oknami różnych klas (Windows Vista)

Menu a MDI

W aplikacji MDI Windows może za nas odwalić część brudnej roboty. Może dodać obsługę okienek MDI przez menu. Najpierw jednak trzeba takie menu mieć. Zróbmy więc proste menu:

C/C++
200 MENU {
   
POPUP "&Program" {
       
MENUITEM "&Nowe okno", 30222
   
}
}

Jak widać jest tam tylko jedna pozycja obsługująca dodawanie nowego okna MDI. Oczywiście nasze menu trzeba jeszcze dodać do okna głównego. Jeżeli tego nie umiesz, przeczytaj artykuł o » Biblioteki C++» Kurs WinAPI, C++» Podstawymenu lekcja.

Zakładam, że menu już jest dodane do okna. Teraz wróćmy na chwilę do struktury CLIENTCREATESTRUCT. Mieliśmy w niej pole hWindowMenu do którego mogliśmy przypisać uchwyt do menu. Na początku wpisaliśmy tam NULL. Teraz wpiszemy tam to:

C/C++
ccs.hWindowMenu = GetSubMenu( GetMenu( hwnd ), 0 );

Do poprawnego działania pozycji z menu potrzebne jest zmodyfikowane obsługi komunikatu WM_COMMAND w oknie głównym w taki sposób:

C/C++
case WM_COMMAND: {
   
switch( LOWORD( wParam ) ) {
   
case ID_NEW:
       
// ...
       
break;
       
// ewentualne inne case'y
   
default: {
           
if( LOWORD( wParam ) >= ID_MDI_FIRSTCHILD ) {
               
DefFrameProc( hwnd, hMDIClient, message, wParam, lParam );
           
}
        }
    }
}
break;

Teraz już chyba wiesz po co trzeba było podać ID pierwszego okna przy tworzeniu klienta MDI. Właśnie komunikaty o tym ID (i wyższym dla kolejnych okien) są wysyłane, jeżeli wybierzemy pozycję z menu. Wtedy przekazujemy komunikat systemowi (funkcja DefFrameProc), który wybierze odpowiednie okno.

I to wszystko! Zobaczmy jeszcze jak nasze menu będzie wyglądać:

Aplikacja MDI wykorzystująca menu (Windows Vista)
Aplikacja MDI wykorzystująca menu (Windows Vista)
Poprzedni dokument Następny dokument
Haki Unicode w WinAPI