第126章 穴あきウィンドウを作る


今回は、表題の通り穴あきウィンドウを作ります。 クライアント領域の一部に穴があいていてその穴を通して 下にあるウィンドウを見たり、クリックしたりすることができます。 サッシのよい方はすでに作り方がわかったと思います。 前章の「丸いウィンドウ」と同じ作り方です。



クライアント領域に「星形」の穴があいて、下にある デスクトップの一部が見えています。その他穴の形はメニューから 「長方形」「丸」「三角形」が選べます。

ウィンドウをよく観察してみると 「最大化ボタン」がついており、ウィンドウ枠も大きさの変えられる 枠のようです。しかし、ここではウィンドウの大きさを変えられては 都合が悪いこともあるのでプログラムで大きさを変えられないように してあります。



では、プログラムを見てみましょう。

// round02.rcの一部 ///////////////////////////////////////////////////////////////////////////// // // Menu // MYMENU MENU DISCARDABLE BEGIN POPUP "ファイル(&F)" BEGIN MENUITEM "終了(&X)", IDM_END END POPUP "穴の種類(&A)" BEGIN MENUITEM "長方形(&R)", IDM_RECT MENUITEM "三角形(&T)", IDM_TRI MENUITEM "丸(&C)", IDM_RND MENUITEM "星形(&S)", IDM_STR END END

これは、普通のメニューリソースです。

// round02.cpp #define STRICT #include <windows.h> #include "resource.h" #define WINX 300 #define WINY 200 LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); BOOL InitApp(HINSTANCE); BOOL InitInstance(HINSTANCE, int); char szClassName[] = "round02"; //ウィンドウクラス int WINAPI WinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst, LPSTR lpsCmdLine, int nCmdShow) { MSG msg; if (!InitApp(hCurInst)) return FALSE; if (!InitInstance(hCurInst, nCmdShow)) return FALSE; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; }

ウィンドウの大きさを300*200にします。WINX, WINYという定数を 定義してみました。

//ウィンドウ・クラスの登録 BOOL InitApp(HINSTANCE hInst) { WNDCLASSEX wc; wc.cbSize = sizeof(WNDCLASSEX); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = WndProc; //プロシージャ名 wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInst; //インスタンス wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wc.lpszMenuName = "MYMENU"; //メニュー名 wc.lpszClassName = (LPCSTR)szClassName; 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座標 WINX,//幅 WINY,//高さ NULL,//親ウィンドウのハンドル、親を作るときはNULL NULL,//メニューハンドル、クラスメニューを使うときはNULL hInst,//インスタンスハンドル NULL); if (!hWnd) return FALSE; ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); return TRUE; }

これもいつもとほぼ同じです。ウィンドウの大きさをWINX*WINYに 指定しています。

