• Firefoxボタンの謎

    2012年02月18日 17時52分
    FirefoxをWindows7で実行すると、左上に「Firefox」ボタンが付いています。

    Firefoxボタン



    今まで何も考えずに使っていましたが、
    ふと、「これどうやって付けてるんだ?」と思ったので調べてみました。

    詳しく調べるならFirefoxのソースを読むのが良いと思うのですが、
    ソースをダウンロードしたものの、あまりの量に負けてしまいました。
    外からの見た目が大体同じ物を作るということとご理解ください。


    注意して見てみると、似たような物が結構見つかります。(特にMicrosoft製品)
    IEもOfficeもタイトルバーが伸びていて、その中にタブやボタンが配置されています。

    これだけ使われているということは、
    Hack的な実装方法ではなく、きちんとした実装方法がありそうです。


    調べてみるとやはり公式の資料が見つかりました。
    Custom Window Frame Using DWM(Microsoft公式)


    基本的には上記のページを見れば実装できます。
    内容が被るところが多いですが、私が試したことを以下に記載してみようと思います。


    フレームを広げる

    ボタンを配置する前に、フレームを広げる方法です。

    ボタンやタブを配置するためにはある程度広い面積が必要になるため、
    ほとんどのアプリはフレームを広げています。(主にタイトルバー)

    フレームを広げるには、DwmExtendFrameIntoClientArea関数を使用します。

    Windows Vistaから使用されているDWMに対する操作系の関数です。
    クライアント領域までフレームを拡張することが出来ます。

    第1引数に対象ウィンドウのハンドルを、
    第2引数のMARGINSに拡張する量を指定します。
    (拡張していくつになるかではなく、いくつ拡張するかを指定します。)

    例えば、ウィンドウ上部のフレームを64ピクセル拡張するには次のように書きます。
    1MARGINS margins;
    2margins.cyTopHeight = 64;
    3margins.cyBottomHeight = 0;
    4margins.cxLeftWidth = 0;
    5margins.cxRightWidth = 0;
    6DwmExtendFrameIntoClientArea(windowHandle, &margins);


    DwmExtendFrameIntoClientAreaを呼び出した後には、SetWindowPosをSWP_FRAMECHANGED指定で呼び出して更新します。
    1RECT rect;
    2GetWindowRect(windowHandle, &rect);
    3SetWindowPos(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のポインタになっているので、
    その内部の矩形範囲を指定することでクライアント領域を調整できます。

    左右と下のフレームを通常通り非クライアント領域に、
    上のフレームをクライアント領域に設定するには次のように書きます。
    1int x = GetSystemMetrics(SM_CXSIZEFRAME);
    2int y = GetSystemMetrics(SM_CYSIZEFRAME);
    3
    4NCCALCSIZE_PARAMS* pncsp = cast(NCCALCSIZE_PARAMS*)lParam;
    5pncsp.rgrc[0].top    = pncsp.rgrc[0].top    + 0;
    6pncsp.rgrc[0].bottom = pncsp.rgrc[0].bottom - y;
    7pncsp.rgrc[0].left   = pncsp.rgrc[0].left   + x;
    8pncsp.rgrc[0].right  = pncsp.rgrc[0].right  - x;


    先ほどの半透明赤背景だとこうなります。
    半透明赤背景2


    フレームサイズを色々変えてみたのですが、
    左右と下のフレーム幅は自由に決められるものの、
    上のフレームを本来のタイトルバーの高さより小さくすると表示がずれるようです。(0だけは例外的に大丈夫)

    この辺りは資料が少ないので使い方を間違っているのかもしれません。


    拡張したフレーム部分と非クライアントの衝突判定

    ここまで実装すれば、あとはフレームと被ったクライアント領域に描画すればボタン等の表示は可能です。
    ボタン表示


    ただ、このままだとクライアント領域にしたフレーム部分を非クライアント領域として扱えません。
    (フレームをドラッグしてウィンドウを移動したり、ダブルクリックで最大化したりが動かない)

    そのため、WM_NCHITTESTに接触判定処理を追加します。
    WM_NCHITTESTの戻り値として、HTTOPやHTCAPTIONを返せば、
    マウスカーソルが上の枠やキャプションと接触しているとして扱われます。

    具体的な例は次のとおりです。
    1// マウスカーソル位置を取得
    2int x = LOWORD(lParam);
    3int y = HIWORD(lParam);
    4
    5// 枠のサイズを取得
    6int borderYWidth = GetSystemMetrics(SM_CYSIZEFRAME); // 水平枠の高さ
    7int borderXWidth = GetSystemMetrics(SM_CXSIZEFRAME); // 垂直枠の幅
    8
    9// 現在のウィンドウサイズを取得
    10RECT windowRect;
    11GetWindowRect(windowHandle, &windowRect);
    12
    13// 左右の枠を避ける
    14if (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を捕まえて自分で処理します。
    1if (wParam == HTCAPTION)
    2    TrackPopupMenu(GetSystemMenu(windowHandle, FALSE), 0, LOWORD(lParam), HIWORD(lParam), 0, windowHandle, null);



    サンプル

    実際に動作を試したソースを置いておきます。
    ソース


    一応動いていますが、Windows的に正しい自信がないので、より正しい方法があれば教えてください。
    というか、Microsoftがもっと丁寧に書くべき。

  • Windowsで動く壁紙

    2011年12月03日 21時19分
    PS3はXMBのテーマによっては背景が動いたりします。

    例えば、画像だとわかりにくいですがクロが踊ったりしてます。
    XMBのSS

    これ、Windowsでも同じこと出来ないのかと思ったのが今回の話。



    デスクトップの背景部分に画像を転送するAPIを見つけて
    Direct3Dのレンダリング結果を投げ込めばいいだろうと始めてみたのですが、
    そもそもデスクトップの背景を操作するようなAPIが見つからない。

    そういえばそういうソフト見たこと無いなと思って探してみたのですが、
    動画を壁紙にするソフトはあるものの、
    壁紙や背景を描画対象にするようなプログラムが見つかりません。


    近いものとしては、他のウィンドウをキャプチャするプログラムがいくつか見つかりました。
    キャプチャするバッファに対して逆に書き込みを行えば
    表示を乗っ取れるのではないかという目論見です。


    というわけで次の手順で適当なウィンドウのバッファを取ってみました。
    1. dwmapi.dllのindexが100の関数を取得
    2. 取得した関数で共有リソースのハンドルを取得
    3. ID3D10Device1.OpenSharedResourceを使って共有リソースハンドルからID3D10Resourceを取得
    4. ID3D10Resource.QueryInterfaceでID3D10Texture2Dを取得

    ソース


    このウィンドウのバッファを取ります。
    対象のウィンドウ


    BMPファイルとして保存したもの。
    キャプチャ結果

    真っ黒です。

    でも、D3D10_TEXTURE2D_DESCはきちんと取れています。
    さらに、書き込みも出来るようです。紫で塗りつぶした後キャプチャしてみました。
    塗りつぶし結果

    なぜか黒いしましまが入ります。
    ちなみにキャプチャ元のウィンドウに変化はありません。


    どうも根本的なことが間違っている気がしますが、さっぱりわかりません。
    dwmapiから取る関数がVista用なのかもしれません。(参考にしたソースがVista用っぽい)
    でも残念ながらVistaマシンなんてありません。


    他に少し考えたのは、DWMの内部バッファなんか無理にさわらなくても、
    ウィンドウのZ値をデスクトップのアイコンより後ろにするとか出来ないものか。など。

    あとは、描画 → ファイル保存 → 壁紙設定を高速で繰り返せば
    一応動くような気もしますが、さすがにそれば・・・


    何か良い知恵がある人は教えてください。
    ナイスな情報を教えてくれた人には何かしらお礼します。