19. 多重文件介面

Post date: 2012/3/23 上午 05:42:07

19. 多重文件介面

多重文件介面(MDI)是Microsoft Windows文件處理應用程式的一種規範,該規範描述了視窗結構和允許使用者在單個應用程式中使用多個文件的使用者介面(如文書處理程式中的文字文件和試算表程式中的試算表)。簡單地說,就像Windows在一個螢幕上維護多個應用程式視窗一樣,MDI應用程式在一個顯示區域內維護多個文件視窗。Windows中的第一個MDI應用程式是Windows下的Microsoft Excel的第一個版本。緊接著又出現了許多其他的應用程式。

MDI概念

儘管MDI規範隨著Windows 2.0的推出已經很普及,但在那時,MDI應用程式寫起來很困難,並且需要一些非常複雜的程式設計工作。從Windows 3.0起,其中許多工作就都由Windows為您做好了。Windows 95中增強的支援也已經被添加進Windows 98和Microsoft Windows NT中。

MDI的組成

MDI程式的主應用程式視窗是很普通的:它有一個標題列、一個功能表、一個縮放邊框、一個系統功能表圖示和最大化/最小化/關閉按鈕。顯示區域經常被稱為「工作空間」,它不直接用於顯示程式輸出。這個工作空間包括零個或多個子視窗,每個視窗都顯示一個文件。

這些子視窗看起來與通常的應用程式視窗以及MDI程式的主視窗很相似。它們有一個標題列、一個縮放邊框、一個系統功能表圖示和最大化/最小化/關閉按鈕,可能還包括捲動列。但是文件視窗沒有功能表,主應用程式視窗上的功能表適用於文件視窗。

在任何時候都只能有一個文件視窗是活動的(加亮標題列來表示),它出現在其他所有文件視窗之前。所有文件視窗都由工作空間區域加以剪裁,而不會出現在應用程式視窗之外。

初看起來,對Windows程式寫作者來說,MDI似乎是相當簡單。需要程式寫作者做的工作好像就是為每個文件建立一個WS_CHILD視窗,並使程式的主應用程式視窗成為文件視窗的父視窗。但對現有的MDI應用程式稍加研究,就會發現一些導致程式寫作困難的複雜問題。例如:

  • MDI文件視窗可以最小化。它的圖示出現在工作空間的底部。一般來說,MDI應用程式可以將不同的圖示分別用於主應用程式視窗和每一類文件應用。
  • MDI文件視窗可以最大化。在這種情況下,文件視窗的標題列(一般用來顯示視窗中文件的檔案名稱)消失,檔案名稱出現在應用程式視窗標題列的應用程式名稱之後,文件視窗的系統功能表圖示成為應用程式視窗的頂層功能表中的第一項。關閉文件視窗按鈕變成頂層功能表中的最後一項,且出現在最右邊。
  • 用以關閉文件視窗的系統鍵盤加速鍵與關閉主視窗的系統鍵盤加速鍵一樣,只是Ctrl鍵代替了Alt鍵。這也就是說,Alt+F4用於關閉應用程式視窗,而Ctrl+F4用於關閉文件視窗。此外,Ctrl+F6可以在活動MDI應用程式的子文件視窗之間切換。與平時一樣,Alt+空白鍵啟動主視窗的系統功能表,Alt+-(減號)啟動活動子文件視窗的系統功能表。
  • 當使用游標鍵在功能表項間移動時,控制項權通常從系統功能表轉到功能表列中的第一項。在MDI應用程式中,控制項權是從應用程式系統功能表轉到活動文件系統功能表,然後再轉到功能表列中的第一項。
  • 如果應用程式能夠支援若干種型態的子視窗(如Microsoft Excel中的工作表和圖表文件),那麼功能表應能反映出與這種型態的文件有關的操作。這就要求當不同的文字視窗變成活動視窗時,程式能更換功能表。此外,當沒有文件視窗存在時,功能表應該被縮減到只剩下與打開新文件有關的操作。
  • 頂層功能表上有一個叫做「視窗(Window)」的功能表項。按照習慣,這是頂層功能表上「Help」之前的那一項,即倒數第二項。「視窗」子功能表上通常包含在工作空間內安排文件視窗的選項。文件視窗可以從左上方開始「平鋪」或「層疊」。在前一種方式下,可以完整地看到每一個文件視窗。這個子功能表同時也包含所有文件視窗的列表。從中選擇一個文件視窗,就可以把此文件視窗移到前景。

Windows 98支援MDI的所有這些方面。當然,需要您做一些工作(如下面的範例程式所示),但是,這遠不是要您程式寫作來直接支援所有這些功能。

MDI支援

探討Windows的MDI支援時需要發表一些新術語。主應用程式視窗稱為「框架視窗」,就像傳統的Windows程式一樣,它是WS_OVERLAPPEDWINDOW樣式的視窗。

MDI應用程式還根據預先定義的視窗類別MDICLIENT建立「客戶視窗」,這一客戶視窗是用這種視窗類別和WS_CHILD樣式呼叫CreateWindow來建立的。這一呼叫的最後一個參數是指向一個CLIENTCREATESTRUCT型態的結構的指標。這個客戶視窗覆蓋框架視窗的顯示區域,並提供許多MDI支援。此客戶視窗的顏色是系統顏色COLOR_APPWORKSPACE。

