SDK Index Previous page Next page

スプリットウインドウを作る


はじめに

エクスプローラー等で使われている、スプリットウインドウを作ってみます。いろんなサイトでやっているのでたいして目新しくはないですが、何となく作ってみました。

2002/11/27
境界線の下の方にマウスカーソルを持っていくと上下矢印になりそこでサイズを変更しようとするとおかしくなるとの御指摘がありバグフィックスしました。
WM_SETCURSORとWM_LBUTTONDOWNに変更が加わっています。

新しいサンプルファイル

を実行すると



こんな風になります。

古い(バグあり)サンプルファイルです

構造

実現方法ですが、このサンプルでは

メインウインドウ
左側のベースウインドウ右側のベースウインドウ
左側の子ウインドウ右側の子ウインドウ

のようなウインドウ階層を作っています。
ベースウインドウを子ウインドウより少し大きく作ることで、境界線を作りだしています。サンプルの境界線のように見えるところは、左側のベースウインドウの一部になっていて、この境界線をドラッグしたときの処理は全てベースウインドウのプロシージャでやってしまいます。このようにベースウインドウにサイズ変更等の処理を任せてしまうと、子ウインドウ側では分割サイズ等を気にしなくていいのでやりやすくなります。

プログラムの説明

プログラムの説明をしますが、例によってWinMainやウインドウの作成のあたりは省きます。

WndProc

メインウインドウのプロシージャです。必要なのはWM_CREATEとWM_SIZEです。WM_CREATEでは先ずベースウインドウを作ります。CreateSplitBaseに親ウインドウのハンドル、左側のウインドウの幅(%)、ベースウインドウの情報を格納する構造体のアドレス、を引数に渡します。

関数が成功すると、構造体にベースウインドウのハンドルとその上に子ウインドウを作る場合の大きさが返ってくるので、それらを使って子ウインドウを作ります。
WM_SIZEでは、メインウインドウの大きさにあわせてベースウインドウの大きさも変えます。

int CALLBACK WndProc(HWND hWnd, unsigned wMessage,
                  WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
    BASEINFO    BaseInfo;

    switch (wMessage)
    {
        case WM_CREATE:
            CenterWindow(hWnd);
            //ベースウインドウを作る
            if(CreateSplitBase(hWnd,30,&BaseInfo)==FALSE) return -1;
            //左側の子ウインドウを作る
            if(CreateChild(BaseInfo.hLeft,&BaseInfo.rcLeft)==NULL) return -1;
            //右側の子ウインドウを作る
            if(CreateChild(BaseInfo.hRight,&BaseInfo.rcRight)==NULL) return -1;
            return  0;

        case WM_DESTROY:
            PostQuitMessage(0);
            return  0;

        case WM_SIZE:
            //サイズの変更
            SetSplitSize(hWnd);
            return  0;

        case WM_PAINT:
            hdc = BeginPaint (hWnd, &ps);
            EndPaint (hWnd, &ps);
            return  0;
    }
    return (DefWindowProc(hWnd, wMessage, wParam, lParam));
}

InitSplitWindow

初期化関数です。忘れずにプログラムのはじめで呼んでください。ベースウインドウのウインドウクラスを登録してるだけです。
//初期化

BOOL InitSplitWindow(void)
{
    WNDCLASS    wndclass;

    wndclass.style          = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc    = (WNDPROC) SplitBaseProc;
    wndclass.cbClsExtra     = 0;
    wndclass.cbWndExtra     = 0;
    wndclass.hInstance      = Instance;
    wndclass.hIcon          = NULL;
    wndclass.hCursor        = NULL;
    wndclass.hbrBackground  = (HBRUSH)(COLOR_BTNFACE+1);
    wndclass.lpszMenuName   = NULL;
    wndclass.lpszClassName  = szSplitClassName;

    if (!RegisterClass (&wndclass))     return FALSE;

    return  TRUE;
}

CreateSplitBase

2つのベースウインドウを作る関数です。長いです。でもやってることは、

