Mesoscopic Programming

タコさんプログラミング専門

ファイルビュワー作成日誌 #003:ウィンドウの分割

概要

メインウィンドウからツールバーとステータスバーを引いた残りのクライアント領域を以下の3つに分割します。

  1. Output(出力画面)
  2. TreeView(ブラウザ)
  3. Document(ビュワー)

3つのウィンドウを梱包するためのウィンドウとしてコンテナウィンドウを準備します。
3つのウィンドウはコンテナの子ウィンドウであるペーンウィンドウとなります。
メインウィンドウはツールバーとステータスバーとコンテナウィンドウを管理し、
コンテナウィンドウは3つのペーンウィンドウを管理するという寸法になっとります。
出力画面とブラウザは表示/非表示が切り替えられますので、そのためのコマンド関係も追加しました。


ウィンドウが分割されると以下のようになります。
ツールバーに表示切替ボタンが追加されてます。

コンテナウィンドウクラス

コンテナウィンドウはペーンウィンドウの大きさを管理します。
ペーンウィンドウの境界線あたりにマウスカーソルを合わせると、
カーソル形状が変化してサイズ変更可能であることを表現します。
ドラッグして好きなようにサイズ変更します。
変更されたサイズはプロファイルに保存してるので、次に立ち上げたときにも再現されます。
それではソースコードをご覧ください。

クラス定義
class Container : public Window
{
public :

    Pane  * pPane;
    RECT  rcSpace;
    UINT  idCursor;
    POINT ptTrack;
    HWND  hTrackWnd;

public :

    Container() : pPane( NULL ) {}

public :

    inline LPCTSTR GetRegistClassName() { return _T( "ClassContainer" ); }
    VOID           GetSpaceRect( RECT * rc );
    LRESULT        OnWmSize();
    LRESULT        OnWmSetCursor();
    LRESULT        OnWmLbuttonUp();
    LRESULT        OnWmMouseMove();
    LRESULT        OnWmNotify();
};
解説

pPane は先頭のペーンウィンドウです。
ペーンウィンドウ自身がリスト情報を保持してますので、ペーン数は特に管理してません。
rcSpace は残りのウィンドウ領域です。
先頭のペーンウィンドウから順々にウィンドウ領域を取得してく仕組みです。
まず出力ウィンドウが画面の下部分を取得します。
次にツリービューウィンドウが画面の左側を取得します。
残りの領域をドキュメントウィンドウが全部使用するという寸法になっとります。
idCursor 以下の変数はサイズ変更用のマウス関係のやつです。


WM_SIZE メッセージ

コンテナウィンドウのサイズはメインウィンドウが管理してます。
メインウィンドウがコンテナウィンドウのサイズを変更すると WM_SIZE メッセージが到着します。
するとコンテナウィンドウはすべてのペーンウィンドウに WM_SIZE メッセージを送信してサイズ変更を促します。
最初 rcSpace はコンテナウィンドウの全クライアント領域です。
サイズ変更を要求されたペーンウィンドウは、自分自身でウィンドウサイズを変更します。
そこで当該ペーンウィンドウのサイズを rcSpace から引いたものが残りのクライアント領域となります。

LRESULT Container :: OnWmSize()
{
    Pane * pane2 = pPane;
    RECT rc, rc2;

    GetClientRect( hwnd, & rcSpace );

    for ( ; pane2 != NULL; pane2 = pane2->pNext )
    {
        SendMessage( pane2->hwnd, WM_SIZE, 0, 0 );

        if ( IsWindowVisible( pane2->hwnd ) )
        {
            GetClientRect( hwnd, & rc );
            GetWindowRect( pane2->hwnd, & rc2 );
            ScreenToClient( hwnd, ( LPPOINT ) & rc2.left );
            ScreenToClient( hwnd, ( LPPOINT ) & rc2.right );
            SubtractRect( & rcSpace, & rcSpace, & rc2 );
        }
    }

    return 0;
}
解説

pane2 で数珠つなぎになったすべてのペーンウィンドウを走査します。
ペーンウィンドウのリスト管理はペーンウィンドウクラスで行いますのでコンテナは何も知りません。
ただ pNext 変数が次のペーンウィンドウを指していることだけを知っています。