文件視窗被稱為「子視窗」。通過初始化一個MDICREATESTRUCT型態的結構,以一個指向此結構的指標為參數將訊息WM_MDICREATE發送給客戶視窗,就可以建立這些文件視窗。

文件視窗是客戶視窗的子視窗,而客戶視窗又是框架視窗的子視窗。父-子視窗分層結構如圖19-1所示。

圖19-1 Windows MDI應用程式的父-子層次圖

您需要框架視窗的視窗類別(及視窗訊息處理程式)和一個由應用程式支援的每類子視窗的視窗類別(及視窗訊息處理程式)。由於已經預先註冊了視窗類別,所以不需要客戶視窗的視窗訊息處理程式。

Windows 98的MDI支援包括一個視窗類別、五個函式、兩個資料結構和12個訊息。前面已經提到了MDI視窗類別,即MDICLIENT,以及資料結構CLIENTCREATESTRUCT和MDICREATESTRUCT。在MDI應用程式中,這五個函式中的兩個用於取代DefWindowProc:不再將DefWindowProc呼叫用於所有未處理的訊息,而是由框架視窗程序呼叫DefFrameProc,子視窗程序呼叫DefMDIChildProc。另一個MDI特有的函式TranslateMDISysAccel與第十章中討論的TranslateAccelerator的使用方式相同。MDI支援也包括ArrangeIconicWindows函式,但有一條專用的MDI訊息使得此函式對MDI程式來說不再必要。

第五個MDI函式是CreateMDIWindow,它使得子視窗可以在單獨的執行緒中被建立。這個函式不需要在單執行緒的程式中,我會展示這一點。

在下面的程式中,我將展示12條MDI訊息中的9條(其他三個訊息一般不用),這些訊息的字首是WM_MDI。框架視窗向客戶視窗發送其中某個訊息,以便在子視窗上完成一項操作或者取得關於子視窗的資訊(例如,框架視窗發送一個WM_MDICREATE訊息給客戶視窗,以建立子視窗)。訊息WM_MDIACTIVATE訊息有點特別:框架視窗可以發送這個訊息給客戶視窗來啟動一個子視窗,而客戶視窗也把這個訊息發送給將被啟動或者失去活動的子視窗,以便通知它們這一變化。

MDI的範例程式

程式19-1 MDIDEMO程式說明了編寫MDI應用程式的基本方法。

