Rozdział XXXII - Interfejs MDI


Ta lekcja uczy tworzenia aplikacji MDI (Multi Document Interface - Interfejs Wielo Dokumentowy). W zasadzie nie jest to zbyt trudne.

 

 

Załaduj {ten przykład}

 

Teoria

Interfejs Wielo Dokumentowy (MDI) jest typem aplikacji, która obsługuje wiele dokumentów w tym samym czasie. Na pewno znasz Notatnik: jest to przykład Interfejsu Jedno Dokumentowego (SDI - Single Document Interface). Notatnik może jednocześnie obsługiwać tylko jeden dokument. Jeśli chcesz otworzyć następny, to najpierw musisz zamknąć poprzedni. Oczywiście nie jest to zbyt wygodne. Przeciwieństwem jest procesor tekstu Microsoft Word, który jednocześnie potrafi otworzyć dowolną liczbę dokumentów i pozwala użytkownikowi wybrać jeden z nich do pracy. Microsoft Word jest przykładem Interfejsu Wielo Dokumentowego.

Aplikacja MDI posiada kilka charakterystyk, które ją wyróżniają. Przedstawię niektóre z nich:

 

Główne okno zawierające okna potomne nazywane jest oknem ramowym (frame window). To na jego obszarze roboczym żyją okna potomne, stąd nazwa "rama". Ma ono nieco więcej do roboty od zwykłego okna, ponieważ musi obsłużyć współdziałanie okien potomnych.

Aby kontrolować dowolną liczbę okien potomnych (child windows) na swoim obszarze roboczym, potrzebujesz specjalnego okna zwanego oknem klienckim (client window). Możesz je potraktować jak przeźroczyste okno pokrywające całość obszaru roboczego okna ramowego. To okno klienckie jest prawdziwym oknem nadrzędnym dla wszystkich okien potomnych MDI.

 

Rysunek 1. Hierarchia aplikacji MDI

 

Tworzenie Okna Ramowego

Teraz zajmiemy się szczegółami. Na początek utworzymy okno ramowe. Tworzy się je w identyczny sposób jak każde okno przez wywołanie funkcji CreateWindowEx. Istnieją jednak dwie główne różnice w stosunku do zwykłych okien.

Pierwszą jest to, iż MUSISZ wywołać w swojej procedurze okna funkcję DefFrameProc zamiast DefWindowProc do obsługi wiadomości systemu Windows, których sam nie obsługujesz. W ten sposób zwalasz całą brudną robotę zarządzania aplikacją MDI na system Windows. Jeśli zapomnisz sobie o wywołaniu DefFrameProc, twoja aplikacja nie będzie posiadała cech aplikacji MDI. Funkcja DefFrameProc posiada następującą składnię:

 

DefFrameProc PROC hwndFrame:  DWORD, 
                  hwndClient: DWORD,
                  uMsg:       DWORD, 
                  wParam:     DWORD, 
                  lParam:     DWORD

 

Jeśli porównasz funkcję DefFrameProc z DefWindowProc, zauważysz, iż jedyną różnicą pomiędzy nimi jest to, iż funkcja DefFrameProc posiada 5 parametrów, natomiast DefWindowProc ma tylko 4. Dodatkowy parametr jest uchwytem okna klienckiego. Ten uchwyt jest konieczny, aby system Windows mógł wysyłać wiadomości związane z MDI do okna klienckiego.

Drugą różnicą jest to, iż w pętli wiadomości okna ramowego musisz wywołać funkcję TranslateMDISysAccel. Jest to niezbędne, jeśli chcesz, aby system Windows obsługiwał dla ciebie skróty klawiaturowe związane z MDI, takie jak Ctrl+F4, Ctrl+Tab. Posiada ona następującą składnię:

 

TranslateMDISysAccel PROTO hwndClient: DWORD,
                           lpMsg:      DWORD

 

Pierwszy parametr jest uchwytem okna klienckiego. Nie powinno cię to zaskoczyć, ponieważ jest ono oknem nadrzędnym dla wszystkich okien potomnych MDI. Drugi parametr jest adresem struktury MSG, którą wypełniasz wywołując funkcję GetMessage. Celem jest przesłanie tej struktury do okna klienckiego, aby mogło ono sprawdzić, czy struktura MSG nie zawiera wiadomości o naciśnięciu klawiszy związanych z obsługą MDI. Jeśli tak, obsłuży ono samodzielnie te zdarzenia i zwróci wartość różną od zera, w przeciwnym razie zwróci FALSE.

