Mesoscopic Programming

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

ウィンドウズプログラミング講座第10回:2つのビューウィンドウ

概要

こんばんわ。今回は、とりあえずツリービューの元となる左側のビューウィンドウを作成します。表示/非表示の切り替えとマウスによるサイズ変更に対応しています。

ウィンドウズのプログラミング技術紹介っちゅうことで最終的に何を作ろうか考えていましたが、やっと目標が見えてきました。何でも表示できるビュワーなんてどうでしょうか?
左側のツリービューにファイルツリーを表示して、そのファイルを選択するとファイルタイプに合わせてテキストや画像を表示するやつです。何か便利そう。
とりあえずテキストとバイナリは問題ないとして、あと画像と動画とXファイルなんか表示できたらいいなと思います。ま、時間の問題があるのでどうなるか分かりませんが。


ビューウィンドウクラス

修正内容

ビューウィンドウクラスは、ドキュメントビュークラスとツリービュークラスの基底クラスです。
マウスドラッグでビューウィンドウの幅を変更可能にするために、ビューウィンドウが受け取ったマウスイベントを親ウィンドウに通知する処理を追加しました。
本当は親ウィンドウ自身が子ウィンドウのマウスイベントを検知できれば簡単なんですが、いろいろググってみたもののどうやらそんなことはできそうもないと分かりました。
そこで仕方なく子ウィンドウから親ウィンドウへマウスイベントを通知する処理を追加しました。

class View : public Window
{
    virtual LRESULT OnWmMouseMove();
    virtual LRESULT OnWmNcMouseMove();
};

WM_MOUSEMOVE だけでなく WM_NCMOUSEMOVE メッセージにも対応します。
最初は WM_MOUSEMOVE だけで良いかと思ったんですが、実は WM_MOUSEMOVE はクライアントエリアのマウスイベントしか教えてくれません。
ビューウィンドウは拡張ウィンドウスタイルで WS_EX_CLIENTEDGE を指定しているもんで、非クライアントエリアがほんのちょっと*1だけ存在するんですね。
ここのマウスイベントも検知しないとうまく行かないと分かったので、めんどくさいけど WM_NCMOUSEMOVE にも対応したっちゅう訳なんです。

LRESULT View :: OnWmMouseMove()
{
    NMMOUSE nmMouse;

    ZeroMemory( & nmMouse, sizeof nmMouse );

    nmMouse.hdr.hwndFrom = hwnd;
    nmMouse.hdr.idFrom   = GetDlgCtrlID( hwnd );
    nmMouse.hdr.code     = WM_MOUSEMOVE;
    nmMouse.pt.x         = LOWORD( lparam );
    nmMouse.pt.y         = HIWORD( lparam );

    ClientToScreen( hwnd, & nmMouse.pt );

    if ( SendMessage( GetParent( hwnd ), WM_NOTIFY, wparam, ( LPARAM ) & nmMouse ) )
    {
        return 0;
    }

    return DefaultProc();
}

これがクライアントエリアでマウスカーソルが移動したときのイベントです。
これを WM_NOTIFY として親ウィンドウへ通知します。
既成の通知コードを調べたところ、マウスクリック通知はあってもマウス移動通知はないことが分かったので、通知コードとしてまんま WM_MOUSUMOVE を設定しました。
WM_MOUSEMOVE で受け取るカーソル位置はクライアントエリア相対座標なのでスクリーン座標に変換しています。
あと親ウィンドウが必要とするカーソル座標は子ウィンドウの端っこだけなんで、親が対応しなかった場合はデフォルトのマウス移動処理へ渡しています。こうしないと何か問題が起こりますから。

LRESULT View :: OnWmNcMouseMove()
{
    NMMOUSE nmMouse;

    ZeroMemory( & nmMouse, sizeof nmMouse );

    nmMouse.hdr.hwndFrom = hwnd;
    nmMouse.hdr.idFrom   = GetDlgCtrlID( hwnd );
    nmMouse.hdr.code     = WM_MOUSEMOVE;
    nmMouse.pt.x         = LOWORD( lparam );
    nmMouse.pt.y         = HIWORD( lparam );

    if ( SendMessage( GetParent( hwnd ), WM_NOTIFY, wparam, ( LPARAM ) & nmMouse ) )
    {
        return 0;
    }

    return DefaultProc();
}

こちらは非クライアントエリアのマウス移動イベントです。受け取るマウスカーソル座標はスクリーン座標系なので変換していません。


アプリケーションクラス

ツリービューウィンドウを追加しました。ツリービューと言ってもツリーの実装はまだなんですけどね。
とりあえずビューの追加とサイズ変更対応だけでも大変な処理量なのでここでアップしときます。

識別子

class App : public Window
{
    enum CommandID
    {
        idViewTree,
    };
    enum WindowID
    {
        idTreeView,
    };
    enum ViewMenuID
    {
        menuViewTree,
    };

とりあえずツリービューに必要なIDの追加です。
idViewTree はコマンドID、idTreeView はコントロールID、menuViewTree は表示メニュー内のメニューポジションですね。

変数

