ソフトウェア開発 Win32プログラミング

スプリッタで操作画面を分割する

戻る


操作画面を分割するときには、スプリッタ ウィンドウ(splitter window)と呼ばれるものを使って、分割幅を調節できるようにすることが多い。

自作のスプリッタウィンドウ(MSplitterWndクラス)を以下に示す。

#include "MWindowBase.hpp"
#include <vector>

// The styles of MSplitterWnd
#define SWS_HORZ            0
#define SWS_VERT            1
#define SWS_LEFTALIGN       0
#define SWS_TOPALIGN        0
#define SWS_RIGHTALIGN      2
#define SWS_BOTTOMALIGN     2

class MSplitterWnd : public MWindowBase
{
public:
    enum { m_cxyBorder = 4, m_cxyMin = 8 };

    MSplitterWnd() : m_iDraggingBorder(-1), m_nPaneCount(0)
    {
        m_vecPanes.resize(1);
    }

    BOOL CreateDx(HWND hwndParent, INT nPaneCount = 2,
                  DWORD dwStyle = WS_CHILD | WS_VISIBLE | SWS_HORZ | SWS_LEFTALIGN,
                  DWORD dwExStyle = 0)
    {
        RECT rc;
        GetClientRect(hwndParent, &rc);

        if (!CreateWindowDx(hwndParent, NULL, dwStyle, dwExStyle,
                            rc.left, rc.top,
                            rc.right - rc.left, rc.bottom - rc.top))
        {
            return FALSE;
        }

        SetPaneCount(nPaneCount);
        PostMessageDx(WM_SIZE);
        return TRUE;
    }

    BOOL IsHorizontal() const
    {
        return !IsVertical();
    }
    BOOL IsVertical() const
    {
        return (GetStyleDx() & SWS_VERT) == SWS_VERT;
    }
    BOOL IsRightBottomAlign() const
    {
        return (GetStyleDx() & SWS_RIGHTALIGN) == SWS_RIGHTALIGN;
    }

    INT GetPaneCount() const
    {
        return m_nPaneCount;
    }
    VOID SetPaneCount(INT nCount)
    {
        m_vecPanes.resize(nCount + 1);
        m_nPaneCount = nCount;
        SplitEqually();
    }

    HWND GetPane(INT nIndex) const
    {
        assert(0 <= nIndex && nIndex < m_nPaneCount);
        return m_vecPanes[nIndex].hwndPane;
    }
    VOID SetPane(INT nIndex, HWND hwndPane)
    {
        if (m_nPaneCount == 0)
            return;

        assert(0 <= nIndex && nIndex < m_nPaneCount);
        m_vecPanes[nIndex].hwndPane = hwndPane;
    }

    INT GetPanePos(INT nIndex) const
    {
        assert(0 <= nIndex && nIndex <= m_nPaneCount);
        return m_vecPanes[nIndex].xyPos;
    }
    VOID SetPanePos(INT nIndex, INT nPos, BOOL bBounded = TRUE)
    {
        if (m_nPaneCount == 0)
            return;

        assert(0 <= nIndex && nIndex <= m_nPaneCount);
        if (nIndex == 0)
            return;

        if (bBounded)
        {
            if (nIndex < m_nPaneCount)
            {
                const PANEINFO& info = m_vecPanes[nIndex];
                const PANEINFO& next_info = m_vecPanes[nIndex + 1];
                if (next_info.xyPos < nPos + info.cxyMin)
                    nPos = next_info.xyPos - info.cxyMin;
            }

            const PANEINFO& prev_info = m_vecPanes[nIndex - 1];
            if (nPos < prev_info.xyPos + prev_info.cxyMin)
                nPos = prev_info.xyPos + prev_info.cxyMin;
        }

        m_vecPanes[nIndex].xyPos = nPos;
    }

