第118章 分割ウィンドウもどきを作る その2


今回は、親ウィンドウに2つの子ウィンドウを貼り付けて ツリービューを各子ウィンドウに表示してみます。

左の図のようなウィンドウを作ります。 表示ウィンドウに表示しきれないときはスクロールバーが出ます。 右、左の子ウィンドウの位置、大きさの計算に誤差があると スクロールバーが出たときにみっともないことになります。



これは、どのように作ってあるかというと左の子ウィンドウは WS_EX_CLIENTEDGEの拡張スタイルにして境界を少し太いものにしています。 右の子ウィンドウはWS_BORDERで非常に細い枠にしています。 実は右のウィンドウの枠は無くてもかまいません。 しかし、細い枠を付けておくとウィンドウの大きさが変化したときに 正確に位置、大きさが変化したかどうかの確認がとりやすいです。

さて、子ウィンドウの位置、大きさの関係はどのようにすれば よいのでしょうか。じつは、これが簡単そうでいて結構頭が混乱します。

1.親のウィンドウ幅、高さをx0, y0とする 2.右ウィンドウの幅、高さをx1, y1とする 3.左ウィンドウの幅、高さをx2, y2とする 4.親のクライアント領域の幅、高さをcx0, cy0とする 5.左ウィンドウの境界(ウィンドウ枠)の幅をfとする 6.cx0 = (x1 - f) + x2 7.cy0 = y1 - 2*f, cy0 = cy2 8.左ウィンドウの始点(-f, -f) 9.右ウィンドウの始点(cx0 - x1 + f, 0)

ウィンドウ幅と、クライアント領域の幅の差ををきちんと計算しないと 微妙な誤差が出て、左右のウィンドウの間に隙間ができたりします。

さて、中央に見える左右の区切りは左ウィンドウの右側のウィンドウ境界です。 親ウィンドウの横幅が変更になったときは左ウィンドウの幅は変化しません。 右のウィンドウの幅が変わります。(エクスプローラなどはそのように動作します) 親ウィンドウの高さが変化したら左右の子ウィンドウの高さも変化します。

中央の境界をドラッグして移動させると左右のウィンドウの幅が変わります。 以上のことを考えてプログラムを書くことになります。 しかし、ここで非常に微妙な問題が生じます。

0.各ウィンドウのサイズをグローバル変数にしておき   サイズ変更があったら書き込んでおく 1.親のプロシージャにWM_SIZEが来たとき   左右のウィンドウの大きさを計算してそれぞれがきちんと収まるようにする 2.この時左右のウィンドウの大きさが変化したのでそれぞれのプロシージャにも   WM_SIZEメッセージが届く 3.一方、中央の境界を動かしたときにも左ウィンドウのプロシージャに   WM_SIZEメッセージが届く   このメッセージ処理のところで右ウィンドウの位置、大きさを計算して   正しく表示されるようにする

つまり、親のサイズ変更があった場合右ウィンドウはまず親のプロシージャで そのサイズ、位置が変更になります。左ウィンドウもサイズ(高さ)が変更されれば 左ウィンドウのプロシージャでも右ウィンドウの大きさ、位置を変えようとします。 しかし、それぞれのプロシージャが実行される時期は予測が付きません。 実際上の考えに基づいてプログラムを書くと、親のサイズをゆっくり変更しても 何も問題は起こりません。しかし、素早く動かすと右ウィンドウの高さが おかしくなることがあります。多分、グローバル変数の値を読むタイミングが 親と左ウィンドウのプロシージャで一定していないためと思われます。 (たとえは適切でないかもしれませんが、マルチスレッドでスレッド間共有変数に アクセスするときの問題に似ています。)

そこで親のプロシージャで子供の位置、大きさを変更している最中は 左プロシージャではWM_SIZEがきても何もしないようにしておいてはどうでしょうか。 どうも、これが解決策のようです。

// split02.cpp #define STRICT #include <windows.h> #include <commctrl.h> #define ID_MYTREE 100 LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); LRESULT CALLBACK MyChildProc(HWND, UINT, WPARAM, LPARAM); LRESULT CALLBACK MyChildProc2(HWND, UINT, WPARAM, LPARAM); BOOL InitApp(HINSTANCE, WNDPROC, LPCTSTR); BOOL InitInstance(HINSTANCE, int); void AddItem(HWND hTree); int GetMyFrameWidth(HWND); char szClassName[] = "split02"; //ウィンドウクラス int x0, y0, x1=200, y1, x2, y2; //ウィンドウの幅、高さ int frame; HWND hChild2; BOOL bParentSizeChange = FALSE; int WINAPI WinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst, LPSTR lpsCmdLine, int nCmdShow) { MSG msg; if (!InitApp(hCurInst, WndProc, szClassName)) return FALSE; if (!InitInstance(hCurInst, nCmdShow)) return FALSE; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; }