    static const LONG    cursorOffset = 4;
    static const LPCTSTR keyTreeView;
    static const LPCTSTR keyTreeWidth;
    INT                  treeWidth;
    BOOL                 bTreeView;
    TreeView             treeView;
    BOOL                 bTrack;
    POINT                ptTrack;

cursorOffset はツリービューウィンドウの右端からの相対X座標で、この範囲でマウスドラッグに対応します。
keyTreeView と keyTreeWidth はプロファイル保存キーで表示フラグとウィンドウ幅用のやつです。bTreeView と treeWidth 変数がそれぞれに対応するんです。
treeView はツリービューオブジェクトそのものでやんす。
bTrack はサイズ変更のためのマウストラック中フラグで、ptTrack は直前のカーソル座標でごんす。

関数

今回修正のあった関数と追加関数です。

    BOOL    Init( int nCmdShow );
    VOID    InitTreeView();
    VOID    AdjustSize();
    VOID    UpdateMinSize();
    VOID    UpdateMenu();
    LRESULT OnWmDestroy();
    LRESULT OnWmMove();
    LRESULT OnWmSize();
    LRESULT OnWmGetMinMaxInfo();
    LRESULT OnWmCommand();
    LRESULT OnWmNotify();
    LRESULT OnWmMouseMove();
    LRESULT OnWmLbuttonDown();
    LRESULT OnWmLbuttonUp();
    LRESULT OnViewTree();
};
概要

今回のメインは何つってもマウスドラッグで子ウィンドウのサイズ変更に対応する処理でしょう。
まず WM_NOTIFY で子ウィンドウからマウス移動通知を受け取ります。
マウスカーソル座標がツリービューウィンドウの右端っこだったら、カーソル形状を左右矢印の奴に変更してマウスをキャプチャします。
マウスをキャプチャすると以降のマウスイベントは親である自分自身へ届くことになりますので、あとは自分あてのマウスイベントに対応すれば良し。
WM_LBUTTONDOWN でマウストラッキングを開始し、以降の WM_MOUSEMOVE でビューウィンドウのサイズをリアルタイムでぐりぐり変更しちゃうもんね。
んで WM_LBUTTONUP が来たらトラッキング終了っちゅうことでやんす。

WM_NOTIFY メッセージ

子ウィンドウのマウス移動イベントだけが必要なんで、それ以外の通知だったら基底クラスの処理に任せます。

LRESULT App :: OnWmNotify()
{
    RECT rc;

    if ( lpnmhdr->code == WM_MOUSEMOVE && bTreeView )
    {
        GetWindowRect( treeView.hwnd, & rc );

        if ( abs( lpnmmouse->pt.x - rc.right ) <= cursorOffset )
        {
            SetCursor( LoadCursor( NULL, IDC_SIZEWE ) );
            SetCapture( hwnd );

            bTrack = FALSE;

            return TRUE;
        }

        return FALSE;
    }

    return Window :: OnWmNotify();
}

ビューウィンドウクラスを基底クラスとするドキュメントウィンドウとツリーウィンドウの両方からこの通知が来ます。しかしサイズ変更したいのはツリービューだけなのでツリービューが非表示だったら無視します。
最初ツリービューからの通知だけで良いんじゃね?と思ってたんですが、ツリービューの右端のさらに数ドット外側のイベントもないとうまく行かないと分かったので、ドキュメントビューからのイベントも処理するようにしました。
この通知でもらえるマウスカーソル座標はスクリーン系なので変換することなくツリービューの右端あたりをチェックしてます。
ヒットしてたらカーソル形状を左右矢印に変更してマウスをキャプチャします。これにより今後はマウスカーソルがどこにあろうと、マウスをキャプチャしたウィンドウへイベントが届くことになります。
まだボタンは押されていないのでトラッキングフラグはオフにしておきます。

WM_LBUTTONDOWN メッセージ

さあいよいよトラッキングの開始です。

LRESULT App :: OnWmLbuttonDown()
{
    if ( GetCapture() == hwnd )
    {
        bTrack = TRUE;

        GetCursorPos( & ptTrack );
    }

    return 0;
}

現在マウスキャプチャ中であることを確認し、トラッキングフラグをオンします。
直前のカーソル座標を忘れずに保存しつつ。

WM_MOUSEMOVE メッセージ

トラッキング前とトラッキング中にこれを受信します。いずれにせよマウスキャプチャ中確認を忘れずにね。
トラッキング前ならカーソルが範囲外へ出たかどうかの確認だけです。
トラッキング中なら2つのビューウィンドウサイズをぐりぐりと変更します。