    VOID SetPaneExtent(INT nIndex, INT cxy, BOOL bUpdate = TRUE)
    {
        if (m_nPaneCount == 0)
            return;

        assert(0 <= nIndex && nIndex < m_nPaneCount);
        if (nIndex == m_nPaneCount - 1)
        {
            SetPanePos(nIndex, m_vecPanes[m_nPaneCount].xyPos - cxy);
        }
        else
        {
            SetPanePos(nIndex + 1, m_vecPanes[nIndex].xyPos + cxy);
        }
        UpdatePanes();
    }

    VOID SetPaneMinExtent(INT nIndex, INT cxyMin = MSplitterWnd::m_cxyMin)
    {
        if (m_nPaneCount == 0)
            return;

        assert(0 <= nIndex && nIndex < m_nPaneCount);
        m_vecPanes[nIndex].cxyMin = cxyMin;
    }

    INT GetTotalMinExtent() const
    {
        INT cxy = 0;
        for (INT i = 0; i < m_nPaneCount; ++i)
        {
            cxy += m_vecPanes[i].cxyMin;
        }
        return cxy;
    }

    VOID GetPaneRect(INT nIndex, RECT *prc) const
    {
        assert(0 <= nIndex && nIndex < m_nPaneCount);
        ::GetClientRect(m_hwnd, prc);
        if (IsVertical())
        {
            prc->top = m_vecPanes[nIndex].xyPos;
            prc->bottom = m_vecPanes[nIndex + 1].xyPos;
            if (nIndex < m_nPaneCount - 1)
                prc->bottom -= m_cxyBorder;
        }
        else
        {
            prc->left = m_vecPanes[nIndex].xyPos;
            prc->right = m_vecPanes[nIndex + 1].xyPos;
            if (nIndex < m_nPaneCount - 1)
                prc->right -= m_cxyBorder;
        }
    }

    INT HitTestBorder(POINT ptClient) const
    {
        RECT rcClient;
        ::GetClientRect(m_hwnd, &rcClient);
        if (!::PtInRect(&rcClient, ptClient))
            return -1;

        INT xy = (IsVertical() ? ptClient.y : ptClient.x);
        for (INT i = 1; i < m_nPaneCount; ++i)
        {
            INT xyPos = m_vecPanes[i].xyPos;
            if (xyPos - m_cxyBorder <= xy && xy <= xyPos)
            {
                return i;
            }
        }
        return -1;
    }

    void SplitEqually()
    {
        if (m_nPaneCount == 0)
            return;

        RECT rc;
        GetClientRect(m_hwnd, &rc);

        INT cxy = (IsVertical() ? rc.bottom : rc.right);
        INT xy = 0, cxyPane = cxy / m_nPaneCount;
        for (INT i = 0; i < m_nPaneCount; ++i)
        {
            m_vecPanes[i].xyPos = xy;
            xy += cxyPane;
        }
        m_vecPanes[m_nPaneCount].xyPos = cxy;
        PostMessageDx(WM_SIZE);
    }

    void UpdatePanes()
    {
        RECT rc;
        HDWP hDWP = BeginDeferWindowPos(m_nPaneCount);
        for (INT i = 0; i < m_nPaneCount; ++i)
        {
            const PANEINFO *pInfo = &m_vecPanes[i];
            HWND hwndPane = pInfo->hwndPane;
            if (hwndPane == NULL)
                continue;

            GetPaneRect(i, &rc);
            hDWP = DeferWindowPos(hDWP, hwndPane, NULL, 
                rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
                SWP_NOZORDER | SWP_NOACTIVATE);
        }
        EndDeferWindowPos(hDWP);
    }

    virtual LRESULT CALLBACK
    WindowProcDx(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
        switch (uMsg)
        {
        HANDLE_MSG(hwnd, WM_SIZE, OnSize);
        HANDLE_MSG(hwnd, WM_LBUTTONDOWN, OnLButtonDown);
        HANDLE_MSG(hwnd, WM_LBUTTONDBLCLK, OnLButtonDown);
        HANDLE_MSG(hwnd, WM_LBUTTONUP, OnLButtonUp);
        HANDLE_MSG(hwnd, WM_MOUSEMOVE, OnMouseMove);
        HANDLE_MSG(hwnd, WM_SETCURSOR, OnSetCursor);
        HANDLE_MSG(hwnd, WM_COMMAND, OnCommand);
        HANDLE_MSG(hwnd, WM_NOTIFY, OnNotify);
        case WM_CAPTURECHANGED:
            m_iDraggingBorder = -1;
            return 0;
        default:
            return DefaultProcDx();
        }
    }

