概要

分割ウィンドウとは、ひとつのフレームウィンドウのクライアント領域を複数のペインに分割して表示するウィンドウ構成である。
Microsoft Foundation Classes (MFC) では、CSplitterWnd クラスを使用してこの機能を実現できる。

各ペインは通常、異なる CView 派生クラス、または 同じ CView 派生クラスのインスタンスを割り当てる。

MFCの分割ウィンドウには、静的スプリッタと動的スプリッタの2種類が存在する。

静的スプリッタは CreateStatic() を使用して、作成時に決めた行数と列数のペインを全て生成する。
ユーザはペインのサイズを変更できるが、ペインの数や順序は変更できない。
<br. 動的スプリッタは Create() を使用して、初期状態では1つのペインから始まる。
ユーザがスプリッタボックスを操作すると、フレームワークが新しいペインを動的に作成・破棄する。

静的スプリッタは異なるビュークラスを組み合わせて、複数の情報を同時に表示する用途に適している。
動的スプリッタは、同じドキュメントを複数の視点やスクロール位置で確認する用途に適している。

CSplitterWnd クラスは CFrameWnd または CMDIChildWnd の派生クラスに埋め込んで使用する。

実装の中核は、フレームクラスの OnCreateClient() をオーバーライドして、その中で CreateStatic() または Create() を呼び出すことである。
その後、静的スプリッタの場合は CreateView() を使用して、各ペインにビューを割り当てる必要がある。

分割ウィンドウは、ドキュメント/ビューアーキテクチャと組み合わせることで、複雑なユーザインターフェースを比較的少ないコードで構築できる。

ただし、MFCは引き続きサポートされているが、新機能の追加やドキュメントの更新は行われていないことに注意すること。


前提条件

分割ウィンドウを理解するためには、以下に示す知識と環境が必要である。

  • C++によるデスクトップ開発ワークロードを有効にする。
  • ドキュメント / ビューアーキテクチャの基礎知識
    CWinAppCDocumentCViewCFrameWnd の関係を理解していること。
  • C++とWindows APIの知識
    ポインタ、参照、メッセージハンドラ、CRuntimeClass クラス等を理解していること。
  • DECLARE_DYNCREATE / IMPLEMENT_DYNCREATE マクロの理解
    分割ウィンドウの各ペインに割り当てるViewクラスは、動的生成が可能である必要がある。



分割ウィンドウの種類

MFCで提供される分割ウィンドウには、静的スプリッタと動的スプリッタの2種類がある。
用途に応じて適切な方を選択すること。

静的スプリッタと動的スプリッタの比較
項目 静的スプリッタ (CreateStatic) 動的スプリッタ (Create)
ペイン数 固定 ユーザ操作で増減
最大分割数 16行 x 16列 2行 x 2列
ペインのクラス 異なる CView 派生クラスを割り当て可能 基本的に同じ CView 派生クラス
スクロールバー 共有スクロールバーを WS_HSCROLL / WS_VSCROLL で追加可能 動的に生成されるペインと連動
使用例 Windowsエクスプローラー
Visual Studioのグラフィックスエディタ
Microsoft Excel
Microsoft Word
主な用途 異なる情報を同じウィンドウ内に同時表示 同じドキュメントを複数視点で確認


静的スプリッタを使用する場合、全てのペインを OnCreateClient() 内で明示的に作成する必要がある。

動的スプリッタを使用する場合、フレームワークがペインの作成と破棄を自動的に行う。


基本的な実装手順

分割ウィンドウを実装する時の流れを以下に示す。

  1. フレームクラスに CSplitterWnd クラスのメンバ変数を追加する。
    SDIアプリケーションの場合は、CMainFrame クラス、MDIアプリケーションの場合は、CChildFrame クラスに追加する。
  2. フレームクラスの OnCreateClient() をオーバーライドする。
    クラスビューから OnCreateClient() を追加する、または、ヘッダファイルで手動で宣言する。
  3. OnCreateClient() 内で CreateStatic() または Create() を呼び出す。
    静的スプリッタの場合は CreateStatic()、動的スプリッタの場合は Create() を使用する。
  4. 静的スプリッタの場合、全てのペインに CreateView() でビューを割り当てる。
    各ペインに対応する CView 派生クラスを RUNTIME_CLASS マクロで指定する。
  5. 必要に応じて、SetRowInfo() / SetColumnInfo() でサイズを調整する。
    リサイズ時には、OnSize() ハンドラで再調整することもある。


