15. Többszörös dokumentumok (App Part 4: Multiple Document Interface)
2014.06.14. 13:22
Példa: app_four
MDI áttekintés
Először egy kicsit a háttérről.. Minden ablak rendelkezik ügyfélterülettel (Client Area). Ez az a terület, ahol a legtöbb program képeket rajzol, vezérlőket helyez el. Az ügyfélterület nem különül el az ablaktól, hanem egy kissebb speciális régiója. Néhány ablak csak ebből áll, mások pedig kissebbek, hogy legyen helye a menünek, görgetősávnak, stb.
Az MDI felől nézve a fő ablakunk egy keret, aminek a gyermek ablaka az MDI. Ez az MDI egy teljesen különálló ablak, saját ügyfélterülettel, saját vezérlőkkel és talán saját pár pixeles kerettel. Az MDI kliensnek nem küldhetünk közvetlenül üzeneteket, ellenben előre definiált ablak osztály (az MDICLIENT) segítségével igen. Amikor megjelenik a programunk, akkor egy üzenetet küldünk, ami által létrejön az új általunk megadott ablak. Az új ablak az MDI kliens gyermeke lesz, ami viszont a keret ablak gyermeke. Továbbá lehetséges az MDI kliensnek is saját gyermek ablaka, mint például a szerkesztő vezérlőnk ebben a példában.
Írnunk kell kettő (vagy több) Window eljárást, egyet a fő ablaknak (frame), másikat pedig az MDI gyermek részére. Most tehát egynél több típusú gyermekünk van, így minden egyes típusra külön külön meg kell írnuk.
Ha még nem tiszta, akkor az alábbi kép majd gondoskodik a tisztaságról.
Ismerkedés az MDI-vel
Mivel az MDI kezeléséhez az egész programra kiható apró változtatások szükségesek, olvassuk el figyelmesen a következőket! Ha nem működik az MDI programunk, vagy furcsa, ne várt dolgokat produkál, akkor elképzelhető hogy az egyik ilyen módosítást nem tettük meg.
MDI ügyfélablak
Mielőtt létrehoznánk MDI ablakunkat, meg kell változtassuk az alap üzenetkezelőt, hogy a mi saját ablakkezelőnk dolgozzon. Mi magunk hozzuk létre az MDI ügyfelet a fő ablakban. Meg kell változtatni a DefWindowProc hívását DefFrameProc-ra. Ez képes kezelni a speciális üzeneteket, amik az MDI működéséhez szükségesek.
default: return DefFrameProc(hwnd, g_hMDIClient, msg, wParam, lParam);
Következő lépés az MDI kliens ablak létrehozása a főablakban, ezt mint mindig a WM_CREATE-tel tesszük meg.
CLIENTCREATESTRUCT ccs; ccs.hWindowMenu = GetSubMenu(GetMenu(hwnd), 2); ccs.idFirstChild = ID_MDI_FIRSTCHILD; g_hMDIClient = CreateWindowEx(WS_EX_CLIENTEDGE, "mdiclient", NULL, WS_CHILD | WS_CLIPCHILDREN | WS_VSCROLL | WS_HSCROLL | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, hwnd, (HMENU)IDC_MAIN_MDI, GetModuleHandle(NULL), (LPVOID)&ccs);
Ezt meg valaki fordítsa le nekem, mert éppen átmegy kínaiba!
The menu handle is the handle to the popup menu that the MDI client will add items to representing each window that is created, allowing the user to select the window they want to activate from the menu, we'll add functionality shortly to handle this case. In this example it's the 3rd popup (index 2) since I've added Edit and Window to the menu after File.
A ccs.idFirstChild egy szám, amit első ID-nek használunk az ablak menü kliens elemeihez. Ez egy olyan szám legyen, ami eléggé elkülönül az általunk használt ID-ktől. Legyen például 50.000 az értéke! Ekkora nagyságrendben már biztosan nem használunk ID-ket.
A megfelelő működéshez ki kell egészítsük a WM_COMMAND kezelőt.
case WM_COMMAND: switch(LOWORD(wParam)) { case ID_FILE_EXIT: PostMessage(hwnd, WM_CLOSE, 0, 0); break; // ... handle other regular IDs ... // Handle MDI Window commands default: { if(LOWORD(wParam) >= ID_MDI_FIRSTCHILD) { DefFrameProc(hwnd, g_hMDIClient, msg, wParam, lParam); } else { HWND hChild = (HWND)SendMessage(g_hMDIClient, WM_MDIGETACTIVE,0,0); if(hChild) { SendMessage(hChild, WM_COMMAND, wParam, lParam); } } } } break;
Hozzáadtunk egy default opciót. Ha üzenetet fogunk, akkor megnézzük hogy az kissebb-e mint ID_MDI_FIRSTCHILD. Mert ha igen, akkor a fő ablak kapja meg feldolgozásra ( DefFrameProc() ). Ha viszont nem, akkor a fő ablak gyermek ablaka kapja meg. Ez lehetővé teszi, hogy a gyermek ablakok akár teljesen különbözőképpen kezeljék az egyes parancsokat. A példában csak azokat az általános parancsokat dolgozzuk fel, amik a szülő ablaknak is részei.
Jön az üzenethurok módosítása
while(GetMessage(&Msg, NULL, 0, 0)) { if (!TranslateMDISysAccel(g_hMDIClient, &Msg)) { TranslateMessage(&Msg); DispatchMessage(&Msg); } }
Hozzáadtunk egy plusz lépést ( TranslateMDISysAccel() ), ez ellenőrzi az előre definiált gyors-billentyűket. Ctrl+F6 vált a következő ablakra, Ctrl+F4 bezárja az aktuális gyermek ablakot, stb. Ha ezt az ellenőrzést nem tennénk meg, akkor a felhasználó nem lenne képes használni a megszokott viselkedést, így azt nekünk magunknak kellene megvalósítanunk.
Gyermek ablak osztály
Amellett hogy van egy fő ablakunk (ami a szülő, vagy keret ablak), minden gyermek ablaknál szükséges megmondanunk annak típusát. Van szerkesztő-, kép-, stb ablaktípus. Ebben a példában csak egy féle, a szerkesztő típust használjuk.
BOOL SetUpMDIChildWindowClass(HINSTANCE hInstance) { WNDCLASSEX wc; wc.cbSize = sizeof(WNDCLASSEX); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = MDIChildWndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInstance; wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)(COLOR_3DFACE+1); wc.lpszMenuName = NULL; wc.lpszClassName = g_szChildClassName; wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); if(!RegisterClassEx(&wc)) { MessageBox(0, "Could Not Register Child Window", "Oh Oh...", MB_ICONEXCLAMATION | MB_OK); return FALSE; } else return TRUE; }
Ez lényegében megegyezik a hagyományos fő ablak regisztrálásával, nincsenek különleges flag-ek. A menü be van állítva NULL-ra, és a window eljárás a gyermek ablakra mutat, amit mindjárt megírunk.
MDI gyermek eljárás
Pár kivételtől eltekintve az MDI gyermek window eljárása megegyezik bármi máséval. A default üzenetet a DefMDIChildProc() kapja meg a DefWindowProc() helyett.
Szeretnénk lehetőséget tenni arra, hogy a menüben az Edit, és a Window menüpontok inaktívvá válhassanak, ha nincs rájuk szükség. Egyszerűen mert az jó dolog. Ezért használni fogunk egy WM_MDIACTIVATE parancsot, és ezzel engedjük vagy tiltjuk attól függően, hogy az ablak aktív, vagy nem. Ha különböző típusú ablakot használunk, akkor ezen a ponton avatkozhatunk be a működésbe olyanképp, hogy az egyes ablakok a tőlük elvárt módon viselkedjenek. A teljesség kedvéért tiltjuk a Close és Save as menüpontokat is.
LRESULT CALLBACK MDIChildWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { case WM_CREATE: { HFONT hfDefault; HWND hEdit; // Create Edit Control hEdit = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", "", WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL, 0, 0, 100, 100, hwnd, (HMENU)IDC_CHILD_EDIT, GetModuleHandle(NULL), NULL); if(hEdit == NULL) MessageBox(hwnd, "Could not create edit box.", "Error", MB_OK | MB_ICONERROR); hfDefault = GetStockObject(DEFAULT_GUI_FONT); SendMessage(hEdit, WM_SETFONT, (WPARAM)hfDefault, MAKELPARAM(FALSE, 0)); } break; case WM_MDIACTIVATE: { HMENU hMenu, hFileMenu; UINT EnableFlag; hMenu = GetMenu(g_hMainWindow); if(hwnd == (HWND)lParam) { //being activated, enable the menus EnableFlag = MF_ENABLED; } else { //being de-activated, gray the menus EnableFlag = MF_GRAYED; } EnableMenuItem(hMenu, 1, MF_BYPOSITION | EnableFlag); EnableMenuItem(hMenu, 2, MF_BYPOSITION | EnableFlag); hFileMenu = GetSubMenu(hMenu, 0); EnableMenuItem(hFileMenu, ID_FILE_SAVEAS, MF_BYCOMMAND | EnableFlag); EnableMenuItem(hFileMenu, ID_FILE_CLOSE, MF_BYCOMMAND | EnableFlag); EnableMenuItem(hFileMenu, ID_FILE_CLOSEALL, MF_BYCOMMAND | EnableFlag); DrawMenuBar(g_hMainWindow); } break; case WM_COMMAND: switch(LOWORD(wParam)) { case ID_FILE_OPEN: DoFileOpen(hwnd); break; case ID_FILE_SAVEAS: DoFileSave(hwnd); break; case ID_EDIT_CUT: SendDlgItemMessage(hwnd, IDC_CHILD_EDIT, WM_CUT, 0, 0); break; case ID_EDIT_COPY: SendDlgItemMessage(hwnd, IDC_CHILD_EDIT, WM_COPY, 0, 0); break; case ID_EDIT_PASTE: SendDlgItemMessage(hwnd, IDC_CHILD_EDIT, WM_PASTE, 0, 0); break; } break; case WM_SIZE: { HWND hEdit; RECT rcClient; // Calculate remaining height and size edit GetClientRect(hwnd, &rcClient); hEdit = GetDlgItem(hwnd, IDC_CHILD_EDIT); SetWindowPos(hEdit, NULL, 0, 0, rcClient.right, rcClient.bottom, SWP_NOZORDER); } return DefMDIChildProc(hwnd, msg, wParam, lParam); default: return DefMDIChildProc(hwnd, msg, wParam, lParam); } return 0; }
Beépítettük a File Open és Save as parancsokat. A DoFileOpen(), DoFileSave() közel ugyanaz mint a korábbi példában volt, megváltozott a szerkesztő vezérlő ID-je, és a gyermek ablak fejléce módosult a fájlnévre. A szerkesztési parancsok könnyűek, mert a szerkesztő vezérlő alapból tud mindent.
Most nézzük azt a bizonyos furcsa viselkedést! A WM_SIZE parancs végén a DefMDIChildProc() hívása nagyon ajánlott, különben a rendszernek nincs esélye elkapni a saját üzeneteit. Nézzük meg az MSDN-en a DefMDIChildProc() üzeneteit, és adjuk oda neki amit kér!
Ablak létrehozása, és bezárása
Az MDI gyermek ablakokat nem közvetlenül hozzuk létre, hanem küldünk egy WM_MDICREATE üzenetet a kilens ablaknak. Azt, hogy milyen ablakot szeretnénk létrehozni, a MDICREATESTRUCT mezői mondják meg. A mezők lehetséges értékeit a dokumentációban találjuk. A WM_MDICREATE visszatérési értékét az újonnan létrejött ablak handleréből tudjuk meg.
HWND CreateNewMDIChild(HWND hMDIClient) { MDICREATESTRUCT mcs; HWND hChild; mcs.szTitle = "[Untitled]"; mcs.szClass = g_szChildClassName; 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(hMDIClient, "MDI Child creation failed.", "Oh Oh...", MB_ICONEXCLAMATION | MB_OK); } return hChild; }
Az MDICREATESTRUCT egyik mezője nagyon hasznos, noha itt nem használtuk. Ez az lParam. Ezzel egy 32 bites értéket tudunk a gyermek számára küldeni (mint mutató), így tetszőleges adatot tudunk az új ablakkal megosztani. A gyermek ablak WM_CREATE handlerében létrehozunk egy CREATESTRUCT rekordot, amit ráállítunk lParam-ra. Ennek a rekordnak a lpCreatePArams mezője mutat a pMDICreateStruct rekordra. Ezt elmondani bonyolultabb mint leírni :)
case WM_CREATE: { CREATESTRUCT* pCreateStruct; MDICREATESTRUCT* pMDICreateStruct; pCreateStruct = (CREATESTRUCT*)lParam; pMDICreateStruct = (MDICREATESTRUCT*)pCreateStruct->lpCreateParams; /* pMDICreateStruct now points to the same MDICREATESTRUCT that you sent along with the WM_MDICREATE message and you can use it to access the lParam. */ } break;
Ezt lehet egyetlen lépésben is, és akkor nem kell két mutatóval szórakozni.
((MDICREATESTRUCT*)((CREATESTRUCT*)lParam)->lpCreateParams)->lParam
Most megvalósítjuk a File menüpontot a keret ablakunk menüjében.
case ID_FILE_NEW: CreateNewMDIChild(g_hMDIClient); break; case ID_FILE_OPEN: { HWND hChild = CreateNewMDIChild(g_hMDIClient); if(hChild) { DoFileOpen(hChild); } } break; case ID_FILE_CLOSE: { HWND hChild = (HWND)SendMessage(g_hMDIClient, WM_MDIGETACTIVE,0,0); if(hChild) { SendMessage(hChild, WM_CLOSE, 0, 0); } } break;
Végül egy kis apróság, ez már igazán nem sok:
case ID_WINDOW_TILE: SendMessage(g_hMDIClient, WM_MDITILE, 0, 0); break; case ID_WINDOW_CASCADE: SendMessage(g_hMDIClient, WM_MDICASCADE, 0, 0); break;
A bejegyzés trackback címe:
Kommentek:
A hozzászólások a vonatkozó jogszabályok értelmében felhasználói tartalomnak minősülnek, értük a szolgáltatás technikai üzemeltetője semmilyen felelősséget nem vállal, azokat nem ellenőrzi. Kifogás esetén forduljon a blog szerkesztőjéhez. Részletek a Felhasználási feltételekben és az adatvédelmi tájékoztatóban.