WinMain自体はいつもと同じです。しかし、InitApp関数は 子供を作るときにも利用できるように引数を変えています。 また、今回はコモンコントロールを使いますので、その準備も 忘れないでください。(comctl32.libのリンクなど)

//ウィンドウ・クラスの登録 BOOL InitApp(HINSTANCE hInst, WNDPROC fnWndProc, LPCTSTR szClassNm) { WNDCLASSEX wc; wc.cbSize = sizeof(WNDCLASSEX); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = fnWndProc; //プロシージャ名 wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInst; //インスタンス wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wc.lpszMenuName = NULL; //メニュー名 wc.lpszClassName = (LPCSTR)szClassNm; wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); return (RegisterClassEx(&wc)); }

やっていることはいつもと同じですが、再利用できるように引数に 小細工をしています。

//ウィンドウの生成 BOOL InitInstance(HINSTANCE hInst, int nCmdShow) { HWND hWnd; hWnd = CreateWindow(szClassName, "猫でもわかる分割ウィンドウ",//タイトルバーにこの名前が表示されます WS_OVERLAPPEDWINDOW, //ウィンドウの種類 CW_USEDEFAULT, //X座標 CW_USEDEFAULT, //Y座標 CW_USEDEFAULT, //幅 CW_USEDEFAULT, //高さ NULL, //親ウィンドウのハンドル、親を作るときはNULL NULL, //メニューハンドル、クラスメニューを使うときはNULL hInst, //インスタンスハンドル NULL); if (!hWnd) return FALSE; ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); return TRUE; }

これは、いつもと同じです。

//ウィンドウプロシージャ LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) { int id; HINSTANCE hInst; static HWND hChild1; RECT rc; switch (msg) { case WM_CREATE: frame = GetSystemMetrics(SM_CXSIZEFRAME); hInst = ((LPCREATESTRUCT)lp)->hInstance; GetWindowRect(hWnd, &rc); x0 = rc.right - rc.left; y0 = rc.bottom - rc.top; GetClientRect(hWnd, &rc); y1 = rc.bottom + frame*2; InitApp(hInst, MyChildProc, "mychild1"); hChild1 = CreateWindowEx(WS_EX_CLIENTEDGE, "mychild1", "", //タイトルバーにこの名前が表示されます WS_CHILD | WS_VISIBLE | WS_THICKFRAME, //ウィンドウの種類 -frame, //X座標 -frame, //Y座標 x1, //幅 y1, //高さ hWnd, //親ウィンドウのハンドル、親を作るときはNULL (HMENU)1, //メニューハンドル、クラスメニューを使うときはNULL hInst, //インスタンスハンドル NULL); GetClientRect(hWnd, &rc); x2 = rc.right - x1 + frame; y2 = rc.bottom; InitApp(hInst, MyChildProc2, "mychild2"); hChild2 = CreateWindow("mychild2", "", WS_CHILD | WS_VISIBLE | WS_BORDER, x1 - frame, 0, x2, y2, hWnd, (HMENU)2, hInst, NULL); break; case WM_SIZE: bParentSizeChange = TRUE; GetWindowRect(hWnd, &rc); x0 = rc.right - rc.left; if (x0 <= x1) x1 = x0 - frame; y0 = rc.bottom - rc.top; GetClientRect(hWnd, &rc); y1 = rc.bottom + frame*2; x2 = rc.right - x1 +frame; y2 = rc.bottom; MoveWindow(hChild1, -frame, -frame, x1, y1, TRUE); MoveWindow(hChild2, x1-frame , 0, x2, y2, TRUE); bParentSizeChange = FALSE; break; case WM_CLOSE: id = MessageBox(hWnd, "終了してもよいですか", "終了確認", MB_YESNO | MB_ICONQUESTION); if (id == IDYES) { DestroyWindow(hWnd); } break; case WM_DESTROY: PostQuitMessage(0); break; default: return (DefWindowProc(hWnd, msg, wp, lp)); } return 0; }

WM_CREATEメッセージが来たら、まず ウィンドウ枠の太さをframeに保存しておきます。 次に自分のウィンドウサイズを グローバル変数に保存しておきます。

次にいつもと引数を変更したInitApp関数でウィンドウクラスを 登録してCreateWindowEX関数で左子ウィンドウを作成します。 WS_EX_CLIENTEDGEスタイルを指定して枠を太くします。

親のクライアント領域の幅、高さを求めてこれより、 右ウィンドウの幅、高さを計算しておきます。

この幅、高さを使って右ウィンドウを作ります。

WM_SIZEメッセージが来たら、まずbParentSizeChange変数を TRUEにしておきます。 左右のウィンドウの位置、大きさを計算して それぞれのウィンドウを正しい位置、大きさにします。 ここでは、単純にMoveWindow関数を使っていますが、 複数のウィンドウを動かすときは普通は DeferWindowPos関数一派を使います。 使い方はそんなに難しくありません。ヘルプで調べて 書き換えてみてください。 ウィンドウの移動が終わったら再びbParentSizeChangeを FALSEに戻しておきます。