下図は、ドキュメント / ビューアーキテクチャにおける分割ウィンドウの位置づけを示している。

 



静的分割ウィンドウ

静的分割ウィンドウは、CreateStatic() を使用して作成する。

ユーザはペインの境界線をドラッグしてサイズを変更できるが、ペインの数は増減しない。

左右2分割の作成

1行2列の分割ウィンドウを作成する例を示す。

左側のペイン (Pane A) にはプロジェクト作成時に生成されたViewクラスを使用し、右側のペイン (Pane B) には新規に作成したViewクラスを使用する。
左側がPaneA (CPaneAView)、右側がPaneB (CPaneBView) となっている。

 


 


まず、右ペイン用のViewクラスを追加する。
ソリューションエクスプローラーでプロジェクトを右クリックして、[追加] - [クラス] を選択する。

  • クラスの種類
    MFCクラス
  • クラス名
    CPaneBView
  • 基本クラス
    CFormView または CView


次に、CMainFrame.hCSplitterWnd のメンバ変数を追加する。
MDIアプリケーションの場合は CChildFrame.h に同様の変更を行う。

 // CMainFrame.h
 
 class CMainFrame : public CFrameWndEx
 {
    // ...
 
 public:
    CSplitterWnd m_SplitMain;  // 分割ウィンドウ
    // ...
 };


次に、CMainFrame.cppOnCreateClient() を以下に示すように実装する。

 // CMainFrame.cpp
 
 #include "CPaneDoc.h"
 #include "CPaneAView.h"
 #include "CPaneBView.h"
 
 BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
 {
    // 1行2列の静的スプリッタを作成
    if (!m_SplitMain.CreateStatic(this, 1, 2))
    {
       TRACE0("Failed to create splitter window\n");
       return FALSE;
    }
 
    // ペインA (row=0, col=0)
    if (!m_SplitMain.CreateView(0, 0, RUNTIME_CLASS(CPaneAView), CSize(200, 0), pContext))
    {
       TRACE0("Failed to create pane A\n");
       return FALSE;
    }
 
    // ペインB (row=0, col=1)
    if (!m_SplitMain.CreateView(0, 1, RUNTIME_CLASS(CPaneBView), CSize(0, 0), pContext))
    {
       TRACE0("Failed to create pane B\n");
       return FALSE;
    }
 
    return TRUE;
 }


CreateStatic() の第2引数は行数、第3引数は列数である。

CreateView() の第1引数と第2引数は、それぞれ行番号と列番号を0始まりで指定する。
第3引数には、ペインを管理する CView 派生クラスの CRuntimeClass クラスを RUNTIME_CLASS マクロで指定する。
第4引数の CSize クラスは、理想サイズを示す。
第5引数の pContext は、親フレームから渡された作成コンテキストをそのまま渡す。

ネストした3分割の作成

CreateStatic() は、簡単な n行 x m列 のグリッドしか直接作成できない。

例えば、左側に1つ、右側に上下2つという変則的なレイアウトを作成する場合は、2つの CSplitterWnd クラスをネストする。
具体的には、メインフレームを左右に分割した後、右側のペインをさらに上下に分割する。

 


まず、Pane A、Pane B、Pane C向けのViewクラスをそれぞれ作成する。

CMainFrame.h には、メインスプリッタとサブスプリッタの2つのメンバ変数を追加する。

 // CMainFrame.h
 
 class CMainFrame : public CFrameWndEx
 {
    // ...
 
 public:
    CSplitterWnd m_SplitMain;  // メインフレームを左右分割
    CSplitterWnd m_SplitSub;   // 右ペインを上下分割
    // ...
 };