Podsumujmy następująco kroki niezbędne do utworzenia okna ramowego:

  1. Wypełnij jak zwykle strukturę WNDCLASSEX.
  2. Zarejestruj klasę okna ramowego wywołując RegisterClassEx.
  3. Utwórz okno ramowe wywołując CreateWindowEx.
  4. W pętli wiadomości wywołaj funkcję TranslateMDISysAccel.
  5. W procedurze okna przekazuj nie obsłużone wiadomości do DefFrameProc zamiast do DefWindowProc.

Tworzenie Okna Klienckiego

Mając już okno ramowe możemy przejść do utworzenia okna klienckiego. Klasa okna klienckiego jest już zarejestrowana w systemie Windows. Posiada ona nazwę "MDICLIENT". Do funkcji CreateWindowEx musisz również przekazać adres struktury CLIENTCREATESTRUCT. Struktura ta posiada następującą definicję:

 

CLIENTCREATESTRUCT STRUCT
    hWindowMenu  DD ?
    idFirstChild DD ?
CLIENTCREATESTRUCT ENDS
    .ELSEIF uMsg==WM_COMMAND
        .IF lParam==0  ;ta wiadomość pochodzi od menu
            mov eax, wParam
            .IF ax==IDM_CASCADE
                ...
            .ELSEIF ax==IDM_TILEVERT
                ...
            .ELSE
                INVOKE DefFrameProc, hwndFrame,
                                     hwndClient, uMsg,wParam, lParam
                ret
            .ENDIF
        .ENDIF

 

Zwykle ignorujemy wiadomości z nieobsługiwanych przypadków. Lecz dla MDI, jeśli zignorujesz je, gdy użytkownik klika nazwę okna potomnego MDI na liście okien, to wybrane okno nie stanie się aktywne. Musisz przekazać je do DefFrameProc, aby zostały poprawnie obsłużone.

Ostrożnie z wartością pola idFirstChild: nie należy używać wartości 0. Lista okien w takim przypadku zacznie się niepoprawnie zachowywać, tj. znaczek wyboru nie pojawi się przed nazwą pierwszego okna potomnego MDI, nawet jeśli będzie ono aktywne. Wybierz jakąś bezpieczną wartość, taką jak 100 lub więcej.

Wypełniwszy pola struktury CLIENTCREATESTRUCT, możesz utworzyć okno klienckie przez wywołanie CreateWindowEx z predefiniowaną nazwą klasy "MDICLIENT" i przekazanie w lParam adresu struktury CLIENTCREATESTRUCT. W parametrze hWndParent musisz również podać uchwyt do okna ramowego, aby system Windows znał relacje pomiędzy oknem ramowym a oknem klienckim. Powinieneś użyć następujących stylów: WS_CHILD, WS_VISIBLE u WS_CLIPCHILDREN. Jeśli zapomnisz o stylu WS_VISIBLE, nie zobaczysz okien potomnych MDI, nawet jeśli zostaną utworzone z powodzeniem.

Kroki tworzenia okna klienckiego są następujące:

  1. Pobierz uchwyt do podmenu, do którego ma zostać dołączona lista okien.
  2. Umieść wartość tego uchwytu menu wraz z wartością numeru ID pierwszego okna potomnego MDI w strukturze CLIENTCREATESTRUCT.
  3. Wywołaj CreateWindowEx z nazwą klasy "MDICLIENT" przekazując w lParam adres struktury CLIENTCREATESTRUCT.

Tworzenie Okna Potomnego MDI

Teraz masz już oba okna, ramowe i klienckie. Zostało przygotowane miejsce na utworzenie okna potomnego MDI. Można to zrobić na dwa sposoby.

.DATA?

mdicreate MDICREATESTRUCT <>
...

.CODE

    ...
    [wypełnij pola struktury mdicreate]
    ...
    INVOKE SendMessage, hwndClient, WM_MDICREATE, ADDR mdicreate, 0

Jeśli operacja się powiedzie, to funkcja SendMessage zwróci uchwyt do nowo utworzonego okna potomnego MDI. Jednakże nie musisz go zachowywać. Można ten uchwyt uzyskać innymi środkami, jeśli stanie się potrzebny. Struktura MDICREATESTRUCT posiada następującą definicję:

MDICREATESTRUCT STRUCT
    szClass DWORD ?
    szTitle DWORD ?
    hOwner  DWORD ?
    x       DWORD ?
    y       DWORD ?
    lx      DWORD ?
    ly      DWORD ?
    style   DWORD ?
    lParam  DWORD ?