ベースウインドウを作る×2
ベースウインドウの親ウインドウのハンドルなんかをセット×2
子ウインドウの大きさを計算×2

だけです。2つ分あるのでそれで長くなってます。
//ベースウインドウを作る

BOOL CreateSplitBase(HWND hParent,int Rate,LPBASEINFO lpBaseInfo)
{
    HWND        hWndLeft,hWndRight;
    RECT        rc;
    int         LeftWidth,RightWidth,Height;

    GetClientRect(hParent,&rc);

    LeftWidth=rc.right*Rate/100;
    RightWidth=rc.right-LeftWidth;
    Height=rc.bottom;

    //左側のベースウインドウ作成
    hWndLeft = CreateWindowEx( 0,
                          szSplitClassName,
                          szSplitAppName,
                          WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN,
                          0,
                          0,
                          LeftWidth,
                          Height,
                          hParent,
                          NULL,
                          Instance,
                          NULL);
    if(hWndLeft==NULL) return FALSE;

    //右側のベースウインドウ作成
    hWndRight = CreateWindowEx( 0,
                          szSplitClassName,
                          szSplitAppName,
                          WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN,
                          LeftWidth,
                          0,
                          RightWidth,
                          Height,
                          hParent,
                          NULL,
                          Instance,
                          NULL);
    if(hWndRight==NULL) return FALSE;

    //ベースウインドウ情報を設定
    ZeroMemory(&g_LeftInfo,sizeof(g_LeftInfo));
    g_LeftInfo.hWnd=hWndLeft;
    g_LeftInfo.hParent=hParent;
    g_LeftInfo.hRight=hWndRight;
    SetWindowLong(hWndLeft,GWL_USERDATA,(LONG)&g_LeftInfo);

    //ベースウインドウ情報を設定
    ZeroMemory(&g_RightInfo,sizeof(g_RightInfo));
    g_RightInfo.hWnd=hWndRight;
    g_RightInfo.hParent=hParent;
    SetWindowLong(hWndRight,GWL_USERDATA,(LONG)&g_RightInfo);

    //呼び出したプログラムに、ウインドウハンドルと子ウインドウの大きさを返す
    lpBaseInfo->hLeft=hWndLeft;
    lpBaseInfo->rcLeft.left=0;
    lpBaseInfo->rcLeft.right=LeftWidth-BORDERWIDTH;
    lpBaseInfo->rcLeft.top=0;
    lpBaseInfo->rcLeft.bottom=rc.bottom;

    lpBaseInfo->hRight=hWndRight;
    lpBaseInfo->rcRight.left=0;
    lpBaseInfo->rcRight.right=RightWidth;
    lpBaseInfo->rcRight.top=0;
    lpBaseInfo->rcRight.bottom=rc.bottom;

    return  TRUE;
}

SetChildToBase

子ウインドウをベースウインドウに関連づけます。 これをしておかないと、ベースウインドウの大きさが変わったときに、子ウインドウの大きさを変更できません。
//子ウインドウをベースウインドウに関連づける

void SetChildToBase(HWND hBase,HWND hChild)
{
    LPBWNDINFO  lpBWndInfo;

    lpBWndInfo=(LPBWNDINFO)GetWindowLong(hBase,GWL_USERDATA);
    lpBWndInfo->hChild=hChild;
}

SetSplitSize

全体の大きさが変更されたときに、2つのベースウインドウの大きさを更新します。 左のウインドウの大きさはそのままで右のウインドウの大きさを変えています。
//全体の大きさを変更する

void SetSplitSize(HWND hParent)
{
    RECT    rc,rc2;
    int     LeftWidth,RightWidth,Height;

    GetClientRect(hParent,&rc);
    GetWindowRect(g_LeftInfo.hWnd,&rc2);
    LeftWidth=rc2.right-rc2.left;
    RightWidth=rc.right-LeftWidth;
    Height=rc.bottom-rc.top;
    if(RightWidth<0) RightWidth=0;

    SetWindowPos(g_LeftInfo.hWnd,NULL,0,0,LeftWidth,Height,SWP_NOZORDER);
    SetWindowPos(g_RightInfo.hWnd,NULL,LeftWidth,0,RightWidth,Height,SWP_NOZORDER);
}