WM_SETCURSOR メッセージ

マウスカーソル形状の変更を行うために WM_SETCURSOR メッセージに対応します。
ペーンウィンドウの境界線あたりにカーソルが来たら、サイズ変更方向、つまり左右または上下のカーソル形状に変更します。

LRESULT Container :: OnWmSetCursor()
{
    POINT pt;
    RECT  rc, rc2;

    GetCursorPos( & pt );
    GetClientRect( hwnd, & rc );
    GetWindowRect( hchild, & rc2 );
    ClientToScreen( hwnd, ( LPPOINT ) & rc.left );
    ClientToScreen( hwnd, ( LPPOINT ) & rc.right );

    idCursor  = OCR_NORMAL;
    hTrackWnd = NULL;

    if ( rc2.left == rc.left )
    {
        if ( rc2.top == rc.top )
        {
            if ( abs( pt.x - rc2.right ) <= GetSystemMetrics( SM_CXEDGE ) )
            {
                idCursor  = OCR_SIZEWE;
                hTrackWnd = hchild;
            }
        }
        else if ( rc2.bottom == rc.bottom )
        {
            if ( abs( pt.y - rc2.top ) <= GetSystemMetrics( SM_CYEDGE ) )
            {
                idCursor  = OCR_SIZENS;
                hTrackWnd = hchild;
            }
        }
    }

    if ( hTrackWnd != NULL )
    {
        SetCursor( LoadCursor( NULL, MAKEINTRESOURCE( idCursor ) ) );

        return TRUE;
    }

    return Window :: OnWmSetCursor();
}
解説

hchild は WM_SETCURSOR メッセージの wParam を共用体宣言したもので、カーソル位置のウィンドウハンドルです。
コンテナウィンドウのクライアント領域はペーンウィンドウで埋め尽くされているので、これはペーンウィンドウのどれかです。
左端のペーンウィンドウなら右側を、下端のペーンウィンドウなら上側の境界線をドラッグ可能とし、
カーソル位置がそれぞれの条件に見合ったなら、マウスカーソル形状を変更するという寸法になっとります。
ちなみにマウスカーソル形状の識別子である OCR_NORMAL は、windows.h をインクルードする前に OEMRESOURCE を定義しないと使えないみたいですよ。

※今ぐぐったらマウスキャプチャ中は WM_SETCURSOR は来ないことが分かったのでチェック外した。


マウスキャプチャとドラッグ処理

WM_NCLBUTTONDOWN でドラッグを開始し WM_LBUTTONUP でドラッグを止めるかそれとも人間を止めます。
ここで問題なのは、普通だとコンテナウィンドウには WM_NCLBUTTONDOWN メッセージが来ないということです。
何故ならばマウスカーソルが子ウィンドウ内にあるからです。
境界線の部分をコンテナの子コントロールにすれば NM_CLICK かなんかが来るんでしょうけど、余計なものが増えるので嫌です。
そこで子ウィンドウに来た WM_NCLBUTTONDOWN メッセージを親ウィンドウに通知するためのフック処理を追加しました。
本当はペーンウィンドウクラスで直接処理しても良いだけど、
フックにしとけばこれ一カ所でほかのウィンドウでも使えるようになるから良いと思います。

WM_NCLBUTTONDOWN メッセージフック関数

以下のように、アプリケーションクラスのメッセージループ中でこのフック関数を呼びます。

int App :: MessageLoop()
{
    MSG msg;

    while ( GetMessage( & msg, NULL, 0, 0 ) )
    {
        if ( ! HookMessage( & msg ) )
        {
            TranslateMessage( & msg );
            DispatchMessage( & msg );
        }
    }

    return ( int ) msg.wParam;
}