MDICREATESTRUCT ENDS
CreateMDIWindow PROTO lpClassName:  DWORD
                      lpWindowName: DWORD
                      dwStyle:      DWORD
                      x:            DWORD
                      y:            DWORD
                      nWidth:       DWORD
                      nHeight:      DWORD
                      hWndParent:   DWORD
                      hInstance:    DWORD
                      lParam:       DWORD

 

Jeśli dokładnie przyjrzysz się tym parametrom, to odkryjesz, iż są one identyczne z polami struktury MDICREATESTRUCT z wyjątkiem hWndParent. Co istotne, jest to ten sam parametr, który występuje z wiadomością WM_MDICREATE. Struktura MDICREATESTRUCT nie posiada pola hWndParent, ponieważ i tak musisz przekazać całą strukturę do właściwego okna klienckiego za pomocą SendMessage.

W tym miejscu zastanawiasz się: której metody powinienem użyć? Czym się one różnią? Oto odpowiedź:

Metoda WM_MDICREATE może tworzyć jedynie okna potomne MDI w tym samym wątku jako od wywołujący. Na przykład, jeśli twoja aplikacja posiada dwa wątki, a pierwszy tworzy okno ramowe MDI, to jeśli drugi wątek chce utworzyć potomka MDI, musi to zrobić za pomocą wywołania CreateMDIChild: wysłanie wiadomości WM_MDICREATE do pierwszego wątku nie będzie działać. Jeśli twoja aplikacja ma tylko jeden wątek, możesz użyć dowolnej metody.

Nieco więcej szczegółów wymaga opis procedury okna w potomku MDI. Podobnie jak w przypadku okna ramowego, nie wolno wywoływać w niej DefWindowProc do przetworzenia nie obsługiwanych wiadomości, lecz DefMDIChildProc. Funkcja ta ma dokładnie takie same parametry co DefWindowProc. Oprócz wiadomości WM_MDICREATE istnieje wiele innych wiadomości związanych z oknami MDI. Wymieniam je poniżej:

WM_MDIACTIVATE

Ta wiadomość może zostać wysłana przez aplikację do okna klienckiego w celu aktywacji przez nie wybranego okna potomnego MDI. Gdy okno klienckie odbierze wiadomość, uaktywnia wybrane okno potomne MDI i wysyła wiadomość WM_MDIACTIVATE do okna potomnego dezaktywowanego i aktywowanego. Wiadomość ma dwukrotne zastosowanie: może być używana przez aplikację do aktywacji pożądanego okna potomnego i może być użyta przez samo okno potomne MDI jako wskaźnik jego aktywacji / dezaktywacji. Na przykład, jeśli każde okno MDI posiada inne menu, może wykorzystać tę sposobność do zmiany menu okna ramowego przy swojej aktywacji / dezaktywacji.

 

WM_MDICASCADE WM_MDITILE WM_MDIICONARRANGE

Wiadomości obsługi układu, aranżacji okien potomnych MDI. Ma przykład, jeśli chcesz, aby okna potomne MDI ułożyły się kaskadowo jedno za drugim, wyślij do ich okna klienckiego wiadomość WM_MDICASCADE.

 

WM_MDIDESTROY

Wyślij tę wiadomość do okna klienckiego w celu usunięcia okna potomnego MDI. Powinieneś korzystać z tej wiadomości zamiast wywoływania DestroyWindow, ponieważ w przypadku, gdy usuwane okno jest zmaksymalizowane, wiadomość ta przywróci oryginalny tytuł oknu ramowemu. Jeśli zastosujesz DestroyWindow, tytuł okna ramowego nie będzie odtworzony.

 

WM_MDIGETACTIVE

Wyślij tę wiadomość, aby otrzymać uchwyt aktualnie aktywnego okna potomnego MDI.

 

WM_MDIMAXIMIZE WM_MDIRESTORE

Wiadomość WM_MDIMAXIMIZE maksymalizuje wybrane okno potomne MDI, a WM_MDIRESTORE przywraca je do stanu poprzedniego. Dla tych operacji zawsze wykorzystuj te wiadomości. Jeśli użyjesz funkcji ShowWindow z SW_MAXIMIZE, okno potomne MDI zostanie poprawnie zmaksymalizowane, lecz problemy pojawią się przy próbie powrotu do stanu poprzedniego. Z minimalizacją okna potomnego MDI przy pomocy ShowWindow nie ma jednakże problemów.

 

WM_MDINEXT

Wyślij tę wiadomość do okna klienckiego, aby aktywować następne lub poprzednie okno potomne MDI zgodnie z zawartością parametrów wParam i lParam.

 