    virtual LPCTSTR GetWndClassNameDx() const
    {
        return TEXT("MZC4 MSplitterWnd Class");
    }

    virtual VOID ModifyWndClassDx(WNDCLASSEX& wcx)
    {
    }

    void OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
    {
        FORWARD_WM_COMMAND(GetParent(hwnd), id, hwndCtl, codeNotify, PostMessage);
    }

    LRESULT OnNotify(HWND hwnd, int idFrom, LPNMHDR pnmhdr)
    {
        return FORWARD_WM_NOTIFY(GetParent(hwnd), idFrom, pnmhdr, SendMessage);
    }

protected:
    struct PANEINFO
    {
        HWND    hwndPane;
        INT     xyPos;
        INT     cxyMin;

        PANEINFO()
        {
            hwndPane = NULL;
            xyPos = 0;
            cxyMin = m_cxyMin;
        }
    };
    INT                     m_iDraggingBorder;
    INT                     m_nPaneCount;
    std::vector<PANEINFO>   m_vecPanes;

    void OnLButtonDown(HWND hwnd, BOOL fDoubleClick, int x, int y, UINT keyFlags)
    {
        if (fDoubleClick)
            return;

        POINT pt = { x, y };
        INT iBorder = HitTestBorder(pt);
        if (iBorder < 0)
            return;

        ::SetCapture(hwnd);
        m_iDraggingBorder = iBorder;

        if (IsVertical())
            ::SetCursor(::LoadCursor(NULL, IDC_SIZENS));
        else
            ::SetCursor(::LoadCursor(NULL, IDC_SIZEWE));
    }

    void OnLButtonUp(HWND hwnd, int x, int y, UINT keyFlags)
    {
        if (m_iDraggingBorder == -1)
            return;

        SetPanePos(m_iDraggingBorder, (IsVertical() ? y : x) + m_cxyBorder / 2);
        UpdatePanes();

        m_iDraggingBorder = -1;
        ::ReleaseCapture();
    }

    void OnMouseMove(HWND hwnd, int x, int y, UINT keyFlags)
    {
        if (m_iDraggingBorder == -1)
            return;

        SetPanePos(m_iDraggingBorder, (IsVertical() ? y : x) + m_cxyBorder / 2);
        UpdatePanes();
    }

    void OnSize(HWND hwnd, UINT state, int cx, int cy)
    {
        if (m_nPaneCount == 0)
            return;

        RECT rc;
        GetClientRect(hwnd, &rc);
        INT cxy = (IsVertical() ? rc.bottom : rc.right);
        Resize(cxy);
    }

    void Resize(INT cxy)
    {
        if (IsRightBottomAlign())
        {
            INT dxy = cxy - m_vecPanes[m_nPaneCount].xyPos;
            for (INT i = 1; i < m_nPaneCount; ++i)
            {
                SetPanePos(i, m_vecPanes[i].xyPos + dxy);
            }
        }

        SetPanePos(m_nPaneCount, cxy);
        UpdatePanes();
    }

    BOOL OnSetCursor(HWND hwnd, HWND hwndCursor, UINT codeHitTest, UINT msg)
    {
        POINT pt;
        ::GetCursorPos(&pt);
        ::ScreenToClient(hwnd, &pt);

        if (HitTestBorder(pt) == -1)
        {
            ::SetCursor(::LoadCursor(NULL, IDC_ARROW));
            return TRUE;
        }

        if (IsVertical())
            ::SetCursor(::LoadCursor(NULL, IDC_SIZENS));
        else
            ::SetCursor(::LoadCursor(NULL, IDC_SIZEWE));
        return TRUE;
    }
};