BOOL App :: HookMessage( LPMSG pmsg )
{
    HWND    hWnd;
    NMCLICK nmclick;

    if ( GetCapture() == NULL )
    {
        if ( pmsg->message == WM_NCLBUTTONDOWN )
        {
            hWnd = GetParent( pmsg->hwnd );

            if ( hWnd != NULL )
            {
                ZeroMemory( & nmclick, sizeof nmclick );

                nmclick.hdr.hwndFrom = pmsg->hwnd;
                nmclick.hdr.idFrom   = GetDlgCtrlID( nmclick.hdr.hwndFrom );
                nmclick.hdr.code     = WM_NCLBUTTONDOWN;

                GetCursorPos( & nmclick.pt );

                if ( SendMessage( hWnd, WM_NOTIFY, nmclick.hdr.idFrom, ( LPARAM ) & nmclick ) )
                {
                    return TRUE;
                }
            }
        }
    }

    return FALSE;
}
解説

マウスキャプチャ中は無視します。
そうでなければ子ウィンドウが WM_NCLBUTTONDOWN メッセージを受け取ったことを、
WM_NOTIFY メッセージとして親ウィンドウへ通知します。
もし親ウィンドウがこの通知メッセージを処理したらゼロ以外を返してください。
そうすればこのメッセージは本人には知らせませんから。
でももし親が処理したうえで更に本人にも知らせたいならゼロを返しても別に構いませんけど。


もしも WM_NOTIFY メッセージの内容が WM_NCLBUTTONDOWN だったなら…

このメッセージが来たらもう子供じゃないのでドラッグを始めましょう。
でももちろん合法なので安心してください。

LRESULT Container :: OnWmNotify()
{
    if ( lpnmhdr->code == WM_NCLBUTTONDOWN )
    {
        if ( hTrackWnd != NULL )
        {
            ptTrack = lpnmclick->pt;

            SetCapture( hwnd );

            return 1;
        }
    }

    return Window :: OnWmNotify();
}
解説

hTrackWnd には WM_SETCURSOR で検査したペーンウィンドウハンドルが既に入っています。
ptTrack にドラッグ開始点のカーソル座標をセットします。
マウスをキャプチャします。


WM_MOUSEMOVE メッセージと WM_LBUTTONUP メッセージが来ました

マウスをキャプチャすると、たとえカーソルが子ウィンドウ内にあってもすべてのマウス関係メッセージを受け取れるようになります。

LRESULT Container :: OnWmMouseMove()
{
    POINT pt;
    RECT  rc;

    if ( GetCapture() == hwnd && hTrackWnd != NULL )
    {
        GetCursorPos( & pt );
        GetWindowRect( hTrackWnd, & rc );
        ScreenToClient( hwnd, ( LPPOINT ) & rc.left );
        ScreenToClient( hwnd, ( LPPOINT ) & rc.right );

        if ( idCursor == OCR_SIZEWE )
        {
            rc.right += ( pt.x - ptTrack.x );
        }
        else if ( idCursor == OCR_SIZENS )
        {
            rc.top += ( pt.y - ptTrack.y );
        }

        MoveWindow( hTrackWnd, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, TRUE );
        SendMessage( hwnd, WM_SIZE, 0, 0 );

        ptTrack = pt;
    }

    return 0;
}

LRESULT Container :: OnWmLbuttonUp()
{
    if ( GetCapture() == hwnd )
    {
        ReleaseCapture();
    }

    return 0;
}
解説

現状ではキャプチャ中以外来ることありませんが、念のためキャプチャ中であることチェックしています。
マウスが移動した分ペーンウィンドウのサイズに反映させます。
当該ペーンウィンドウ以外のペーンウィンドウもサイズ変更が必要になるので WM_SIZE メッセージを自分自身へ送ります。
マウスボタンが離されたらドラッグ止めるあるね。


ペーンウィンドウ基底クラス

コンテナウィンドウの中のウィンドウをペーンウィンドウと呼びます。
ペーン(Pane)とは窓枠とか仕切りとかいう意味だそうです。
たしか MFC かなんかでこういう呼び方してた気がしたので真似しました。
MFC 使ってたのはもう10年以上も前だから良く覚えてません。ていうか思い出したくもありません。
MFC 大嫌いです。
MFC 使うと、単純なのは良いけど細かいことやろうとすると本当に大変なのです。
MFC のドキュメント見てもよく分からないので結局 Win32 API 調べなきゃなんないから二度手間なのです。
なんだかクソみたいなコメント追加されたクソみたいなソースを大量に勝手に吐き出されるのでイライラします。