WM_MDIREFRESHMENU

Wyślij tę wiadomość do okna klienckiego w celu odświeżenia menu okna ramowego. Uwaga: po wysłaniu tej wiadomości musisz wywołać funkcję DrawMenuBar, aby odświeżyć pasek menu.

 

WM_MDISETMENU

Wyślij tę wiadomość do okna klienckiego w celu zastąpienia całego menu okna ramowego lub tylko podmenu okna. Musisz użyć tej wiadomości zamiast SetMenu. Po wysłaniu wiadomości musisz wywołać DrawMenuBar, aby odświeżyć pasek menu. Zwykle stosujemy tę wiadomość, gdy aktywne okno potomne MDI posiada swoje własne menu i chcesz nim zastąpić menu okna ramowego.

 

Podsumujmy teraz kroki tworzenia aplikacji MDI:

  1. Zarejestruj klasy okien, zarówno klasę okna ramowego jak i klasę okna potomnego MDI.
  2. Utwórz okno ramowe za pomocą CreateWindowEx.
  3. Wewnątrz pętli wiadomości wywołuj funkcję TranslateMDISysAccel, aby obsługiwać wiadomości związane z klawiszami skrótów MDI.
  4. Wewnątrz procedury okna w oknie ramowym wywołuj DefFrameProc do obsługi WSZYSTKICH wiadomości, których sam nie obsługujesz w swoim kodzie.
  5. Utwórz okno klienckie wywołując CreateWindowEx z nazwą predefiniowanej klasy okna "MDICLIENT" i przekazując w lParam adres struktury CLIENTCREATESTRUCT. Typowo okno to powstaje w sekcji obsługi wiadomości WM_CREATE w procedurze okna ramowego.
  6. Okna potomne MDI tworzysz wysyłając do okna klienckiego wiadomości WM_MDICREATE lub wywołując funkcję CreateMDIWindow.
  7. Wewnątrz procedury okna w oknie potomnym MDI przekazuj nie obsłużone wiadomości do DefMDIChildProc.
  8. Stosuj wersje MDI wiadomości, jeśli są dostępne. Na przykład, użyj WM_MDIDESTROY zamiast wywołania funkcji DestroyWindow.

Przykład

Plik MDI.ASM

 

.386

.MODEL FLAT, STDCALL

OPTION CASEMAP:NONE

INCLUDE    \masm32\include\windows.inc
INCLUDE    \masm32\include\user32.inc
INCLUDE    \masm32\include\kernel32.inc
INCLUDELIB \masm32\lib\user32.lib
INCLUDELIB \masm32\lib\kernel32.lib

WinMain PROTO :DWORD, :DWORD, :DWORD, :DWORD

.CONST

IDR_MAINMENU  EQU   101
IDR_CHILDMENU EQU   102
IDM_EXIT      EQU 40001
IDM_TILEHORZ  EQU 40002
IDM_TILEVERT  EQU 40003
IDM_CASCADE   EQU 40004
IDM_NEW       EQU 40005
IDM_CLOSE     EQU 40006

.DATA

ClassName          DB "MDIASMClass", 0
MDIClientName      DB "MDICLIENT", 0
MDIChildClassName  DB "Win32asmMDIChild", 0
MDIChildTitle      DB "Potomek MDI", 0
AppName            DB "Przykład MDI w Win32asm", 0
ClosePromptMessage DB "Jesteś pewny, że chcesz zamknąć to okno?", 0

.DATA?

hInstance  DD ?
hMainMenu  DD ?
hwndClient DD ?
hChildMenu DD ?
mdicreate  MDICREATESTRUCT <>
hwndFrame  DD ?

.CODE

start:

    INVOKE GetModuleHandle, NULL
    mov    hInstance, eax
    INVOKE WinMain, hInstance, NULL, NULL, SW_SHOWDEFAULT
    INVOKE ExitProcess, eax

WinMain PROC hInst:     HINSTANCE,
             hPrevInst: HINSTANCE,
             CmdLine:   LPSTR,
             CmdShow:   DWORD

LOCAL wc  : WNDCLASSEX
LOCAL msg : MSG