//ウィンドウプロシージャ LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) { int id; static HRGN hRgn1, hRgn2, hRgn3, hRgn, hRgnX; WINDOWPLACEMENT wndpl; RECT rc; static int def; //メニュー、タイトルバーの高さ POINT pta[3], ptb[3]; switch (msg) { case WM_CREATE: def = GetSystemMetrics(SM_CYMENUSIZE) + GetSystemMetrics(SM_CYCAPTION); hRgn = CreateRectRgn(0, 0, 1, 1); hRgn1 = CreateRectRgn(0, 0, 1, 1); hRgn2 = CreateRectRgn(0, 0, 1, 1); hRgn3 = CreateRectRgn(0, 0, 1, 1); hRgnX = CreateRectRgn(0, 0, 1, 1); break; case WM_COMMAND: switch (LOWORD(wp)) { case IDM_END: SendMessage(hWnd, WM_CLOSE, 0, 0); break; case IDM_RECT: hRgn = CreateRectRgn(0, 0, 1, 1); hRgn1 = CreateRectRgn(0, 0, WINX, WINY); hRgn2 = CreateRectRgn(30, 30 + def, WINX - 30, WINY - 30); CombineRgn(hRgn, hRgn1, hRgn2, RGN_DIFF); SetWindowRgn(hWnd, hRgn, TRUE); break; case IDM_RND: hRgn = CreateRectRgn(0, 0, 1, 1); hRgn1 = CreateRectRgn(0, 0, WINX, WINY); hRgn2 = CreateEllipticRgn(30, 30+def, WINX -30, WINY - 30); CombineRgn(hRgn, hRgn1, hRgn2, RGN_DIFF); SetWindowRgn(hWnd, hRgn, TRUE); break; case IDM_STR: pta[0].x = 70; pta[0].y = WINY - 50; pta[1].x = WINX / 2; pta[1].y = 10 + def; pta[2].x = WINX - 70; pta[2].y = WINY - 50; ptb[0].x = 70; ptb[0].y = 50 + def; ptb[1].x = WINX - 70; ptb[1].y = 50 + def; ptb[2].x = WINX / 2; ptb[2].y = WINY - 10; hRgn = CreateRectRgn(0, 0, 1, 1); hRgnX = CreateRectRgn(0, 0, 1, 1); hRgn1 = CreateRectRgn(0, 0, WINX, WINY); hRgn2 = CreatePolygonRgn(pta, 3, WINDING); hRgn3 = CreatePolygonRgn(ptb, 3, WINDING); CombineRgn(hRgnX, hRgn2, hRgn3, RGN_OR); CombineRgn(hRgn, hRgn1, hRgnX, RGN_DIFF); SetWindowRgn(hWnd, hRgn, TRUE); break; case IDM_TRI: pta[0].x = 30; pta[0].y = WINY - 30; pta[1].x = WINX - 30; pta[1].y = WINY - 30; pta[2].x = WINX / 2; pta[2].y = def + 30; hRgn = CreateRectRgn(0, 0, 1, 1); hRgnX = CreateRectRgn(0, 0, 1, 1); hRgn1 = CreateRectRgn(0, 0, WINX, WINY); hRgn2 = CreatePolygonRgn(pta, 3, WINDING); CombineRgn(hRgn, hRgn1, hRgn2, RGN_DIFF); SetWindowRgn(hWnd, hRgn, TRUE); break; } break; case WM_SIZE: wndpl.length = sizeof(WINDOWPLACEMENT); GetWindowPlacement(hWnd, &wndpl); rc = wndpl.rcNormalPosition; MoveWindow(hWnd, rc.left, rc.top, WINX, WINY, TRUE); break; case WM_CLOSE: id = MessageBox(hWnd, "終了してもよいですか", "終了確認", MB_YESNO | MB_ICONQUESTION); if (id == IDYES) { DestroyWindow(hWnd); } break; case WM_DESTROY: DeleteObject(hRgn); DeleteObject(hRgn1); DeleteObject(hRgn2); DeleteObject(hRgn3); DeleteObject(hRgnX); PostQuitMessage(0); break; default: return (DefWindowProc(hWnd, msg, wp, lp)); } return 0L; }

ちょっと長いのですがメッセージごとに見ていくと簡単です。 ウィンドウができたらすぐにリージョンハンドルを 初期化しておきます。なぜこんなことをするかというと 起動してすぐに終了するとき、無効なリージョンハンドルがあると DeleteObjectが失敗するからです。 (別に失敗しても差し支えはありません。はじめからリージョンが 存在しないのでDeleteObjectに失敗しても大丈夫です。)

また、defはタイトルバーとメニューバーの高さを合計したものです。 これを求めておくとクライアント領域に美しく?穴をあけるのに 役立ちます。

メニューからIDM_RECT(「長方形」)が選択されると、 ウィンドウ全体と同じ大きさのリージョン(hRgn1)と これより一回り小さいリージョン(hRgn2)を作ります。 そして、この2つをRGN_DIFFで合成します(hRgn)。 これをSetWindowRgnすれば四角い穴のあいたウィンドウができあがります。 つまりhRgnはウィンドウと同じ大きさのリージョンから小さい長方形 のリージョンを差し引いたリージョン、つまり穴あきリージョンと なります。 リージョンの合成については第48章を 参照してください。