次に、CMainFrame.cppOnCreateClient() を以下に示すように定義する。

 // CMainFrame.cpp
 
 #include "CPaneDoc.h"
 #include "CPaneAView.h"
 #include "CPaneBView.h"
 #include "CPaneCView.h"
 
 BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
 {
    // メインスプリッタを左右2分割
    if (!m_SplitMain.CreateStatic(this, 1, 2))
    {
       TRACE0("Failed to create main splitter\n");
       return FALSE;
    }
 
    // サブスプリッタを右ペインに上下2分割
    if (!m_SplitSub.CreateStatic(&m_SplitMain, 2, 1, WS_CHILD | WS_VISIBLE | WS_BORDER, m_SplitMain.IdFromRowCol(0, 1)))
    {
       TRACE0("Failed to create sub splitter\n");
       return FALSE;
    }
 
    // ペインA (左側)
    if (!m_SplitMain.CreateView(0, 0, RUNTIME_CLASS(CPaneAView), CSize(320, 240), pContext))
    {
       TRACE0("Failed to create pane A\n");
       return FALSE;
    }
 
    // ペインB (右側上部)
    if (!m_SplitSub.CreateView(0, 0, RUNTIME_CLASS(CPaneBView), CSize(320, 120), pContext))
    {
       TRACE0("Failed to create pane B\n");
       return FALSE;
    }
 
    // ペインC (右側下部)
    if (!m_SplitSub.CreateView(1, 0, RUNTIME_CLASS(CPaneCView), CSize(320, 120), pContext))
    {
       TRACE0("Failed to create pane C\n");
       return FALSE;
    }
 
    return TRUE;
 }


サブスプリッタの親ウィンドウには、メインスプリッタのアドレス &m_SplitMain を指定する。
第5引数の子ウィンドウIDには、m_SplitMain.IdFromRowCol(0, 1) を使用して、メインスプリッタの右ペインのIDを取得する。

この指定が無いと、サブスプリッタが正しい領域に配置されない。


動的分割ウィンドウ

動的分割ウィンドウは、Create() を使用して作成する。

初期状態では1つのペインが表示され、ユーザがスプリッタボックスをドラッグするか、[ウィンドウ] - [分割]メニューを選択することにより、新しいペインが作成される。
MFCアプリケーションウィザードで[分割ウィンドウ]オプションを有効にすると、雛形が自動生成される。

CMainFrame.h にメンバ変数を追加する。

 // CMainFrame.h
 
 class CMainFrame : public CFrameWndEx
 {
    // ...
 
 protected:
    CSplitterWnd m_wndSplitter;
    // ...
 };


CMainFrame.cppOnCreateClient() を以下に示すように定義する。

 // CMainFrame.cpp
 
 BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT /*lpcs*/, CCreateContext* pContext)
 {
    return m_wndSplitter.Create(
       this,
       2, 2,              // 最大行数、最大列数
       CSize(10, 10),     // 最小ペインサイズ
       pContext);
 }


動的スプリッタでは、CreateView() を明示的に呼び出す必要はない。
フレームワークが必要に応じてビューを動的に生成するからである。

ただし、動的スプリッタの全てのペインは、同じ CView 派生クラスを使用する。


ペイン間のアクセス

分割ウィンドウ内のあるペインから、別のペインの View オブジェクトにアクセスしたい場合がある。
例えば、左ペインで選択した項目に応じて、右ペインの内容を更新する場合である。

各ペインは CSplitterWnd::GetPane メンバ関数で取得できる。
SDI アプリケーションの場合、AfxGetMainWnd を使ってメインフレームを取得し、フレームが保持するスプリッタを経由してアクセスする。