;=================================
; Rejestrujemy klasę okna ramowego
;=================================

    mov    wc.cbSize, SIZEOF WNDCLASSEX
    mov    wc.style, CS_HREDRAW OR CS_VREDRAW
    mov    wc.lpfnWndProc, OFFSET WndProc
    mov    wc.cbClsExtra, NULL
    mov    wc.cbWndExtra, NULL
    push   hInstance
    pop    wc.hInstance
    mov    wc.hbrBackground, COLOR_APPWORKSPACE
    mov    wc.lpszMenuName, IDR_MAINMENU
    mov    wc.lpszClassName, OFFSET ClassName
    INVOKE LoadIcon, NULL, IDI_APPLICATION
    mov    wc.hIcon, eax
    mov    wc.hIconSm, eax
    INVOKE LoadCursor, NULL, IDC_ARROW
    mov    wc.hCursor, eax
    INVOKE RegisterClassEx, ADDR wc

;======================================
; Rejestrujemy klasę okna potomnego MDI
;======================================

    mov    wc.lpfnWndProc, OFFSET ChildProc
    mov    wc.hbrBackground, COLOR_WINDOW+1
    mov    wc.lpszClassName, OFFSET MDIChildClassName
    INVOKE RegisterClassEx, ADDR wc
    INVOKE CreateWindowEx, NULL, ADDR ClassName, ADDR AppName,
                           WS_OVERLAPPEDWINDOW OR WS_CLIPCHILDREN,
                           CW_USEDEFAULT, CW_USEDEFAULT,
                           CW_USEDEFAULT, CW_USEDEFAULT,
                           NULL, 0, hInst, NULL
    mov    hwndFrame, eax
    INVOKE LoadMenu, hInstance, IDR_CHILDMENU
    mov    hChildMenu, eax
    INVOKE ShowWindow, hwndFrame, SW_SHOWNORMAL
    INVOKE UpdateWindow, hwndFrame

    .WHILE TRUE
        INVOKE GetMessage, ADDR msg, NULL, 0, 0
        .BREAK .IF (!eax)
        INVOKE TranslateMDISysAccel, hwndClient, ADDR msg
        .IF eax==0
            INVOKE TranslateMessage, ADDR msg
            INVOKE DispatchMessage, ADDR msg
        .ENDIF
    .ENDW

    INVOKE DestroyMenu, hChildMenu
    mov    eax, msg.wParam
    ret

WinMain ENDP

WndProc PROC hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

LOCAL ClientStruct: CLIENTCREATESTRUCT

    .IF uMsg==WM_CREATE
        INVOKE GetMenu, hWnd
        mov    hMainMenu, eax
        INVOKE GetSubMenu, eax, 1
        mov    ClientStruct.hWindowMenu, eax
        mov    ClientStruct.idFirstChild, 100
        INVOKE CreateWindowEx, NULL, ADDR MDIClientName, NULL,
                               WS_CHILD OR WS_VISIBLE OR WS_CLIPCHILDREN,
                               CW_USEDEFAULT, CW_USEDEFAULT,
                               CW_USEDEFAULT, CW_USEDEFAULT,
                               hWnd, NULL, hInstance, ADDR ClientStruct
        mov  hwndClient, eax

;=====================================
; Inicjujemy strukturę MDICREATESTRUCT
;=====================================

        mov  mdicreate.szClass, OFFSET MDIChildClassName
        mov  mdicreate.szTitle, OFFSET MDIChildTitle
        push hInstance
        pop  mdicreate.hOwner
        mov  eax, CW_USEDEFAULT
        mov  mdicreate.x, eax
        mov  mdicreate.y, eax
        mov  mdicreate.lx, eax
        mov  mdicreate.ly, eax
    .ELSEIF uMsg==WM_COMMAND
        .IF lParam==0
            mov eax, wParam
            .IF ax==IDM_EXIT
                INVOKE SendMessage, hWnd, WM_CLOSE, 0, 0
            .ELSEIF ax==IDM_TILEHORZ
                INVOKE SendMessage, hwndClient, WM_MDITILE,
                                    MDITILE_HORIZONTAL, 0
            .ELSEIF ax==IDM_TILEVERT
                INVOKE SendMessage, hwndClient, WM_MDITILE,
                                    MDITILE_VERTICAL, 0
               .ELSEIF ax==IDM_CASCADE
                INVOKE SendMessage, hwndClient, WM_MDICASCADE,
                                    MDITILE_SKIPDISABLED, 0
            .ELSEIF ax==IDM_NEW
                INVOKE SendMessage, hwndClient, WM_MDICREATE,
                                    0, ADDR mdicreate
            .ELSEIF ax==IDM_CLOSE
                INVOKE SendMessage, hwndClient, WM_MDIGETACTIVE, 0, 0
                INVOKE SendMessage, eax, WM_CLOSE, 0, 0
            .ELSE
                INVOKE DefFrameProc, hWnd, hwndClient, uMsg, wParam, lParam
                ret
            .ENDIF
        .ENDIF
    .ELSEIF uMsg==WM_DESTROY
        INVOKE PostQuitMessage, NULL
    .ELSE
        INVOKE DefFrameProc, hWnd, hwndClient, uMsg, wParam, lParam
        ret
    .ENDIF

    xor eax, eax
    ret