SplitBaseProc

ベースウインドウのプロシージャです。今回のメインとなる部分なのでメッセージごとに説明をしていきます。

WM_CREATE

なにもしてません。
        case WM_CREATE:
            return  0;

WM_DESTROY

ここも何にもなし。
        case WM_DESTROY:
            return  0;

WM_SETCURSOR

ベースウインドウの右端と下端の数ドットが境界線の代わりになっているので、ウインドウの端にマウスカーソルが来たときに、右端なら左右の矢印、下端なら上下の矢印にマウスカーソルの形状を変更します。サンプルでは、どうせ端以外は子ウインドウで隠れているという考えからウインドウの下端なら上下矢印、それ以外は左右矢印にしています。

WM_NCHITTESTを細工しても同じ事が出来ると思いますが、今回はこうしました。

        case WM_SETCURSOR:
            //マウスカーソルの形を左右矢印か上下矢印のどちらかにする
            GetCursorPos(&pt);
            GetClientRect(hWnd,&rc);
            ScreenToClient(hWnd,&pt);
            
            //2002/11/27 左右境界線の下端で上下矢印になってしまうバグを修正
            //下側にベースあり&カーソルがウインドウの下部 → 上下矢印
            //左にベースウインドウがある → 左右矢印にする
            //それ以外は標準カーソル
            if(lpBWndInfo->hDown && pt.y>=rc.bottom-BORDERWIDTH) Cursor=IDC_SIZENS;  //上下矢印
            else if(lpBWndInfo->hRight)                          Cursor=IDC_SIZEWE;  //左右矢印
            else                                                 Cursor=IDC_ARROW;   //標準
 
            SetCursor(LoadCursor(NULL,Cursor));                 //カーソルの設定
            return  0;

WM_SIZE

ベースウインドウの大きさが変わったら、その上の子ウインドウの大きさも変えないといけないので、その大きさを計算します。子ウインドウの大きさはベースウインドウの大きさから境界線の長さ分を引くだけですが、境界線が必要ないところと必要なところを調べないといけません。サンプルでは、隣に他のベースウインドウがあれば境界が必要だと判定しています。
        case WM_SIZE:
            if(lpBWndInfo){
                //子ウインドウのサイズを決めます
                x=0;
                y=0;
                cx=LOWORD(lParam);
                cy=HIWORD(lParam);

                //境界線が必要ならその分の長さを引く
                if(lpBWndInfo->hRight) cx-=BORDERWIDTH;
                if(lpBWndInfo->hDown) cy-=BORDERWIDTH;
                //子ウインドウのサイズを変更する
                if(lpBWndInfo->hChild) SetWindowPos(lpBWndInfo->hChild,NULL,x,y,cx,cy,SWP_NOZORDER);
            }
            return  0;

WM_PAINT

なにもしていません。
        case WM_PAINT:
            hdc = BeginPaint (hWnd, &ps);
            EndPaint (hWnd, &ps);
            return  0;

WM_LBUTTONDOWN

境界線がクリックされたときの処理です。境界線以外は子ウインドウで隠されているので、無条件に境界線上だと判断してOKです。

高さを変更しようとしているのか幅を変更しようとしているのかを調べて、モードをs_Capture変数に保存しておきます。また、境界線には幅があるので、境界線のどの当たりでクリックされたのかを境界線の右端または下端からの差で保存しておきます。

        case WM_LBUTTONDOWN:
        
            //2002/11/27 左右境界線の下端でサイズ変更するとおかしくなるバグを修正
            //境界がクリックされたら、サイズ変更モードに移行します
            GetClientRect(hWnd,&rc);
            if(lpBWndInfo->hDown && MAKEPOINTS(lParam).y>=rc.bottom-BORDERWIDTH){   //高さ変更モード
                s_Capture=CT_TATE;
                s_Gap=rc.bottom-MAKEPOINTS(lParam).y;
            }else if(lpBWndInfo->hRight){                                           //幅変更モード
                s_Capture=CT_YOKO;
                s_Gap=rc.right-MAKEPOINTS(lParam).x;
            }else{
                break;  //何もしない
            }
         
            SetCapture(hWnd);
            break;