話がそれましたがペーンウィンドウクラスは抽象クラスなので、その実態は派生クラスにあります。
ペーンウィンドウをコンテナウィンドウのどの部分に配置するかとかは派生クラスの方で行います。

クラス定義
class Pane : public Window
{
public :

    Container * pParent;
    Pane      * pNext;
    Caption   * pCaption;
    BOOL      bCaption;
    BOOL      bHit;
    UINT      nTimer;

public :

    Pane();
    virtual ~ Pane();

public :

    virtual inline LONG    GetCreateStyle()   { return WS_VISIBLE | WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_EX_DLGMODALFRAME; }
    virtual inline DWORD   GetCreateExStyle() { return WS_EX_CLIENTEDGE; }
    virtual inline int     GetCreateWidth()   { return 200; }
    virtual inline int     GetCreateHeight()  { return 200; }
    virtual inline LPCTSTR GetSectionName()   { return GetCreateWindowName(); }
    virtual BOOL           Init( Container * container, UINT id );
    virtual VOID           WatchCaption();
    virtual LRESULT        OnWmDestroy();
    virtual LRESULT        OnWmSize();
    virtual LRESULT        OnWmPaint();
    virtual LRESULT        OnWmTimer();
    virtual LRESULT        OnWmMouseMove();
    virtual LRESULT        OnWmNotify();
    virtual LRESULT        OnScClose();
};
解説

pParent はコンテナウィンドウです。
pNext は次のペーンウィンドウです。自分が最後尾なら NULL です。
pCaption 以下の変数は、タイトルバーを表示させるためのからくりです。
タイトルバーは普段は表示しませんが、マウスカーソルをペーンの上部に置いておくと現れます。
クローズボタンのためにそうしましたが、ウィンドウタイトルも表示できるので一挙両得です。
しかしタイトルバーといっても、実はペーンウィンドウの子ウィンドウとして実装されてます。
最初はペーンウィンドウのタイトルバーとして実装を試みたんですが、
どうにも WM_NCCALCSIZE メッセージの意味がよく分かんないのであきらめました。
でも結局子ウィンドウにした方がソースコードもすっきりするし良いことだらけでした。

ペーン本来の仕事はコンテナ内での配置を制御するだけなんで極めて単純でがす。
コードの大部分はこのタイトルバー制御のからくりのためのものです。


キャプションウィンドウクラス

ペーンのタイトルバーとして使用されるウィンドウです。
本当はタイトルバーごときでクラス作るの嫌だったんでスタティックコントロールで代用してたんですが、
スタティックコントロールだとどうしてもクローズボタン処理がうまく行かないので諦めました。
でもクラス作った方がコードがシンプルになってもっけの幸いでした。

クラス定義と実装
class Caption : public Window
{
public :

    inline LPCTSTR GetRegistClassName() { return _T( "ClassCaption" ); }
    inline LONG    GetCreateStyle()     { return WS_CHILD | WS_CLIPSIBLINGS | WS_CAPTION | WS_SYSMENU; }
    inline DWORD   GetCreateExStyle()   { return WS_EX_TOOLWINDOW; }
    LRESULT        OnWmNcLbuttonDown();
    LRESULT        OnScClose();
};

LRESULT Caption :: OnWmNcLbuttonDown()
{
    if ( hittest == HTCAPTION )
    {
        return 0;
    }

    return Window :: OnWmNcLbuttonDown();
}

LRESULT Caption :: OnScClose()
{
    SendMessage( GetParent( hwnd ), WM_SYSCOMMAND, SC_CLOSE, 0 );

    return 0;
}
解説

スタティックコントロールの代わり程度のものなんでほとんど何もしないです。
クローズボタンが押された時に親であるペーンウィンドウに通知するのと、
このウィンドウがドラッグされたら困るので、その開始イベントを抑止してるだけです。


以上です。