WndProc ENDP

ChildProc PROC hChild:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD

    .IF uMsg==WM_MDIACTIVATE
        mov eax, lParam
        .IF eax==hChild
            INVOKE GetSubMenu, hChildMenu, 1
            mov    edx, eax
            INVOKE SendMessage, hwndClient, WM_MDISETMENU, hChildMenu, edx
        .ELSE
            INVOKE GetSubMenu, hMainMenu, 1
            mov    edx, eax
            INVOKE SendMessage, hwndClient, WM_MDISETMENU, hMainMenu, edx
        .ENDIF
        INVOKE DrawMenuBar, hwndFrame
    .ELSEIF uMsg==WM_CLOSE
        INVOKE MessageBox, hChild, ADDR ClosePromptMessage,
                           ADDR AppName, MB_YESNO
        .IF eax==IDYES
            INVOKE SendMessage, hwndClient, WM_MDIDESTROY, hChild, 0
        .ENDIF       
    .ELSE
        INVOKE DefMDIChildProc, hChild, uMsg, wParam, lParam
        ret
    .ENDIF

    xor eax, eax
    ret

ChildProc ENDP

END start

 

Plik MDI.RC

 

#include "RESOURCE.H"

#define IDR_MAINMENU    101
#define IDR_CHILDMENU   102
#define IDM_EXIT      40001
#define IDM_TILEHORZ  40002
#define IDM_TILEVERT  40003
#define IDM_CASCADE   40004
#define IDM_NEW       40005
#define IDM_CLOSE     40006

IDR_MAINMENU MENU DISCARDABLE 
{
    POPUP "&Plik"
    {
        MENUITEM "&Nowy", IDM_NEW
        MENUITEM "&Koniec", IDM_EXIT
    }
    POPUP "&Okno"   
    {
    	MENUITEM "Ułóż przy sobie poziomo", IDM_TILEHORZ
    	MENUITEM "Ułóż przy sobie pionowo", IDM_TILEVERT
    	MENUITEM "Ułóż jedno na drugim", IDM_CASCADE
    }
}

IDR_CHILDMENU MENU DISCARDABLE 
{
    POPUP "&Plik (potomek)"
    {
        MENUITEM "&Nowy", IDM_NEW
        MENUITEM "&Zamknij",IDM_CLOSE
        MENUITEM "&Koniec", IDM_EXIT
    }
    POPUP "&Okno (potomek)"   
    {
    	MENUITEM "Ułóż przy sobie poziomo", IDM_TILEHORZ
    	MENUITEM "Ułóż przy sobie pionowo", IDM_TILEVERT
    	MENUITEM "Ułóż jedno na drugim", IDM_CASCADE
    }
}

Analiza

Pierwszą rzeczą wykonywaną przez ten program jest zarejestrowanie klasy okna ramowego i klasy okna potomnego MDI. Po wykonaniu tych czynności program wywołuje funkcję CreateWindowEx do utworzenia okna ramowego. Wewnątrz sekcji obsługi wiadomości WM_CREATE okna ramowego tworzymy okno klienckie:

 

LOCAL ClientStruct: CLIENTCREATESTRUCT

    .IF uMsg==WM_CREATE
        INVOKE GetMenu, hWnd
        mov    hMainMenu, eax
        INVOKE GetSubMenu, eax, 1
        mov    ClientStruct.hWindowMenu, eax
        mov    ClientStruct.idFirstChild, 100
        INVOKE CreateWindowEx, NULL, ADDR MDIClientName, NULL,
                               WS_CHILD OR WS_VISIBLE OR \
                               WS_CLIPCHILDREN, CW_USEDEFAULT,
                               CW_USEDEFAULT, CW_USEDEFAULT,
                               CW_USEDEFAULT, hWnd, NULL, hInstance,
                               ADDR ClientStruct
        mov    hwndClient, eax

 