m_vecPanesメンバーに格納されるPANEINFO構造体は、 ペイン(pane、子ウィンドウ)の情報を保持する。 hwndPaneがペインのウィンドウハンドルで、xyPosが位置を表し、cxyMinが最小サイズを意味する。

マウスの左ボタンが押されたら、OnLButtonDownメソッドが呼ばれ、 境界線の当たり判定のHitTestBorderメソッドによって境界線上にあるか確認する。 境界線上にあれば、キャプチャーをセットして、マウスポインタの形状をセットする。 ここでマウスポインタの形状をセットしなければ、クリックされたときに反応しないので注意する。

キャプチャーされていれば、ドラッグしている間、OnMouseMoveメソッドが呼ばれる。 マウスの位置に応じて、境界線を移動する。さらに境界線に合わせて ペイン(子ウィンドウ)の位置を補正する。

ウィンドウ上で、もしくはキャプチャーしているときにマウスを移動しているときは、 OnSetCursorメソッドが呼ばれるので、適切なマウスポインタの形状を セットする。左ボタンを離すと、OnLButtonUpメソッドが呼ばれる。そのとき、 キャプチャーを解放し、マウスの位置に合わせて境界線を移動し、ドラッグを終了する。

SetPanePosはペインの位置を決める関数である。bBoundedがTRUEであれば、指定された位置を補正した結果を使う。 このとき、隣の境界線の位置やcxyMinの情報が使われて、ペインの位置が補正される。 実際にペインの位置を補正するのは、UpdatePanesメソッドである。実際にDeferWindowPos関数を使って 画面のちらつきを防止している。

OnCommandとOnNotifyメソッドは、親ウィンドウにWM_COMMANDとWM_NOTIFYメッセージを通知するために ある。これらのメソッドは、MSplitterWndの子ウィンドウに対するアクションを親に通知するために 必要になる。例えば、MSplitterWndの子ウィンドウのツリービューがクリックされたとき、反応しないと困るだろう。

例として、MSplitterWndクラスを使うメインウィンドウを見てみよう。

    BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
    {
        DWORD style = WS_CHILD | WS_VISIBLE | SWS_HORZ | SWS_RIGHTALIGN;
        DWORD exstyle = 0;
        if (!m_splitter.CreateDx(hwnd, 3, style, exstyle))
            return FALSE;

        for (INT i = 0; i < 3; ++i)
        {
            TCHAR szText[64];
            wsprintf(szText, TEXT("m_edit[%d]"), i);
            style = WS_CHILD | WS_VISIBLE | ES_CENTER;
            exstyle = WS_EX_CLIENTEDGE;
            if (!m_edit[i].CreateAsChildDx(m_splitter, szText, style, exstyle))
                return FALSE;

            m_splitter.SetPane(i, m_edit[i]);
        }

        m_splitter.SetPaneExtent(0, 50);
        m_splitter.SetPaneMinExtent(1, 100);

        return TRUE;
    }

    ...

    void OnSize(HWND hwnd, UINT state, int cx, int cy)
    {
        RECT rc;
        GetClientRect(hwnd, &rc);
        MoveWindow(m_splitter, 0, 0, rc.right, rc.bottom, TRUE);
    }

SWS_HORZとSWS_RIGHTALIGNは、MSplitterWndのウィンドウスタイルである。 SWS_HORZは水平向き(horizontal)を意味し、SWS_RIGHTALIGNは右そろえを意味する。 m_editは、エディットコントロールのウィンドウの配列であり、ウィンドウを作成した後、 SetPaneでペインとしてインデックス(index)を指定してセットしている。 SetPaneExtentでペインのサイズを指定し、SetPaneMinExtentで最小幅をセットしている。

m_editに拡張スタイルWS_EX_CLIENTEDGEを指定しているのは、ウィンドウの周りを へこんだように描画させるためである。このへこみがなければ、境界線が分かりづらい。

ソース: https://github.com/katahiromz/SplitterSample


戻る

©片山博文MZ
katayama.hirofumi.mz@gmail.com

inserted by FC2 system