程式19-1 MDIDEMO MDIDEMO.C /*--------------------------------------------------------------------------- MDIDEMO.C -- Multiple-Document Interface Demonstration (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" #define INIT_MENU_POS 0 #define HELLO_MENU_POS 2 #define RECT_MENU_POS 1 #define IDM_FIRSTCHILD 50000 LRESULT CALLBACK FrameWndProc (HWND, UINT, WPARAM, LPARAM) ; BOOL CALLBACK CloseEnumProc (HWND, LPARAM) ; LRESULT CALLBACK HelloWndProc (HWND, UINT, WPARAM, LPARAM) ; LRESULT CALLBACK RectWndProc (HWND, UINT, WPARAM, LPARAM) ; // structure for storing data unique to each Hello child window typedef struct tagHELLODATA { UINT iColor ; COLORREF clrText ; } HELLODATA, * PHELLODATA ; // structure for storing data unique to each Rect child window typedef struct tagRECTDATA { short cxClient ; short cyClient ; } RECTDATA, * PRECTDATA ; // global variables TCHAR szAppName[] = TEXT ("MDIDemo") ; TCHAR szFrameClass[] = TEXT ("MdiFrame") ; TCHAR szHelloClass[] = TEXT ("MdiHelloChild") ; TCHAR szRectClass[] = TEXT ("MdiRectChild") ; HINSTANCE hInst ; HMENU hMenuInit, hMenuHello, hMenuRect ; HMENU hMenuInitWindow, hMenuHelloWindow, hMenuRectWindow ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HACCEL hAccel ; HWND hwndFrame, hwndClient ; MSG msg ; WNDCLASS wndclass ; hInst = hInstance ; // Register the frame window class wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = FrameWndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) (COLOR_APPWORKSPACE + 1) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szFrameClass ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } // Register the Hello child window class wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = HelloWndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = sizeof (HANDLE) ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szHelloClass ; RegisterClass (&wndclass) ; // Register the Rect child window class wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = RectWndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = sizeof (HANDLE) ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szRectClass ; RegisterClass (&wndclass) ; // Obtain handles to three possible menus & submenus hMenuInit = LoadMenu (hInstance, TEXT ("MdiMenuInit")) ; hMenuHello = LoadMenu (hInstance, TEXT ("MdiMenuHello")) ; hMenuRect = LoadMenu (hInstance, TEXT ("MdiMenuRect")) ; hMenuInitWindow = GetSubMenu (hMenuInit, INIT_MENU_POS) ; hMenuHelloWindow = GetSubMenu (hMenuHello, HELLO_MENU_POS) ; hMenuRectWindow = GetSubMenu (hMenuRect, RECT_MENU_POS) ; // Load accelerator table hAccel = LoadAccelerators (hInstance, szAppName) ; // Create the frame window hwndFrame = CreateWindow (szFrameClass, TEXT ("MDI Demonstration"), WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, hMenuInit, hInstance, NULL) ; hwndClient = GetWindow (hwndFrame, GW_CHILD) ; ShowWindow (hwndFrame, iCmdShow) ; UpdateWindow (hwndFrame) ; // Enter the modified message loop while (GetMessage (&msg, NULL, 0, 0)) { if ( !TranslateMDISysAccel (hwndClient, &msg) && !TranslateAccelerator (hwndFrame, hAccel, &msg)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } } // Clean up by deleting unattached menus DestroyMenu (hMenuHello) ; DestroyMenu (hMenuRect) ; return msg.wParam ; } LRESULT CALLBACK FrameWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HWND hwndClient ; CLIENTCREATESTRUCT clientcreate ; HWND hwndChild ; MDICREATESTRUCT mdicreate ; switch (message) { case WM_CREATE: // Create the client window clientcreate.hWindowMenu = hMenuInitWindow ; clientcreate.idFirstChild = IDM_FIRSTCHILD ; hwndClient = CreateWindow ( TEXT ("MDICLIENT"), NULL, WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU) 1, hInst, (PSTR) &clientcreate) ; return 0 ; case WM_COMMAND: switch (LOWORD (wParam)) { case IDM_FILE_NEWHELLO: // Create a Hello child window mdicreate.szClass = szHelloClass ; mdicreate.szTitle = TEXT ("Hello") ; mdicreate.hOwner = hInst ; mdicreate.x = CW_USEDEFAULT ; mdicreate.y = CW_USEDEFAULT ; mdicreate.cx = CW_USEDEFAULT ; mdicreate.cy = CW_USEDEFAULT ; mdicreate.style = 0 ; mdicreate.lParam = 0 ; hwndChild = (HWND) SendMessage (hwndClient, WM_MDICREATE, 0, (LPARAM) (LPMDICREATESTRUCT) &mdicreate) ; return 0 ; case IDM_FILE_NEWRECT: // Create a Rect child window mdicreate.szClass = szRectClass ; mdicreate.szTitle = TEXT ("Rectangles") ; mdicreate.hOwner = hInst ; mdicreate.x = CW_USEDEFAULT ; mdicreate.y = CW_USEDEFAULT ; mdicreate.cx = CW_USEDEFAULT ; mdicreate.cy = CW_USEDEFAULT ; mdicreate.style = 0 ; mdicreate.lParam = 0 ; hwndChild = (HWND) SendMessage (hwndClient, WM_MDICREATE, 0, (LPARAM) (LPMDICREATESTRUCT) &mdicreate) ; return 0 ; case IDM_FILE_CLOSE: // Close the active window hwndChild = (HWND) SendMessage (hwndClient, WM_MDIGETACTIVE, 0, 0) ; if (SendMessage (hwndChild, WM_QUERYENDSESSION, 0, 0)) SendMessage (hwndClient, WM_MDIDESTROY, (WPARAM) hwndChild, 0) ; return 0 ; case IDM_APP_EXIT: // Exit the program SendMessage (hwnd, WM_CLOSE, 0, 0) ; return 0 ; // messages for arranging windows case IDM_WINDOW_TILE: SendMessage (hwndClient, WM_MDITILE, 0, 0) ; return 0 ; case IDM_WINDOW_CASCADE: SendMessage (hwndClient, WM_MDICASCADE, 0, 0) ; return 0 ; case IDM_WINDOW_ARRANGE: SendMessage (hwndClient, WM_MDIICONARRANGE, 0, 0) ; return 0 ; case IDM_WINDOW_CLOSEALL: // Attempt to close all children EnumChildWindows (hwndClient, CloseEnumProc, 0) ; return 0 ; default: // Pass to active child... hwndChild = (HWND) SendMessage (hwndClient, WM_MDIGETACTIVE, 0, 0) ; if (IsWindow (hwndChild)) SendMessage (hwndChild, WM_COMMAND, wParam, lParam) ; break ; // ...and then to DefFrameProc } break ; case WM_QUERYENDSESSION: case WM_CLOSE: // Attempt to close all children SendMessage (hwnd, WM_COMMAND, IDM_WINDOW_CLOSEALL, 0) ; if (NULL != GetWindow (hwndClient, GW_CHILD)) return 0 ; break ; // i.e., call DefFrameProc case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } // Pass unprocessed messages to DefFrameProc (not DefWindowProc) return DefFrameProc (hwnd, hwndClient, message, wParam, lParam) ; } BOOL CALLBACK CloseEnumProc (HWND hwnd, LPARAM lParam) { if (GetWindow (hwnd, GW_OWNER)) // Check for icon title return TRUE ; SendMessage (GetParent (hwnd), WM_MDIRESTORE, (WPARAM) hwnd, 0) ; if (!SendMessage (hwnd, WM_QUERYENDSESSION, 0, 0)) return TRUE ; SendMessage (GetParent (hwnd), WM_MDIDESTROY, (WPARAM) hwnd, 0) ; return TRUE ; } LRESULT CALLBACK HelloWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static COLORREF clrTextArray[] = { RGB (0, 0, 0), RGB (255, 0, 0), RGB (0, 255, 0), RGB ( 0, 0, 255), RGB (255, 255, 255) } ; static HWND hwndClient, hwndFrame ; HDC hdc ; HMENU hMenu ; PHELLODATA pHelloData ; PAINTSTRUCT ps ; RECT rect ; switch (message) { case WM_CREATE: // Allocate memory for window private data pHelloData = (PHELLODATA) HeapAlloc (GetProcessHeap (), HEAP_ZERO_MEMORY, sizeof (HELLODATA)) ; pHelloData->iColor = IDM_COLOR_BLACK ; pHelloData->clrText = RGB (0, 0, 0) ; SetWindowLong (hwnd, 0, (long) pHelloData) ; // Save some window handles hwndClient = GetParent (hwnd) ; hwndFrame = GetParent (hwndClient) ; return 0 ; case WM_COMMAND: switch (LOWORD (wParam)) { case IDM_COLOR_BLACK: case IDM_COLOR_RED: case IDM_COLOR_GREEN: case IDM_COLOR_BLUE: case IDM_COLOR_WHITE: // Change the text color pHelloData = (PHELLODATA) GetWindowLong (hwnd, 0) ; hMenu = GetMenu (hwndFrame) ; CheckMenuItem (hMenu, pHelloData->iColor, MF_UNCHECKED) ; pHelloData->iColor = wParam ; CheckMenuItem (hMenu, pHelloData->iColor, MF_CHECKED) ; pHelloData->clrText = clrTextArray[wParam - IDM_COLOR_BLACK] ; InvalidateRect (hwnd, NULL, FALSE) ; } return 0 ; case WM_PAINT: // Paint the window hdc = BeginPaint (hwnd, &ps) ; pHelloData = (PHELLODATA) GetWindowLong (hwnd, 0) ; SetTextColor (hdc, pHelloData->clrText) ; GetClientRect (hwnd, &rect) ; DrawText (hdc, TEXT ("Hello, World!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_MDIACTIVATE: // Set the Hello menu if gaining focus if (lParam == (LPARAM) hwnd) SendMessage (hwndClient, WM_MDISETMENU,(WPARAM) hMenuHello, (LPARAM) hMenuHelloWindow) ; // Check or uncheck menu item pHelloData = (PHELLODATA) GetWindowLong (hwnd, 0) ; CheckMenuItem (hMenuHello, pHelloData->iColor, (lParam == (LPARAM) hwnd) ? MF_CHECKED : MF_UNCHECKED) ; // Set the Init menu if losing focus if (lParam != (LPARAM) hwnd) SendMessage (hwndClient, WM_MDISETMENU, (WPARAM) hMenuInit,(LPARAM) hMenuInitWindow) ; DrawMenuBar (hwndFrame) ; return 0 ; case WM_QUERYENDSESSION: case WM_CLOSE: if (IDOK != MessageBox (hwnd, TEXT ("OK to close window?"), TEXT ("Hello"), MB_ICONQUESTION | MB_OKCANCEL)) return 0 ; break ; // i.e., call DefMDIChildProc case WM_DESTROY: pHelloData = (PHELLODATA) GetWindowLong (hwnd, 0) ; HeapFree (GetProcessHeap (), 0, pHelloData) ; return 0 ; } // Pass unprocessed message to DefMDIChildProc return DefMDIChildProc (hwnd, message, wParam, lParam) ; } LRESULT CALLBACK RectWndProc ( HWND hwnd, UINT message,WPARAM wParam, LPARAM lParam) { static HWND hwndClient, hwndFrame ; HBRUSH hBrush ; HDC hdc ; PRECTDATA pRectData ; PAINTSTRUCT ps ; int xLeft, xRight, yTop, yBottom ; short nRed, nGreen, nBlue ; switch (message) { case WM_CREATE: // Allocate memory for window private data pRectData = (PRECTDATA) HeapAlloc (GetProcessHeap (), HEAP_ZERO_MEMORY, sizeof (RECTDATA)) ; SetWindowLong (hwnd, 0, (long) pRectData) ; // Start the timer going SetTimer (hwnd, 1, 250, NULL) ; // Save some window handles hwndClient = GetParent (hwnd) ; hwndFrame = GetParent (hwndClient) ; return 0 ; case WM_SIZE: // If not minimized, save the window size if (wParam != SIZE_MINIMIZED) { pRectData = (PRECTDATA) GetWindowLong (hwnd, 0) ; pRectData->cxClient = LOWORD (lParam) ; pRectData->cyClient = HIWORD (lParam) ; } break ; // WM_SIZE must be processed by DefMDIChildProc case WM_TIMER: // Display a random rectangle pRectData = (PRECTDATA) GetWindowLong (hwnd, 0) ; xLeft = rand () % pRectData->cxClient ; xRight = rand () % pRectData->cxClient ; yTop = rand () % pRectData->cyClient ; yBottom = rand () % pRectData->cyClient ; nRed = rand () & 255 ; nGreen = rand () & 255 ; nBlue = rand () & 255 ; hdc = GetDC (hwnd) ; hBrush = CreateSolidBrush (RGB (nRed, nGreen, nBlue)) ; SelectObject (hdc, hBrush) ; Rectangle (hdc, min (xLeft, xRight), min (yTop, yBottom), max (xLeft, xRight), max (yTop, yBottom)) ; ReleaseDC (hwnd, hdc) ; DeleteObject (hBrush) ; return 0 ; case WM_PAINT: // Clear the window InvalidateRect (hwnd, NULL, TRUE) ; hdc = BeginPaint (hwnd, &ps) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_MDIACTIVATE: // Set the appropriate menu if (lParam == (LPARAM) hwnd) SendMessage (hwndClient, WM_MDISETMENU, (WPARAM) hMenuRect, (LPARAM) hMenuRectWindow) ; else SendMessage (hwndClient, WM_MDISETMENU, (WPARAM) hMenuInit, (LPARAM) hMenuInitWindow) ; DrawMenuBar (hwndFrame) ; return 0 ; case WM_DESTROY: pRectData = (PRECTDATA) GetWindowLong (hwnd, 0) ; HeapFree (GetProcessHeap (), 0, pRectData) ; KillTimer (hwnd, 1) ; return 0 ; } // Pass unprocessed message to DefMDIChildProc return DefMDIChildProc (hwnd, message, wParam, lParam) ; }

MDIDEMO.RC (摘錄) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Menu MDIMENUINIT MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "New &Hello", IDM_FILE_NEWHELLO MENUITEM "New &Rectangle", IDM_FILE_NEWRECT MENUITEM SEPARATOR MENUITEM "E&xit", IDM_APP_EXIT END END MDIMENUHELLO MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "New &Hello", IDM_FILE_NEWHELLO MENUITEM "New &Rectangle", IDM_FILE_NEWRECT MENUITEM "&Close", IDM_FILE_CLOSE MENUITEM SEPARATOR MENUITEM "E&xit", IDM_APP_EXIT END POPUP "&Color" BEGIN MENUITEM "&Black", IDM_COLOR_BLACK MENUITEM "&Red", IDM_COLOR_RED MENUITEM "&Green", IDM_COLOR_GREEN MENUITEM "B&lue", IDM_COLOR_BLUE MENUITEM "&White", IDM_COLOR_WHITE END POPUP "&Window" BEGIN MENUITEM "&Cascade\tShift+F5", IDM_WINDOW_CASCADE MENUITEM "&Tile\tShift+F4", IDM_WINDOW_TILE MENUITEM "Arrange &Icons", IDM_WINDOW_ARRANGE MENUITEM "Close &All", IDM_WINDOW_CLOSEALL END END MDIMENURECT MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "New &Hello", IDM_FILE_NEWHELLO MENUITEM "New &Rectangle", IDM_FILE_NEWRECT MENUITEM "&Close", IDM_FILE_CLOSE MENUITEM SEPARATOR MENUITEM "E&xit", IDM_APP_EXIT END POPUP "&Window" BEGIN MENUITEM "&Cascade\tShift+F5", IDM_WINDOW_CASCADE MENUITEM "&Tile\tShift+F4", IDM_WINDOW_TILE MENUITEM "Arrange &Icons", IDM_WINDOW_ARRANGE MENUITEM "Close &All", IDM_WINDOW_CLOSEALL END END ///////////////////////////////////////////////////////////////////////////// // Accelerator MDIDEMO ACCELERATORS DISCARDABLE BEGIN VK_F4, IDM_WINDOW_TILE, VIRTKEY, SHIFT, NOINVERT VK_F5, IDM_WINDOW_CASCADE, VIRTKEY, SHIFT, NOINVERT END

RESOURCE.H (摘錄) // Microsoft Developer Studio generated include file. // Used by MDIDemo.rc #define IDM_FILE_NEWHELLO 40001 #define IDM_FILE_NEWRECT 40002 #define IDM_APP_EXIT 40003 #define IDM_FILE_CLOSE 40004 #define IDM_COLOR_BLACK 40005 #define IDM_COLOR_RED 40006 #define IDM_COLOR_GREEN 40007 #define IDM_COLOR_BLUE 40008 #define IDM_COLOR_WHITE 40009 #define IDM_WINDOW_CASCADE 40010 #define IDM_WINDOW_TILE 40011 #define IDM_WINDOW_ARRANGE 40012 #define IDM_WINDOW_CLOSEALL 40013

MDIDEMO支援兩種型態的非常簡單的文件視窗:第一種視窗在它的顯示區域中央顯示"Hello, World!",另一種視窗顯示一系列隨機矩形(在原始碼列表和識別字名中,它們分別叫做「Hello」文件和「Rect」文件)。這兩類文件視窗的功能表不同,顯示"Hello, World!"的文件視窗有一個允許使用者修改文字顏色的功能表。

三個功能表

現在讓我們先看看MDIDEMO.RC資源描述檔,它定義了程式所使用的三個功能表模板。

當文件視窗不存在時,程式顯示MdiMenuInit功能表,這個功能表只允許使用者建立新文件或退出程式。

MdiMenuHello功能表與顯示「Hello, World!」的文件視窗相關聯。「File」子功能表允許使用者打開任何一類新文件、關閉活動文件或退出程式。「Color」子功能表允許使用者設定文字顏色。Window子功能表包括以平鋪或者重疊的方式安排文件視窗、安排文件圖示或關閉所有視窗等選項,這個子功能表也列出了它們建立的所有文件視窗。

MdiMenuRect功能表與隨機矩形文件相關聯。除了不包含「Color」子功能表外,它與MdiMenuHello功能表一樣。

RESOURCE.H表頭檔案定義所有的功能表識別字。另外,以下三個常數定義在MDIDEMO.C中:

#define INIT_MENU_POS 0 #define HELLO_MENU_POS 2 #define RECT_MENU_POS 1

這些識別字說明每個功能表模板中Windows子功能表的位置。程式需要這些資訊來通知客戶視窗文件列表應出現在哪裡。當然,MdiMenuInit功能表沒有Windows子功能表,所以如前所述,文件列表應附加在第一個子功能表中(位置0)。不過,實際上永遠不會在此看到文件列表(在後面討論此程式時,您可以發現這樣做的原因)。

定義在MDIDEMO.C中的IDM_FIRSTCHILD識別字不對應於功能表項,它與出現在Windows子功能表上的文件列表中的第一個文件視窗相關聯。這個識別字的值應當大於所有其他功能表ID的值。

程式初始化

在MDIDEMO.C中,WinMain是從註冊框架視窗和兩個子視窗的視窗類別開始的。視窗訊息處理程式是FrameWndProc、HelloWndProc和RectWndProc。一般來說,這些視窗類別應該與不同的圖示相關聯。為了簡單起見,我們將標準IDI_APPLICATION圖示用於框架視窗和子視窗。

注意,我們已經定義了框架視窗類別的WNDCLASS結構的hbrBackground欄位為COLOR_APPWORKSPACE系統顏色。由於框架視窗的顯示區域被客戶視窗所覆蓋並且客戶視窗具有這種顏色,所以上面的定義不是絕對必要的。但是,在最初顯示框架視窗時,使用這種顏色似乎要好一些。

這三種視窗類別中的lpszMenuName欄位都設定為NULL。對「Hello」和「Rect」子視窗類別來說,這是很自然的。對於框架視窗類別,我在建立框架視窗時在CreateWindow函式中給出功能表代號。

「Hello」和「Rect」子視窗的視窗類別將WNDCLASS結構中的cbWndExtra欄位設為非零值來為每個視窗配置額外空間,這個空間將用於儲存指向一個記憶體塊的指標(HELLODATA和RECTDATA結構的大小定義在MDIDEMO.C的開始處),這個記憶體塊被用於儲存每個文件視窗特有的資訊。

下一步,WinMain用LoadMenu載入三個功能表,並把它們的代號儲存到整體變數中。呼叫三次GetSubMenu函式可獲得Windows子功能表(文件列表將加在它上面)的代號,同樣也把它們儲存到整體變數中。LoadAccelerators函式載入加速鍵表。

在WinMain中呼叫CreateWindow建立框架視窗。在FrameWndProc中WM_CREATE訊息處理期間,框架視窗建立客戶視窗。這項操作涉及到再一次呼叫函式CreateWindow。視窗類別被設定為MDICLIENT,它是預先註冊的MDI顯示區域視窗類別。在Windows中許多對MDI的支援被放入了MDICLIENT視窗類別中。顯示區域視窗訊息處理程式作為框架視窗和不同文件視窗的中間層。當呼叫CreateWindow建立顯示區域視窗時,最後一個參數必須被設定為指向CLIENTCREATESTRUCT型態結構的指標。這個結構有兩個欄位:

  • hWindowMenu是要加入文件列表的子功能表的代號。在MDIDEMO中,它是hMenuInitWindow,是在WinMain期間獲得的。後面將看到如何修改此功能表。
  • idFirstChild是與文件列表中的第一個文件視窗相關聯的功能表ID。它就是IDM_FIRSTCHILD.

再讓我們回過頭來看看WinMain。MDIDEMO顯示新建立的框架視窗並進入訊息迴圈。訊息迴圈與正常的迴圈稍有不同:在呼叫GetMessage從訊息佇列中獲得訊息之後,MDI程式把這個訊息傳送給了TranslateMDISysAccel(以及TranslateAccelerator,如果像MDIDEMO程式一樣,程式本身也有功能表加速鍵的話)。

TranslateMDISysAccel函式把可能對應特定MDI加速鍵(例如Ctrl-F6)的按鍵轉換成WM_SYSCOMMAND訊息。如果TranslateMDISysAccel或TranslateAccelerator都傳回TRUE(表示某個訊息已被這些函式之一轉換),就不能呼叫TranslateMessage和DispatchMessage。

注意傳遞到TranslateMDISysAccel和TranslateAccelerator的兩個視窗代號:hwndClient和hwndFrame。WinMain函式通過用GW_CHILD參數呼叫GetWindow獲得hwndClient視窗代號。

建立子視窗

FrameWndProc的大部分工作是用於處理通知功能表選擇的WM_COMMAND訊息。與平時一樣,FrameWndProc中wParam參數的低字組包含著功能表ID。

在功能表ID的值為IDM_FILE_NEWHELLO和IDM_FILE_NEWRECT的情況下,FrameWndProc必須建立一個新的文件視窗。這涉及到初始化MDICREATESTRUCT結構中的欄位(大多數欄位對應於CreateWindow的參數),並將訊息WM_MDICREATE發送給客戶視窗,訊息的lParam參數設定為指向這個結構的指標。然後由客戶視窗建立子文件視窗。(也可以使用CreateMDIWindow函式。)

MDICREATESTRUCT結構中的szTitle欄位一般是對應於文件的檔案名稱。樣式欄位設定為視窗樣式WS_HSCROLL、WS_VSCROLL或這兩者的組合,以便在文件視窗中包括捲動列。樣式欄位也可以包括WS_MINIMIZE或WS_MAXIMIZE,以便在最初時以最小化或最大化狀態顯示文件視窗。

MDICREATESTRUCT結構的lParam欄位為框架視窗和子視窗共用某些變數提供了一種方法。這個欄位可以設定為含有一個結構的記憶體塊的記憶體代號。在子文件視窗的WM_CREATE訊息處理期間,lParam是一個指向CREATESTRUCT結構的指標,這個結構的lpCreateParams欄位是一個指向用於建立視窗的MDICREATESTRUCT結構的指標。

客戶視窗一旦接收到WM_MDICREATE訊息就建立一個子文件視窗,並把視窗標題加到用於建立客戶視窗的MDICLIENTSTRUCT結構中所指定的子功能表的底部。當MDIDEMO程式建立它的第一個文件視窗時,這個子功能表就是「MdiMenuInit」功能表中的「File」子功能表。後面將看到這個文件列表將如何移到「MdiMenuHello」和「MdiMenuRect」功能表的「Windows」子功能表中。

功能表上可以列出9個文件,每個文件的前面是帶有底線的數字1至9。如果建立的文件視窗多於9個,則這個清單後跟有「More Windows」功能表項。該項啟動帶有清單方塊的對話方塊,清單方塊列出了所有文件。這種文件列表的維護是Windows MDI支援的最好特性之一。

關於框架視窗的訊息處理

在把注意力轉移到子文件視窗之前,我們先繼續討論FrameWndProc的訊息處理。

當從「File」功能表中選擇「Close」時,MDIDEMO關閉活動子視窗。它通過把WM_MDIGETACTIVE訊息發送給客戶視窗,而獲得活動子視窗的代號。如果子視窗以WM_QUERYENDSESSION訊息來回應,那麼MDIDEMO將WM_MDIDESTROY訊息發送給客戶視窗,從而關閉子視窗。

處理「File」功能表中的「Exit」選項只需要框架視窗訊息處理程式給自己發送一個WM_CLOSE訊息。

處理Window子功能表的「Tile」、「Cascade」和「Arrange」選項是極容易的,只需把訊息WM_MDITILE、WM_MDICASCADE和WM_MDIICONARRANGE發送給客戶視窗。

處理「Close All」選項要稍微複雜一些。FrameWndProc呼叫EnumChildWindows,傳送一個引用CloseEnumProc函式的指標。此函式把WM_MDIRESTORE訊息發送給每個子視窗,緊跟著發出WM_QUERYENDSESSION和WM_MDIDESTROY。對圖示平鋪視窗來說並不就此結束,用GW_OWNER參數呼叫GetWindow時,傳回的非NULL值可以顯示出這一點。

FrameWndProc沒有處理任何由「Color」功能表中對顏色的選擇所導致的WM_COMMAND訊息,這些訊息應該由文件視窗負責處理。因此,FrameWndProc把所有未經處理的WM_COMMAND訊息發送到活動子視窗,以便子視窗可以處理那些與它們有關的訊息。

框架視窗訊息處理程式不予處理的所有訊息都要送到DefFrameProc,它在框架視窗訊息處理程式中取代了DefWindowProc。即使框架視窗訊息處理程式攔截了WM_MENUCHAR、WM_SETFOCUS或WM_SIZE訊息,這些訊息也要被送到DefFrameProc中。

所有未經處理的WM_COMMAND訊息也必須送給DefFrameProc。具體地說,FrameWndProc並不處理任何WM_COMMAND訊息,即使這些訊息是使用者在Windows子功能表的文件列表中選擇文件時產生的(這些選項的wParam值是以IDM_FIRSTCHILD開始的)。這些訊息要傳送到DefFrameProc,並在那裏進行處理。

注意框架視窗並不需要維護它所建立的所有文件視窗的視窗代號清單。如果需要這些視窗代號(如處理功能表上的「Close All」選項時),可以使用EnumChildWindows得到它們。

子文件視窗

現在看一下HelloWndProc,它是用於顯示「Hello, World!」的子文件視窗的視窗訊息處理程式。

與用於多個視窗的視窗類別一樣,所有在視窗訊息處理程式(或從該視窗訊息處理程式中呼叫的任何函式)中定義的靜態變數由依據該視窗類別建立的所有視窗共用。

只有對於每個唯一於視窗的資料才必須採用非靜態變數的方法來儲存。這樣的技術要用到視窗屬性。另一種方法(我使用的方法)是使用預留的記憶體空間;可以在註冊視窗類別時將WNDCLASS結構的cbWndExtra欄位設定為非零值以便預留這部分記憶體空間。

MDIDEMO程式使用這個記憶體空間來儲存一個指標,這個指標指向一塊與HELLODATA結構大小相同的記憶體塊。在處理WM_CREATE訊息時,HelloWndProc配置這塊記憶體,初始化它的兩個欄位(它們用於指定目前選中的功能表項和文字顏色),並用SetWindowLong將記憶體指標儲存到預留的空間中。

當處理改變文字顏色的WM_COMMAND訊息(回憶一下,這些訊息來自框架視窗訊息處理程式)時,HelloWndProc使用GetWindowLong獲得包含HELLODATA結構的記憶體塊的指標。利用這個結構,HelloWndProc清除原來對功能表項的選擇,設定所選功能表項為選中狀態,並儲存新的顏色。

當視窗變成活動視窗或不活動的時候,文件視窗訊息處理程式都會收到WM_MDIACTIVATE訊息(lParam的值是否為這個視窗的代號表示了該視窗是活動的還是不活動的)。您也許還能記起MDIDEMO程式中有三個不同的功能表:當無文件時為MdiMenuInit;當「Hello」文件視窗是活動視窗時為MdiMenuHello;當「Rect」文件視窗為活動視窗時為MdiMenuRect。

WM_MDIACTIVATE訊息為文件視窗提供了一個修改功能表的機會。如果lParam中含有本視窗的代號(意味著本視窗將變成活動的),那麼HelloWndProc就將功能表改為MdiMenuHello。如果lParam中包含另一個視窗的代號,那麼HelloWndProc將功能表改為MdiMenuInit。

HelloWndProc經由把WM_MDISETMENU訊息發送給客戶視窗來修改功能表,客戶視窗透過從目前功能表上刪除文件列表並把它添加到一個新的功能表上來處理這個訊息。這就是文件列表從MdiMenuInit功能表(它在建立第一個文件時有效)傳送到MdiMenuHello功能表中的方法。在MDI應用程式中不要用SetMenu函式改變功能表。

另一項工作涉及到「Color」子功能表上的選中旗標。像這樣的程式選項對每個文件來說都是不同的,例如,可以在一個視窗中設定黑色文字,在另一個視窗中設定紅色文字。功能表選中旗標應能反映出活動視窗中選擇的選項。由於這種原因,HelloWndProc在視窗變成非活動視窗時清除選中功能表項的選中旗標,而當視窗變成活動視窗時設定適當功能表項的選中旗標。

WM_MDIACTIVATE的wParam和lParam值分別是失去活動和被啟動視窗的代號。視窗訊息處理程式得到的第一個WM_MDIACTIVATE訊息的lParam參數被設定為目前視窗的代號。而當視窗被消除時,視窗訊息處理程式得到的最後一個訊息的lParam參數被設定為另一個值。當使用者從一個文件切換到另一個文件時,前一個文件視窗收到一個WM_MDIACTIVATE訊息,其lParam參數為第一個視窗的代號(此時,視窗訊息處理程式將功能表設定為MdiMenuInit);後一個文件視窗收到一個WM_MDIACTIVATE訊息,其lParam參數是第二個視窗的代號(此時,視窗訊息處理程式將功能表設定為MdiMenuHello或MdiMenuRect中適當的那個)。如果所有的視窗都關閉了,剩下的功能表就是MdiMenuInit。

當使用者從功能表中選擇「Close」或「Close All」時,FrameWndProc給子視窗發送一個WM_QUERYENDSESSION訊息。HelloWndProc將顯示一個訊息方塊並詢問使用者是否要關閉視窗,以此來處理WM_QUERYENDSESSION和WM_CLOSE訊息(在真實的應用程式中,訊息方塊會詢問是否需要儲存檔案)。如果使用者表示不能關閉視窗,那麼視窗訊息處理程式傳回0。

在WM_DESTROY訊息處理期間,HelloWndProc釋放在WM_CREATE期間配置的記憶體塊。

所有未經處理的訊息必須傳送到用於內定處理的DefMDIChildProc(不是DefWindowProc)。不論子視窗訊息處理程式是否使用了這些訊息,有幾個訊息必須被傳送給DefMDIChildProc。這些訊息是:WM_CHILDACTIVATE、WM_GETMINMAXINFO、WM_MENUCHAR、WM_MOVE、WM_SETFOCUS、WM_SIZE和WM_SYSCOMMAND。

RectWndProc與HelloWndProc非常相似,但是它比HelloWndProc要簡單一些(不含功能表選項並且無需使用者確認是否關閉視窗),所以這裏不對它進行討論了。但應該注意到,在處理WM_SIZE之後RectWndProc使用了「break」敘述,所以WM_SIZE訊息被傳給DefMDIChildProc。

結束處理

在WinMain中,MDIDEMO使用LoadMenu載入資源描述檔中定義的三個功能表。一般說來,當功能表所在的視窗被清除時,Windows也要清除與之關聯的功能表。對於Init功能表,應該清除那些沒有聯繫到視窗的功能表。由於這個原因,MDIDEMO在WinMain的末尾呼叫了兩次DestroyMenu來清除「Hello」和「Rect」功能表。