Wywołana zostaje funkcja GetMenu w celu pobrania uchwytu do menu okna ramowego, który ma być użyty jako parametr dla wywołania GetSubMenu. Do GetSubMenu przekazujemy wartość 1, ponieważ podmenu do wstawienia listy okien jest drugim podmenu. Następnie wypełniamy pola struktury CLIENTCREATESTRUCT. Nie musimy tego robić w tym miejscu, lecz tak jest wygodnie.

 

        mov  mdicreate.szClass, OFFSET MDIChildClassName
        mov  mdicreate.szTitle, OFFSET MDIChildTitle
        push hInstance
        pop  mdicreate.hOwner
        mov  eax, CW_USEDEFAULT
        mov  mdicreate.x, eax ;tutaj wszędzie trafia wartość
        mov  mdicreate.y, eax ;CW_USEDEFAULT
        mov  mdicreate.lx, eax
        mov  mdicreate.ly, eax

 

Po utworzeniu okna ramowego (i również klienckiego) wywołujemy funkcję LoadMenu do załadowania menu dla okna potomnego z zasobów. Musimy dostać uchwyt tego menu, aby go wymienić z menu okna ramowego, gdy pojawi się okno potomne. Nie zapomnij wywołać funkcji DestroyMenu z tym uchwytem przed zakończeniem działania aplikacji i powrotem do systemu Windows. Zwykle system automatycznie usuwa menu skojarzone z oknem, gdy aplikacja kończy pracę, lecz w tym przypadku menu okna potomnego nie jest powiązane z żadnym oknem, stąd wciąż będzie zajmowało cenną pamięć, nawet po zakończeniu aplikacji (jest to tzw. wyciek pamięci - memory leak).

 

    INVOKE LoadMenu, hInstance, IDR_CHILDMENU
    mov    hChildMenu, eax
    ...
    INVOKE DestroyMenu, hChildMenu

 

Wewnątrz pętli wiadomości wywołujemy funkcję TranslateMDISysAccel.

 

    .WHILE TRUE
        INVOKE GetMessage, ADDR msg, NULL, 0, 0
        .BREAK .IF (!eax)
        INVOKE TranslateMDISysAccel, hwndClient, ADDR msg
        .IF eax==0
            INVOKE TranslateMessage, ADDR msg
            INVOKE DispatchMessage, ADDR msg
        .ENDIF
    .ENDW

 

Jeśli TranslateMDISysAccel zwraca w rejestrze eax wartość różną od 0, to znaczy, iż dana wiadomość została już wewnętrznie obsłużona przez sam system Windows, zatem tobie nie pozostaje nic do zrobienia. Jeśli funkcja zwróci wartość 0, to wiadomość nie dotyczy MDI i powinna zostać obsłużona w zwykły sposób.

 

WndProc PROC hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

    ...
    .ELSE
        INVOKE DefFrameProc, hWnd, hwndClient, uMsg, wParam, lParam
        ret
    .ENDIF

    xor eax, eax
    ret

WndProc ENDP

 

Zwróć uwagę, iż wewnątrz procedury okna w oknie ramowym wywołujemy DefFrameProc do obsługi pozostałych wiadomości.

Wołem roboczym procedury okna jest sekcja obsługi wiadomości WM_COMMAND. Gdy użytkownik wybiera opcję menu Nowy, tworzymy okno potomne MDI.

 

    .ELSEIF ax==IDM_NEW
        INVOKE SendMessage, hwndClient, WM_MDICREATE,
                            0, ADDR mdicreate

 

W naszym przykładzie tworzymy okno potomne MDI wysyłając wiadomość WM_MDICREATE do okna klienckiego przekazując mu w iParam wskazanie procedury MDICREATESTRUCT.

 

ChildProc PROC hChild:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD

    .IF uMsg==WM_MDIACTIVATE
        mov eax, lParam
        .IF eax==hChild
            INVOKE GetSubMenu, hChildMenu, 1
            mov    edx, eax
            INVOKE SendMessage, hwndClient, WM_MDISETMENU, hChildMenu, edx
        .ELSE
            INVOKE GetSubMenu, hMainMenu, 1
            mov    edx, eax
            INVOKE SendMessage, hwndClient, WM_MDISETMENU, hMainMenu, edx
        .ENDIF
        INVOKE DrawMenuBar, hwndFrame

 