以下は、Pane A の OnDraw 内から Pane B および Pane C にアクセスする例である。
ネストした3分割の例を前提としている。

 // CPaneAView.h
 #include "CPaneBView.h"
 #include "CPaneCView.h"
 #include "MainFrm.h"
 
 class CPaneAView : public CFormView
 {
    // ...
 public:
    CPaneBView* pPaneB;
    CPaneCView* pPaneC;
    // ...
 };
 // CPaneAView.cpp
 #include "MainFrm.h"
 
 void CPaneAView::OnDraw(CDC* pDC)
 {
    // ...
    CMainFrame* pFrame = static_cast<CMainFrame*>(AfxGetMainWnd());
    pPaneB = static_cast<CPaneBView*>(pFrame->m_SplitSub.GetPane(0, 0));
    pPaneC = static_cast<CPaneCView*>(pFrame->m_SplitSub.GetPane(1, 0));
 
    if (pPaneB != nullptr)
    {
       pPaneB->Invalidate();
    }
    // ...
 }


GetPane の戻り値は CWnd* なので、使用する際は適切な View クラスにキャストする。
MDI アプリケーションの場合は、AfxGetMainWnd ではなく子フレームへのポインタを取得する必要がある。


スプリッターバーの固定化

ユーザがスプリッターバーをドラッグしてペインサイズを変更できないようにする方法を説明する。
最も確実な方法は、CSplitterWnd の派生クラスを作成し、HitTest メンバ関数をオーバーライドすることである。
HitTest は、指定された座標がスプリッタのどの部分に該当するかを示す整数値を返す。
戻り値が 0 (noHit) の場合、フレームワークはその位置での操作を無視する。
そのため、特定の方向のスプリッターバーに対して 0 を返すことで、その方向の移動を禁止できる。

まず、CSplitterWnd の派生クラスを作成する。

 // CMySplitterWnd.h
 #pragma once
 #include <afxext.h>
 
 class CMySplitterWnd : public CSplitterWnd
 {
    DECLARE_DYNAMIC(CMySplitterWnd)
 
 public:
    CMySplitterWnd();
    virtual ~CMySplitterWnd();
 
    virtual int HitTest(CPoint pt) const;
 };
 // CMySplitterWnd.cpp
 #include "pch.h"
 #include "CMySplitterWnd.h"
 
 IMPLEMENT_DYNAMIC(CMySplitterWnd, CSplitterWnd)
 
 CMySplitterWnd::CMySplitterWnd() {}
 CMySplitterWnd::~CMySplitterWnd() {}
 
 int CMySplitterWnd::HitTest(CPoint pt) const
 {
    int ht = CSplitterWnd::HitTest(pt);
 
    // 縦方向のスプリッターバーと交差点を無効化
    if (ht >= hSplitterBar1 && ht <= hSplitterBar15)
    {
       ht = noHit;
    }
    if (ht >= splitterIntersection1 && ht <= splitterIntersection225)
    {
       ht = noHit;
    }
 
    return ht;
 }


上記の例では、縦方向のスプリッターバーと交差点での操作を無効化している。
横方向のスプリッターバーだけ有効にしたい場合は、縦方向と交差点のみを 0 にすればよい。
noHithSplitterBar1splitterIntersection1 等の値は CSplitterWnd の実装内で定義されており、必要に応じて独自に定義する。


ウィンドウリサイズ対応

フレームウィンドウがリサイズされたときに、ペインのサイズを一定の比率で保ちたい場合がある。

その場合、CFrameWnd::OnSize() をオーバーライドして、SetRowInfo() / SetColumnInfo() / RecalcLayout() を呼び出す。

以下の例では、左右2分割のスプリッタで、右半分を2等分し、左半分も同じ幅に調整している。

 // CMainFrame.cpp
 
 void CMainFrame::OnSize(UINT nType, int cx, int cy)
 {
    CFrameWndEx::OnSize(nType, cx, cy);
 
    if (m_bSplitterCreated && nType != SIZE_MINIMIZED)
    {
       int nHalf = cx / 2;
       m_SplitMain.SetColumnInfo(0, nHalf, 50);
       m_SplitMain.SetColumnInfo(1, nHalf, 50);
       m_SplitMain.RecalcLayout();
    }
 }