LRESULT App :: OnWmMouseMove()
{
    RECT  rcMin = { 0 };
    RECT  rcMax = { 0, 0, docView.minWidth, docView.minHeight };
    RECT  rcDocView;
    RECT  rcTreeView;
    RECT  rc;
    DWORD dwStyle;
    DWORD dwExStyle;
    POINT pt;
    INT   width;

    if ( GetCapture() == hwnd )
    {
        GetCursorPos( & pt );

        GetWindowRect( docView.hwnd,  & rcDocView );
        GetWindowRect( treeView.hwnd, & rcTreeView );

        if ( bTrack )
        {
            ScreenToClient( hwnd, ( LPPOINT ) & rcDocView.left );
            ScreenToClient( hwnd, ( LPPOINT ) & rcDocView.right );
            ScreenToClient( hwnd, ( LPPOINT ) & rcTreeView.left );
            ScreenToClient( hwnd, ( LPPOINT ) & rcTreeView.right );

            dwStyle   = GetWindowLong( treeView.hwnd, GWL_STYLE );
            dwExStyle = GetWindowLong( treeView.hwnd, GWL_EXSTYLE );

            AdjustWindowRectEx( & rcMin, dwStyle, FALSE, dwExStyle );

            dwStyle   = GetWindowLong( docView.hwnd, GWL_STYLE );
            dwExStyle = GetWindowLong( docView.hwnd, GWL_EXSTYLE );

            AdjustWindowRectEx( & rcMax, dwStyle, FALSE, dwExStyle );

            GetClientRect( hwnd, & rc );

            rcMin.right -= rcMin.left;
            rcMax.left   = rc.right - ( rcMax.right - rcMax.left );
            width        = rcTreeView.right + ( pt.x - ptTrack.x );

            if ( width < rcMin.right )
            {
                rcTreeView.right = rcMin.right;
                rcDocView.left   = rcMin.right;
            }
            else if ( width > rcMax.left )
            {
                rcTreeView.right = rcMax.left;
                rcDocView.left   = rcMax.left;
            }
            else
            {
                rcDocView.left   += ( pt.x - ptTrack.x );
                rcTreeView.right += ( pt.x - ptTrack.x );
            }

            MoveWindow( docView.hwnd,  rcDocView.left,  rcDocView.top,  rcDocView.right  - rcDocView.left,  rcDocView.bottom  - rcDocView.top,  TRUE );
            MoveWindow( treeView.hwnd, rcTreeView.left, rcTreeView.top, rcTreeView.right - rcTreeView.left, rcTreeView.bottom - rcTreeView.top, TRUE );

            if ( width >= rcMin.right && width <= rcMax.left )
            {
                ptTrack = pt;
            }
        }
        else if ( abs( pt.x - rcTreeView.right ) > cursorOffset || pt.y < rcTreeView.top || pt.y > rcTreeView.bottom )
        {
            ReleaseCapture();
        }
    }

    return 0;
}

rcMin でツリービューの最小サイズをチェックします。
rcMax で同様に最大サイズをチェックします。
最小サイズはツリービューの非クライアント領域の合計幅です。つまり左右のクライアントエッジ幅です。
ツリービューが最小幅未満になったら、ツリービューを非表示にします。
最大サイズはドキュメントビューの最小幅です。最初はドキュメントビューも非表示にする処理を入れていたんですが、ドキュメントビューを非表示にするといろいろ問題があることが分かったので消えてなくならないようにしました。
サイズのチェックが終わったら実際にビューウィンドウをサイズ変更します。これがチョー気持ち良い。

WM_LBUTTONUP メッセージ

トラッキング終了処理です。

LRESULT App :: OnWmLbuttonUp()
{
    DWORD dwStyle   = GetWindowLong( treeView.hwnd, GWL_STYLE );
    DWORD dwExStyle = GetWindowLong( treeView.hwnd, GWL_EXSTYLE );
    RECT  rc0       = { 0 };
    RECT  rc;

    if ( GetCapture() == hwnd && bTrack )
    {
        AdjustWindowRectEx( & rc0, dwStyle, FALSE, dwExStyle );

        GetWindowRect( treeView.hwnd, & rc );

        if ( rc.right - rc.left <= ( rc0.right - rc0.left ) )
        {
            bTreeView = FALSE;
            rc.right  = rc.left + treeWidth;

            ShowWindow( treeView.hwnd, SW_HIDE );
            MoveWindow( treeView.hwnd, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, FALSE );

            AdjustSize();
            UpdateMenu();
        }
        else
        {
            treeWidth = ( rc.right - rc.left );
        }

        UpdateMinSize();

        bTrack = FALSE;

        ReleaseCapture();
    }

    return 0;
}

rc0 で最小幅をチェックし、最小幅以下だったらツリービューを非表示にします。
非表示以外の場合は変更された幅を保存します。非表示にした場合は保存しません。

その他

あとツリービュー表示切り替えコマンド対応などもありますが単純なので説明はいらんでしょう。
ま、だいたいこんなところですか。疲れたのでおしまい。


実行画面

実行ファイル
  1. Windows.zip

以上です。

*1: 2ドット