Po utworzeniu okna potomnego MDI monitoruje ono wiadomość WM_MDIACTIVATE w celu sprawdzenia, czy jest oknem aktywnym. Dokonuje tego porównując wartość lParam zawierającą uchwyt aktywnego okna ze swoim własnym uchwytem. Jeśli są równe, to jest aktywnym oknem i następnym krokiem będzie wymiana menu okna ramowego na jego własne. Ponieważ oryginalne menu zostanie zastąpione, należy ponownie powiadomić system Windows, w którym podmenu ma się pojawiać lista okien. Stąd musimy wywołać ponownie funkcję GetSubMenu w celu pobrania uchwytu do podmenu. Wysyłamy wiadomość WM_MDISETMENU do okna klienckiego, aby osiągnąć pożądany wynik. Parametr wParam wiadomości WM_MDISETMENU zawiera uchwyt menu, którym ma zostać zastąpione menu oryginalne. lParam zawiera uchwyt podmenu, w którym ma się pojawić lista okiem MDI. Zaraz po wysłaniu wiadomości WM_MDISETMENU wywołujemy funkcję DrawMenuBar, aby odświeżyć listę menu, w przeciwnym wypadku będziesz miał bałagan w menu.

 

    .ELSE
        INVOKE DefMDIChildProc, hChild, uMsg, wParam, lParam
        ret
    .ENDIF

    xor eax, eax
    ret

ChildProc ENDP

 

Wewnątrz procedury okna w oknie potomnym MDI musisz przekazać wszystkie nie obsługiwane wiadomości do DefMDIChildProc zamiast do DefWindowProc.

 

    .ELSEIF ax==IDM_TILEHORZ
        INVOKE SendMessage, hwndClient, WM_MDITILE,
                            MDITILE_HORIZONTAL, 0
    .ELSEIF ax==IDM_TILEVERT
        INVOKE SendMessage, hwndClient, WM_MDITILE,
                            MDITILE_VERTICAL, 0
    .ELSEIF ax==IDM_CASCADE
         INVOKE SendMessage, hwndClient, WM_MDICASCADE,
                             MDITILE_SKIPDISABLED, 0

 

Gdy użytkownik wybierze jeden z elementów menu w podmenu Okno, wysyłamy odpowiednią wiadomość do okna klienckiego. Jeśli użytkownik wybierze ułożenie okien jedno obok drugiego, wysyłamy wiadomość WM_MDITILE do okna klienckiego określając w wParam rodzaj pożądanego ułożenia okien. Wiadomość WM_CASCADE jest podobna.

 

    .ELSEIF ax==IDM_CLOSE
        INVOKE SendMessage, hwndClient, WM_MDIGETACTIVE, 0, 0
        INVOKE SendMessage, eax, WM_CLOSE, 0, 0
 

Gdy użytkownik wybierze element menu Zamknij, musimy pobrać uchwyt bieżąco aktywnego okna potomnego MDI najpierw wysyłając wiadomość WM_MDIGETACTIVE do okna klienckiego. Wartość zwrotna w rejestrze eax jest uchwytem do tego okna. Po tej operacji wysyłamy wiadomość WM_CLOSE do aktywnego okna MDI.

 

    .ELSEIF uMsg==WM_CLOSE
        INVOKE MessageBox, hChild, ADDR ClosePromptMessage,
                           ADDR AppName, MB_YESNO
        .IF eax==IDYES
            INVOKE SendMessage, hwndClient, WM_MDIDESTROY, hChild, 0
        .ENDIF

 

Wewnątrz procedury okna w oknie potomnym MDI, gdy zostaje odebrana wiadomość WM_CLOSE, wyświetlamy okienko informacyjne z prośbą o potwierdzenie operacji zamykania okna. Jeśli odpowiedź jest twierdzącą, wysyłamy wiadomość WM_MDIDESTROY do okna klienckiego. WM_MDIDESTROY zamyka okno potomne MDI i przywraca tytuł w oknie ramowym.

 

Autorem kursu jest Iczelion. Kurs programowania Windows znalazł się na serwerze I LO w Tarnowie za pisemną zgodą autora.
Tłumaczenie z języka angielskiego, opracowanie HTML i konwersję przykładów programów wykonał mgr Jerzy Wałaszek.



List do administratora Serwisu Edukacyjnego Nauczycieli I LO

Twój email: (jeśli chcesz otrzymać odpowiedź)
Temat:
Uwaga: ← tutaj wpisz wyraz  ilo , inaczej list zostanie zignorowany

Poniżej wpisz swoje uwagi lub pytania dotyczące tego rozdziału (max. 2048 znaków).

Liczba znaków do wykorzystania: 2048

 

W związku z dużą liczbą listów do naszego serwisu edukacyjnego nie będziemy udzielać odpowiedzi na prośby rozwiązywania zadań, pisania programów zaliczeniowych, przesyłania materiałów czy też tłumaczenia zagadnień szeroko opisywanych w podręcznikach.



   I Liceum Ogólnokształcące   
im. Kazimierza Brodzińskiego
w Tarnowie

©2017 mgr Jerzy Wałaszek

Dokument ten rozpowszechniany jest zgodnie z zasadami licencji
GNU Free Documentation License.