LRESULT CALLBACK MyChildProc(HWND hChild, UINT msg, WPARAM wp, LPARAM lp) { RECT rc; static HWND hTree; HINSTANCE hInst; HWND hParent; switch (msg) { case WM_CREATE: hInst = ((LPCREATESTRUCT)lp)->hInstance; InitCommonControls(); //コモンコントロールの初期化 hTree = CreateWindowEx(0, WC_TREEVIEW, "", WS_CHILD | WS_BORDER | WS_VISIBLE | TVS_HASLINES | TVS_HASBUTTONS | TVS_LINESATROOT, 0, 0, 0, 0, hChild, (HMENU)ID_MYTREE, hInst, NULL); AddItem(hTree); break; case WM_SIZE: if (bParentSizeChange == FALSE) { hParent = GetParent(hChild); GetWindowRect(hChild, &rc); x1 = rc.right - rc.left; y1 = rc.bottom - rc.top; GetClientRect(hParent, &rc); x2 = rc.right- x1 +frame; y2 = rc.bottom; MoveWindow(hChild2, x1 - frame, 0, x2, y2, TRUE); } MoveWindow(hTree, 0, 0, LOWORD(lp), HIWORD(lp), TRUE); break; default: return (DefWindowProc(hChild, msg, wp, lp)); } return 0; }

左子ウィンドウのプロシージャです。

WM_CREATEメッセージが来たらツリービューを作ります。

WM_SIZEメッセージはユーザーが直接このウィンドウのサイズを 変更したときと、親のサイズが変更になったときにやってきます。 親のサイズ変更で一連の子供ウィンドウが動かされたときはここでは 子供のサイズについては何もしないようにしています。(bParentSizeChange=TRUEの時) 当然ツリービューのサイズは変更します。

LRESULT CALLBACK MyChildProc2(HWND hChild, UINT msg, WPARAM wp, LPARAM lp) { static HWND hTree; HINSTANCE hInst; switch (msg) { case WM_CREATE: hInst = ((LPCREATESTRUCT)lp)->hInstance; InitCommonControls(); //コモンコントロールの初期化 hTree = CreateWindowEx(0, WC_TREEVIEW, "", WS_CHILD | WS_BORDER | WS_VISIBLE | TVS_HASLINES | TVS_HASBUTTONS | TVS_LINESATROOT, 0, 0, 0, 0, hChild, (HMENU)ID_MYTREE, hInst, NULL); AddItem(hTree); break; case WM_SIZE: MoveWindow(hTree, 0, 0, LOWORD(lp), HIWORD(lp), TRUE); break; default: return (DefWindowProc(hChild, msg, wp, lp)); } return 0; }

右子ウィンドウのプロシージャです。

WM_CREATEが来たときにツリービューを作ります。

WM_SIZEが来たときはツリービューの大きさを調整します。

void AddItem(HWND hTree) { HTREEITEM hParent1, hParent2, hParent3, hChild1, hChild2; TV_INSERTSTRUCT tv; memset((char *)&tv, '\0', sizeof(tv)); tv.hInsertAfter = TVI_LAST; tv.item.mask = TVIF_TEXT; tv.hParent = TVI_ROOT; tv.item.pszText = "粂井"; hParent1 = TreeView_InsertItem(hTree, &tv); tv.item.pszText = "田中"; hParent2 = TreeView_InsertItem(hTree, &tv); tv.item.pszText = "佐藤"; hParent3 = TreeView_InsertItem(hTree, &tv); tv.hParent = hParent1; tv.item.pszText = "康孝"; hChild1 = TreeView_InsertItem(hTree, &tv); tv.item.pszText = "ひとみ"; hChild2 = TreeView_InsertItem(hTree, &tv); tv.hParent = hChild1; tv.item.pszText = "志麻"; TreeView_InsertItem(hTree, &tv); tv.hParent = hChild1; tv.item.pszText = "櫻都"; TreeView_InsertItem(hTree, &tv); tv.hParent = hParent2; tv.item.pszText = "マイケル"; TreeView_InsertItem(hTree, &tv); tv.hParent = hParent3; tv.item.pszText = "パトリシア"; TreeView_InsertItem(hTree, &tv); return; }

ツリービューに項目を追加する関数です。

今回はいろいろ面倒な問題が出てきました。 まだ、完全ではありません。中央の境界をずーっと左に寄せると 境界線をドラッグできなくなるという欠点があります。 工夫してみてください。


[SDK第2部 Index] [総合Index] [Previous Chapter] [Next Chapter]

Update Apr/09/1998 By Y.Kumei
当ホーム・ページの一部または全部を無断で複写、複製、 転載あるいはコンピュータ等のファイルに保存することを禁じます。