WM_LBUTTONUP

サイズ変更を終了します。マウスのキャプチャーを解除してs_Captureに0を設定します。
        case WM_LBUTTONUP:
            //サイズ変更モードの終了
            if(s_Capture){
                ReleaseCapture();
                s_Capture=0;
            }
            break;

WM_MOUSEMOVE

いやになるほど長いですが、大きさ変更処理のメイン部です。上下移動の時と左右移動の時の2つの処理があるので長くなっています。一緒に出来るといいのですが...
サイズ計算は少しややこしいですが、がんばってソースを見て解析してください。

2つのウインドウサイズが計算できたら、SetWindowPosでサイズの更新をするのですが、そのままだとちらついてしまうので、LockWindowUpdateを使ってウインドウの更新をロックしておきます。

        case WM_MOUSEMOVE:
            //サイズ変更モードになっていた場合、ベースウインドウのサイズを変更する
            if(s_Capture){
                //ベースウインドウの大きさを取得
                GetWindowRect(hWnd,&rc);
                pt.x=rc.left;
                pt.y=rc.top;
                //ベースウインドウの親ウインドウ内での座標に変換
                ScreenToClient(lpBWndInfo->hParent,&pt);
                if(s_Capture==CT_TATE){         //高さ変更モード時
                    //上側のウインドウのサイズを計算
                    x=pt.x;
                    y=pt.y;
                    cx=rc.right-rc.left;
                    cy=MAKEPOINTS(lParam).y+s_Gap;
                    if(cy<MINWIDTH) cy=MINWIDTH;

                    //下側のウインドウの大きさを計算
                    hWnd2=lpBWndInfo->hDown;
                    GetWindowRect(hWnd2,&rc2);
                    cx2=cx;
                    cy2=rc2.bottom-rc.top-cy;   //2つのウインドウの高さを足したものから、
                    if(cy2<MINWIDTH){           //1つ目のウインドウの高さを引く
                        cy2=MINWIDTH;
                        cy=rc2.bottom-rc.top-cy2;
                        if(cy<MINWIDTH) cy=MINWIDTH;
                    }
                    x2=x;
                    y2=y+cy;
                }else{                          //幅変更モード時
                    //左側のウインドウのサイズを計算
                    x=pt.x;
                    y=pt.y;
                    cx=MAKEPOINTS(lParam).x+s_Gap;
                    cy=rc.bottom-rc.top;
                    if(cx<MINWIDTH) cx=MINWIDTH;

                    //右側のウインドウのサイズを計算
                    hWnd2=lpBWndInfo->hRight;
                    GetWindowRect(hWnd2,&rc2);
                    cx2=rc2.right-rc.left-cx;   //2つのウインドウの幅を足したものから、
                    cy2=cy;                     //1つ目のウインドウの幅を引く
                    if(cx2<MINWIDTH){
                        cx2=MINWIDTH;
                        cx=rc2.right-rc.left-cx2;
                        if(cx<MINWIDTH) cx=MINWIDTH;
                    }
                    x2=x+cx;
                    y2=y;
                }
                //ちらつき防止のために、ウインドウの更新を一時ストップ
                LockWindowUpdate(lpBWndInfo->hParent);
                SetWindowPos(hWnd,NULL,x,y,cx,cy,SWP_NOZORDER);
                SetWindowPos(hWnd2,NULL,x2,y2,cx2,cy2,SWP_NOZORDER);
                //更新再開
                LockWindowUpdate(NULL);
                //再開しただけでは書き直されないので、再描画
                UpdateWindow(lpBWndInfo->hParent);
            }
            break;

おわりに

このサンプルでは横に分割していますが、多少変更すれば縦分割も出来るようになっています。SplitBaseProcは、縦横両用に作ってあるので修正の必要はありません。CreateSplitBaseとSetSplitSizeを修正してください。


to sdk prev next