今まで何も考えずに使っていましたが、
ふと、「これどうやって付けてるんだ?」と思ったので調べてみました。
詳しく調べるならFirefoxのソースを読むのが良いと思うのですが、
ソースをダウンロードしたものの、あまりの量に負けてしまいました。
外からの見た目が大体同じ物を作るということとご理解ください。
注意して見てみると、似たような物が結構見つかります。(特にMicrosoft製品)
IEもOfficeもタイトルバーが伸びていて、その中にタブやボタンが配置されています。
これだけ使われているということは、
Hack的な実装方法ではなく、きちんとした実装方法がありそうです。
調べてみるとやはり公式の資料が見つかりました。
Custom Window Frame Using DWM(Microsoft公式)
基本的には上記のページを見れば実装できます。
内容が被るところが多いですが、私が試したことを以下に記載してみようと思います。
フレームを広げる
ボタンを配置する前に、フレームを広げる方法です。ボタンやタブを配置するためにはある程度広い面積が必要になるため、
ほとんどのアプリはフレームを広げています。(主にタイトルバー)
フレームを広げるには、DwmExtendFrameIntoClientArea関数を使用します。
Windows Vistaから使用されているDWMに対する操作系の関数です。
クライアント領域までフレームを拡張することが出来ます。
第1引数に対象ウィンドウのハンドルを、
第2引数のMARGINSに拡張する量を指定します。
(拡張していくつになるかではなく、いくつ拡張するかを指定します。)
例えば、ウィンドウ上部のフレームを64ピクセル拡張するには次のように書きます。
1 | MARGINS margins; |
---|---|
2 | margins.cyTopHeight = 64; |
3 | margins.cyBottomHeight = 0; |
4 | margins.cxLeftWidth = 0; |
5 | margins.cxRightWidth = 0; |
6 | DwmExtendFrameIntoClientArea(windowHandle, &margins); |
DwmExtendFrameIntoClientAreaを呼び出した後には、SetWindowPosをSWP_FRAMECHANGED指定で呼び出して更新します。
1 | RECT rect; |
---|---|
2 | GetWindowRect(windowHandle, &rect); |
3 | SetWindowPos(windowHandle, null, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_FRAMECHANGED); |
これらの処理をWM_CREATEに記述すれば次のようにフレームが拡張されます。
フレームをクライアント領域にする
実際にどうやってフレームに書き込むかというと、フレームをクライアント領域として扱います。クライアント領域ならば画像でもなんでも書くのは簡単です。
そうなるとフレームのガラスっぽい部分も自分で書く必要がありそうですが、
DWMの仕様として「フレームがクライアント領域でも、黒(ARGB(0,0,0,0)の部分はフレームを描画する」という動作になっています。
(より厳密には、「フレームとクライアント領域のデータがアルファブレンドされる」という動作です)
例として先ほどフレームを拡張したウィンドウのクライアント領域を白で塗りつぶすと次のようになります。
さらに半透明の赤で塗りつぶすとこうなります
つまり、ボタンやタブを自分で描画し、それ以外の部分を黒にしておけば、
DWMが勝手に合成して、フレーム上にボタンやタブが置いてあるように見えます。
フレームをクライアント領域にするには、WM_NCCALCSIZEを処理します。
lParamがNCCALCSIZE_PARAMSのポインタになっているので、
その内部の矩形範囲を指定することでクライアント領域を調整できます。
左右と下のフレームを通常通り非クライアント領域に、
上のフレームをクライアント領域に設定するには次のように書きます。
1 | int x = GetSystemMetrics(SM_CXSIZEFRAME); |
---|---|
2 | int y = GetSystemMetrics(SM_CYSIZEFRAME); |
3 | |
4 | NCCALCSIZE_PARAMS* pncsp = cast(NCCALCSIZE_PARAMS*)lParam; |
5 | pncsp.rgrc[0].top = pncsp.rgrc[0].top + 0; |
6 | pncsp.rgrc[0].bottom = pncsp.rgrc[0].bottom - y; |
7 | pncsp.rgrc[0].left = pncsp.rgrc[0].left + x; |
8 | pncsp.rgrc[0].right = pncsp.rgrc[0].right - x; |
先ほどの半透明赤背景だとこうなります。
フレームサイズを色々変えてみたのですが、
左右と下のフレーム幅は自由に決められるものの、
上のフレームを本来のタイトルバーの高さより小さくすると表示がずれるようです。(0だけは例外的に大丈夫)
この辺りは資料が少ないので使い方を間違っているのかもしれません。
拡張したフレーム部分と非クライアントの衝突判定
ここまで実装すれば、あとはフレームと被ったクライアント領域に描画すればボタン等の表示は可能です。ただ、このままだとクライアント領域にしたフレーム部分を非クライアント領域として扱えません。
(フレームをドラッグしてウィンドウを移動したり、ダブルクリックで最大化したりが動かない)
そのため、WM_NCHITTESTに接触判定処理を追加します。
WM_NCHITTESTの戻り値として、HTTOPやHTCAPTIONを返せば、
マウスカーソルが上の枠やキャプションと接触しているとして扱われます。
具体的な例は次のとおりです。
1 | // マウスカーソル位置を取得 |
---|---|
2 | int x = LOWORD(lParam); |
3 | int y = HIWORD(lParam); |
4 | |
5 | // 枠のサイズを取得 |
6 | int borderYWidth = GetSystemMetrics(SM_CYSIZEFRAME); // 水平枠の高さ |
7 | int borderXWidth = GetSystemMetrics(SM_CXSIZEFRAME); // 垂直枠の幅 |
8 | |
9 | // 現在のウィンドウサイズを取得 |
10 | RECT windowRect; |
11 | GetWindowRect(windowHandle, &windowRect); |
12 | |
13 | // 左右の枠を避ける |
14 | if (windowRect.left + borderXWidth <= x && x < windowRect.right - borderXWidth) |
15 | { |
16 | // 上の水平枠と接触しているか返す |
17 | if (windowRect.top <= y && y < windowRect.top + borderYWidth) |
18 | return HTTOP; |
19 | // CAPTIONと接触しているか返す |
20 | else if (windowRect.top + borderYWidth <= y && y < windowRect.top + 32/*フレームサイズ*/) |
21 | return HTCAPTION; |
22 | } |
23 | |
24 | // 上記以外の領域との接触判定はデフォルトに投げる |
あと、これをやっても何故か右クリックでシステムメニューを出すのが動かないので
WM_NCRBUTTONUPを捕まえて自分で処理します。
1 | if (wParam == HTCAPTION) |
---|---|
2 | TrackPopupMenu(GetSystemMenu(windowHandle, FALSE), 0, LOWORD(lParam), HIWORD(lParam), 0, windowHandle, null); |
サンプル
実際に動作を試したソースを置いておきます。ソース
一応動いていますが、Windows的に正しい自信がないので、より正しい方法があれば教えてください。
というか、Microsoftがもっと丁寧に書くべき。