上記の例では、m_bSplitterCreated フラグでスプリッタの作成完了を確認している。
このフラグは OnCreateClient() でスプリッタ作成成功後に TRUE に設定する。

OnSize() はフレーム作成時にスプリッタがまだ存在しない段階でも呼ばれる可能性があるため、このチェックは必須である。

ネストしたスプリッタの場合は、メインスプリッタとサブスプリッタの両方で RecalcLayout() を呼び出す必要がある。

 // ネストしたスプリッタの例
 
 void CMainFrame::OnSize(UINT nType, int cx, int cy)
 {
    CFrameWndEx::OnSize(nType, cx, cy);
 
    if (m_bSplitterCreated && nType != SIZE_MINIMIZED)
    {
       int nLeft = cx / 3;
       int nRight = cx - nLeft;
       m_SplitMain.SetColumnInfo(0, nLeft, 50);
       m_SplitMain.SetColumnInfo(1, nRight, 50);
       m_SplitMain.RecalcLayout();
 
       int nRightHalf = nRight / 2;
       m_SplitSub.SetRowInfo(0, nRightHalf, 30);
       m_SplitSub.SetRowInfo(1, nRightHalf, 30);
       m_SplitSub.RecalcLayout();
    }
 }



トラブルシューティング

問題1: ウィンドウが正しく表示されない

  • 原因
    静的スプリッタでは、OnCreateClient() が戻る前に全てのペインを CreateView で作成していない。
  • 解決方法
    CreateStatic() 呼び出し後、全ての行 / 列に対して CreateView() を呼び出す。


問題2: 分割線がドラッグできない

  • 原因
    CreateStatic() の第4引数で WS_VISIBLE を指定していない、または、ペインの最小幅 / 高さが大きすぎる。
  • 解決方法
    スタイルに WS_CHILD | WS_VISIBLE を含める。
    必要に応じて SetRowInfo() / SetColumnInfo() の最小幅を小さくする。


問題3: サブスプリッタが正しい領域に表示されない

  • 原因
    ネストしたスプリッタの子ウィンドウIDが正しく指定されていない。
  • 解決方法
    親スプリッタの IdFromRowCol を使用して、配置先のペインIDを取得する。


問題4: リサイズ時にアプリケーションがクラッシュする

  • 原因
    OnSize() 内で RecalcLayout() を呼び出したが、スプリッタがまだ作成されていない。
  • 解決方法
    スプリッタ作成完了を示すフラグ変数を用意して、OnSize() 内でチェックする。


問題5: 別のペインにアクセスできない

  • 原因
    GetPane() の戻り値を適切なViewクラスにキャストしていない、または、MDIで子フレームを取得していない。
  • 解決方法
    static_cast で正しい型にキャストする。
    MDIの場合は、CMDIChildWnd 経由で子フレームを取得する。



主要メソッド一覧

下表に、CSplitterWnd クラスでよく使用されるメソッドを示す。

CSplitterWndクラスの主要メソッド
メソッド名 説明
Create 動的分割ウィンドウを作成する。
CreateStatic 静的分割ウィンドウを作成する。
CreateView 静的スプリッタの指定位置にビューを作成する。
SetRowInfo 行の理想サイズと最小幅を設定する。
SetColumnInfo 列の理想サイズと最小幅を設定する。
GetRowInfo 行の現在サイズと最小幅を取得する。
GetColumnInfo 列の現在サイズと最小幅を取得する。
GetPane 指定行 / 列のペイン (CWnd*) を取得する。
GetRowCount 現在の行数を取得する。
GetColumnCount 現在の列数を取得する。
IdFromRowCol 指定行 / 列の子ウィンドウIDを取得する。
RecalcLayout 行 / 列サイズ変更後にレイアウトを再計算する。
GetActivePane アクティブなペインを取得する。
SetActivePane 指定ペインをアクティブにする。



関連情報