MDIアプリ
SDIとMDI
今まで作成してきたウィンドウアプリはSDIと呼ばれる方式のものです。
SDIはSingle Document Interfaceの略で、ひとつのアプリケーションウィンドウでひとつのドキュメントを操作するタイプのアプリです。
例えばWindows付属の「メモ帳」はSDIアプリです。
メモ帳で複数のテキストファイルを同時に編集するにはメモ帳を複数同時に立ち上げる必要があります。
これに対してMDIという方式のアプリがあります。
MDIはMultiple Document Interfaceの略で、ひとつのアプリケーションウィンドウ内に複数の子ウィンドウを作成し、複数のドキュメントを同時に編集可能なタイプのアプリです。
Microsoft Excelなどが代表的なMDIアプリです。
SDIアプリでも複数のウィンドウを同時に表示することは可能ですが、それぞれのウィンドウは独立しています。
MDIアプリは親ウィンドウの領域内にすべての子ウィンドウが含められ、はみ出したりすることあはりません。
MDIのウィンドウ構造
MDIアプリは親ウィンドウ内に子ウィンドウを持つことになりますが、プログラム上では三種類のウィンドウが使用されます。
まずトップレベル(最上位)のフレームウィンドウを作成します。
これはタイトルバーや閉じるボタンなどを持つ通常のウィンドウです。
次に、フレームウィンドウのクライアント領域にクライアントウィンドウを作成します。
クライアントウィンドウは子ウィンドウを配置する専用の特殊なウィンドウで、フレームウィンドウを親とします。
このウィンドウの作成にはMDICLIENT
というシステム定義済みのウィンドウクラスを使用します。
最後に、クライアントウィンドウを親ウィンドウとする子ウィンドウを作成します。
各ウィンドウで独立したデータ(文書)を扱うのでドキュメントウィンドウとも言います。
子ウィンドウは必要なだけいくつでも作成することが出来ます。
子ウィンドウ用のウィンドウクラスはあらかじめシステムに登録する必要があります。
「クライアントウィンドウ」の「子ウィンドウ」であって、トップレベルウィンドウから見れば孫ウィンドウに当たります。
(「フレームウィンドウ」→「クライアントウィンドウ」→「子ウィンドウ」という関係)
まずはごく初歩的なMDIアプリを作成してみます。
随所にサンプルコードを掲載してるのでページが長くなっていますが、それほど複雑ではありません。
フレームウィンドウの作成
フレームウィンドウはアプリケーションのトップレベルウィンドウです。
SDIアプリのウィンドウと基本は同じですが、ウィンドウプロシージャの記述方法が少し異なります。
//ウィンドウクラスの登録
BOOL MyRegisterClass(HINSTANCE hInstance)
{
//フレームウィンドウのウィンドウクラス
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = FrameWndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_APPWORKSPACE + 1); //背景色
wcex.lpszMenuName = NULL;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = NULL;
if (!RegisterClassEx(&wcex))
return FALSE;
return TRUE;
}
//フレームウィンドウプロシージャ
LRESULT CALLBACK FrameWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HWND hClientWnd;
switch (message)
{
case WM_DESTROY: //ウィンドウの破棄
PostQuitMessage(0);
return 0;
}
return DefFrameProc(hWnd, hClientWnd, message, wParam, lParam);
}
ウィンドウプロシージャはフレームウィンドウ用と後に作成する子ウィンドウ用とが必要になるので、分かりやすいように名前を「FrameWndProc」に変更します。
背景色を「COLOR_WINDOW + 1」から「COLOR_APPWORKSPACE + 1」に変更し、濃いグレーにしています。
(実際の色はユーザーの設定により異なる)
最も重要な点は、デフォルトウィンドウプロシージャをDefWindowProc関数からDefFrameProc
関数に変更していることです。
これはフレームウィンドウの決まりです。
- LRESULT DefFrameProcW(
HWND hWnd,
HWND hWndMDIClient,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
); - MDIフレームウィンドウのデフォルトの動作を提供するウィンドウプロシージャ。
DefFrameProc関数との違いは、第二引数にhWndMDIClient
の指定が増えている点です。
これは次に作成するクライアントウィンドウのハンドルを指定します。
その他の引数はDefFrameProc関数と同じです。
今の時点では変数hClientWndは空なのでこのコードは実行できません。
クライアントウィンドウの作成
クライアントウィンドウはウィンドウプロシージャのWM_CREATE
メッセージ内で、CreateWindow関数またはCreateWindowEx関数で作成します。
//フレームウィンドウプロシージャ
LRESULT CALLBACK FrameWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HWND hClientWnd;
CLIENTCREATESTRUCT ccs;
switch (message)
{
case WM_CREATE: //ウィンドウの作成
ccs.hWindowMenu = NULL;
ccs.idFirstChild = 100;
//クライアントウィンドウの作成
hClientWnd = CreateWindow(L"MDICLIENT", NULL,
WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL,
0, 0, 0, 0, hWnd, (HMENU)1, hInst, &ccs);
);
return 0;
case WM_DESTROY: //ウィンドウの破棄
PostQuitMessage(0);
return 0;
}
return DefFrameProc(hWnd, hClientWnd, message, wParam, lParam);
}
ウィンドウクラスにはMDICLIENT
という文字列を指定します。
これはシステム定義のウィンドウクラスです。
(ボタンコントロールの作成にBUTTON
クラスを使用するのと同じ)
ウィンドウスタイルはWS_CHILD
とWS_CLIPCHILDREN
が必要です。
WS_CLIPCHILDRENは、子ウィンドウが配置されている領域は描画しないという指定です。
(子ウィンドウは独自に描画をするため、裏に隠れる領域は描画する必要がない)
WS_VISIBLE
は必須ではありませんが、通常は同時に指定しておきます。
クライアントウィンドウの位置とサイズはすべて0
を指定します。
親ウィンドウには引数hWnd(フレームウィンドウのウィンドウハンドルが格納されている)を指定します。
ウィンドウIDには1
を指定します。
(何でも良い)
CLIENTCREATESTRUCT構造体
引数の最後はCLIENTCREATESTRUCT
構造体のポインタを渡します。
- typedef struct tagCLIENTCREATESTRUCT {
HANDLE hWindowMenu;
UINT idFirstChild;
} CLIENTCREATESTRUCT, *LPCLIENTCREATESTRUCT; - MDIクライアントウィンドウ作成時に使用される情報を格納する構造体。
hWindowMenu
メンバはフレームウィンドウのウィンドウメニューハンドルを指定します。
今はメニューを作成していないためNULL
を指定していますが、MDIアプリはメニューの作成はほぼ必須です。
ここは次ページで変更することにします。
idFirstChild
メンバは子ウィンドウのID(識別子)を指定します。
ここで指定した数値が最初の子ウィンドウのIDとして使用され、子ウィンドウが増えるたびに1ずつ加算されたIDが使用されます。
このIDは他のコマンドIDやメニューIDと重複しないようにする必要があります。
最後に、戻り値をstatic変数に格納します。
これは先ほど説明したDefFrameProc関数で使用します。
子ウィンドウの作成
子ウィンドウは、作成する前に子ウィンドウ用のウィンドウクラスを作成して登録する必要があります。
//グローバル変数
HINSTANCE hInst; //インスタンス
WCHAR szTitle[] = L"テストアプリ"; //タイトル
WCHAR szWindowClass[] = L"MyTestApp"; //ウィンドウクラス名
WCHAR szWindowClass_MDIChild[] = L"MyTestApp_MDIChild"; //MDI子ウィンドウクラス名
//ウィンドウクラスの登録
BOOL MyRegisterClass(HINSTANCE hInstance)
{
//フレームウィンドウのウィンドウクラス
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
//wcex.lpfnWndProc = DefWindowProc;
wcex.lpfnWndProc = FrameWndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_APPWORKSPACE + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = NULL;
if (!RegisterClassEx(&wcex))
return FALSE;
//子ウィンドウのウィンドウクラス
wcex.lpfnWndProc = (WNDPROC)DefMDIChildProc;
wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = szWindowClass_MDIChild;
if (!RegisterClassEx(&wcex))
return FALSE;
return TRUE;
}
MyRegisterClass関数に子ウィンドウのウィンドウクラスの登録処理を追加しています。
ウィンドウプロシージャにはDefMDIChildProc
関数を指定しています。
これはMDI子ウィンドウのデフォルトの動作を提供する関数です。
実際には独自定義した子ウィンドウ用のウィンドウプロシージャをここに指定します。
複数の種類の子ウィンドウを作成する場合はウィンドウクラスも複数登録する必要があります。
CreateMDIWindow関数
子ウィンドウの作成方法は二通りあります。
ひとつはCreateMDIWindow
関数を使用する方法です。
- HWND CreateMDIWindowW(
LPCWSTR lpClassName,
LPCWSTR lpWindowName,
DWORD dwStyle,
int X,
int Y,
int nWidth,
int nHeight,
HWND hWndParent,
HINSTANCE hInstance,
LPARAM lParam
); - MDI子ウィンドウを作成する。
戻り値は作成したウィンドウのハンドル。
失敗した場合はNULLを返す。
引数が多いですが、基本的にCreateWindow関数と同じと考えて大丈夫です。
メニューハンドルの指定(子ウィンドウIDの指定)がありませんが、MDI子ウィンドウのIDはクライアントウィンドウ作成時に使用したCLIENTCREATESTRUCT
構造体のidFirstChild
メンバから自動的に生成されます。
//フレームウィンドウプロシージャ
LRESULT CALLBACK FrameWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HWND hClientWnd;
CLIENTCREATESTRUCT ccs;
switch (message)
{
case WM_CREATE:
ccs.hWindowMenu = NULL;
ccs.idFirstChild = 100;
//クライアントウィンドウの作成
hClientWnd = CreateWindow(L"MDICLIENT", NULL,
WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE,
0, 0, 0, 0, hWnd, (HMENU)1, hInst, &ccs);
//子ウィンドウの作成
CreateMDIWindow(szWindowClass_MDIChild, L"MDI子ウィンドウ", 0,
CW_USEDEFAULT, CW_USEDEFAULT,
300, 200,
hClientWnd, hInst, 0
);
return 0;
case WM_DESTROY: //ウィンドウの破棄
PostQuitMessage(0);
return 0;
}
return DefFrameProc(hWnd, hClientWnd, message, wParam, lParam);
}
今回はクライアントウィンドウ作成の直後に子ウィンドウを作成しています。
ウィンドウクラス名は先ほどシステムに登録したものを指定します。
親ウィンドウはクライアントウィンドウ(hClientWnd)を指定します。
ここまでのコードでMDIアプリのごく基本的な部分が完成します。
一通りのコードを掲載します。
#include <windows.h>
//グローバル変数
HINSTANCE hInst; //インスタンス
WCHAR szTitle[] = L"テストアプリ"; //タイトル
WCHAR szWindowClass[] = L"MyTestApp"; //ウィンドウクラス名
WCHAR szWindowClass_MDIChild[] = L"MyTestApp_MDIChild"; //MDI子ウィンドウクラス名
//関数プロトタイプ宣言
BOOL MyRegisterClass(HINSTANCE);
BOOL InitInstance(HINSTANCE, int);
//ウィンドウプロシージャのプロトタイプ宣言
LRESULT CALLBACK FrameWndProc(HWND, UINT, WPARAM, LPARAM);
int APIENTRY wWinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPWSTR lpCmdLine,
int nCmdShow)
{
MyRegisterClass(hInstance);
if (!InitInstance(hInstance, nCmdShow))
{
return 0;
}
MSG msg;
BOOL ret;
//メッセージループ
while ((ret = GetMessage(&msg, NULL, 0, 0)) != 0)
{
if (ret == -1)
{
return -1;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
//ウィンドウクラスの登録
BOOL MyRegisterClass(HINSTANCE hInstance)
{
//フレームウィンドウのウィンドウクラス
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = FrameWndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_APPWORKSPACE + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = NULL;
if (!RegisterClassEx(&wcex))
return FALSE;
//子ウィンドウのウィンドウクラス
wcex.lpfnWndProc = (WNDPROC)DefMDIChildProc;
wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = szWindowClass_MDIChild;
if (!RegisterClassEx(&wcex))
return FALSE;
return TRUE;
}
//ウィンドウの作成
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance;
HWND hWnd = CreateWindow(
szWindowClass, szTitle,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL
);
if (!hWnd) {
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
return TRUE;
}
//フレームウィンドウプロシージャ
LRESULT CALLBACK FrameWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HWND hClientWnd;
CLIENTCREATESTRUCT ccs;
switch (message)
{
case WM_CREATE:
ccs.hWindowMenu = NULL;
ccs.idFirstChild = 100;
//クライアントウィンドウの作成
hClientWnd = CreateWindow(L"MDICLIENT", NULL,
WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL,
0, 0, 0, 0, hWnd, (HMENU)1, hInst, &ccs);
//子ウィンドウの作成
CreateMDIWindow(szWindowClass_MDIChild, L"MDI子ウィンドウ", 0,
CW_USEDEFAULT, CW_USEDEFAULT,
300, 200,
hClientWnd, hInst, 0
);
return 0;
case WM_DESTROY: //ウィンドウの破棄
PostQuitMessage(0);
return 0;
}
return DefFrameProc(hWnd, hClientWnd, message, wParam, lParam);
}
見た目はMDIアプリになっていますが、まだ不十分です。
特に、子ウィンドウを最大化すると元に戻せなくなるという致命的な欠陥があります。
WM_MDICREATEメッセージ
MDI子ウィンドウは、クライアントウィンドウにWM_MDICREATE
メッセージを送信することでも作成できます。
このメッセージではMDICREATESTRUCT
構造体を使用します。
- typedef struct tagMDICREATESTRUCTW {
LPCWSTR szClass;
LPCWSTR szTitle;
HANDLE hOwner;
int x;
int y;
int cx;
int cy;
DWORD style;
LPARAM lParam;
} MDICREATESTRUCTW, *LPMDICREATESTRUCTW; - MDI子ウィンドウに関する情報を格納する構造体。
メンバは親ウィンドウの指定が無い以外はCreateMDIWindow関数と同じです。
親ウィンドウはWM_MDICREATEメッセージを送信したウィンドウ(つまりクライアントウィンドウ)に設定されます。
メッセージはSendMessage関数で送信します。
送信先はクライアントウィンドウを、メッセージの種類にはWM_MDICREATEを指定します。
WPARAMは使用しません。
LPARAMには必要な情報を格納したMDICREATESTRUCT構造体のポインタを指定します。
//フレームウィンドウプロシージャ
LRESULT CALLBACK FrameWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HWND hClientWnd;
CLIENTCREATESTRUCT ccs;
MDICREATESTRUCT mcs;
switch (message)
{
case WM_CREATE:
ccs.hWindowMenu = NULL;
ccs.idFirstChild = 100;
//クライアントウィンドウの作成
hClientWnd = CreateWindow(L"MDICLIENT", NULL,
WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL,
0, 0, 0, 0, hWnd, (HMENU)1, hInst, &ccs);
//子ウィンドウの作成
//CreateMDIWindow(szWindowClass_MDIChild, L"MDI子ウィンドウ", 0,
// CW_USEDEFAULT, CW_USEDEFAULT,
// 300, 200,
// hClientWnd, hInst, 0
//);
//子ウィンドウの作成
mcs.szClass = szWindowClass_MDIChild;
mcs.szTitle = L"MDI子ウィンドウ";
mcs.style = 0;
mcs.x = CW_USEDEFAULT;
mcs.y = CW_USEDEFAULT;
mcs.cx = 300;
mcs.cy = 200;
mcs.hOwner = hInst;
mcs.lParam = 0;
SendMessage(hClientWnd, WM_MDICREATE, 0, (LPARAM)&mcs);
return 0;
case WM_DESTROY: //ウィンドウの破棄
PostQuitMessage(0);
return 0;
}
return DefFrameProc(hWnd, hClientWnd, message, wParam, lParam);
}
実行結果はCreateMDIWindow関数の時と同じです。
子ウィンドウのスタイル
子ウィンドウのスタイルには以下の定数の組み合わせが指定できます。
定数 | 説明 |
---|---|
WS_MINIMIZE | 子ウィンドウを最小化された状態で作成する |
WS_MAXIMIZE | 子ウィンドウを最大化された状態で作成する |
WS_HSCROLL | 水平スクロールバーを表示する |
WS_VSCROLL | 垂直スクロールバーを表示する |
また、クライアントウィンドウのスタイルにMDIS_ALLCHILDSTYLES
を指定すると、CreateWindow関数で使用できるウィンドウスタイルをMDI子ウィンドウでも使用できるようになります。