メニューからIDM_RND(「丸」)を選択したときも同じ考え方で 処理します。

次にIDM_STR(「星形」)ですが、これは三角形を2つ合成して まず星形のリージョン(hRgnX)を作ります。 そして、ウィンドウと同じ大きさのリージョンからhRhnXを引き算します。

さて、多角形のリージョンの作り方ですが、CreatePolygonRgnを使います。

HRGN CreatePolygonRgn( CONST POINT *lppt, // POINT構造体配列へのポインタ int cPoints, // 配列の個数 int fnPolyFillMode // 多角形フィリングモード );

多角形の各頂点をPOINT構造体の配列に納めます。 そして、lpptに配列のポインタを指定します。

cPointsは配列の個数を指定します。

fnPolyFillModeはALTERNATEかWINDINGのどちらかを指定します。
ALTERNATEは辺1と辺2、辺3と辺4の間のように、多角形の奇数番号の辺と 偶数番号の辺の間を塗りつぶします。
WINDINGはワインディング値が0でないリージョンを塗りつぶします。 ワインディング値とは多角形を描画するペンがリージョンの周囲を回る回数です。 (わかりにくいのですが渦巻き型みたいなものを考えてみてください。)
単純なものであればALTERNATEでも、WINDINGでも結果は同じです。

IDM_TRIの処理については星形がわかれば、説明の必要はないですね。

次に、このウィンドウはいつものように、WS_OVERLAPPEDWINDOWで作ったので 大きさを変えることができます。これは都合が悪いですね。 対策としてはWS_OVERLAPPEDWINDOWで作らないのが一番です。

しかし、WM_SIZEメッセージを処理することにより ユーザーが大きさを変えても強制的に元に戻すことができます。 大きさ自体を元に戻すにはMoveWindow関数を使えばよいのですが ウィンドウの位置がわかりません。そこでウィンドウの位置を知るために GetWindowPlacement関数を使ってみました。

BOOL GetWindowPlacement( HWND hWnd,// ウィンドウハンドル WINDOWPLACEMENT *lpwndpl//WINDOWPLACEMENT構造体へのポインタ );

ウィンドウハンドルを指定することにより、WINDOWPLACEMENT構造体に 位置などの情報を取得することができます。 注意する点はこの関数を使う前にWINDOWPLACEMENT構造体の lengthメンバにsizeof(WINDOWPLACEMENT)を指定しておくことです。 これを忘れるとおかしなことになります。

typedef struct _WINDOWPLACEMENT { // wndpl UINT length; UINT flags; UINT showCmd; POINT ptMinPosition; POINT ptMaxPosition; RECT rcNormalPosition; } WINDOWPLACEMENT;

lengthはこの構造体のサイズです。
flagsはWPF_RESTORETOMAXIMIZEDかWPF_SETMINPOSITIONの いずれか、または両方を設定できます。 前者はアイコン化される前に最大表示されていたかどうかにかかわらず、 元に戻されるウィンドウを最大表示することを指定します。
後者はアイコン化されたウィンドウの x 位置と y 位置が指定できることを示します。
showCmdは現在のウィンドウの表示状態の指定です。 ShowWindow関数の第2引数を思い浮かべてください。
ptMinPositionはウィンドウがアイコン化されるときの、 ウィンドウの左上隅の位置を指定します。
ptMaxPositionはウィンドウが最大化されるときの、 ウィンドウの左上隅の位置を指定します。
rcNormalPositionは、ウィンドウが通常表示されているときの ウィンドウの座標を指定します。

さて、WM_DESTROYメッセージが来たらリージョンを破棄します。 WM_CREATEのところでリージョンを作っておかないと、アプリケーションを 起動後すぐに終了した場合DeleteObject関数が失敗します。 最初にも書いたように、リージョンが最初からない場合は DeleteObjectが失敗するのは当然ですし、失敗しても多分 不都合はないと思います。


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

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