「MFCコントロール - 分割ウインドウ」の版間の差分

提供: MochiuWiki : SUSE, EC, PCB

編集の要約なし
 
(3人の利用者による、間の13版が非表示)
1行目: 1行目:
== 概要 ==
== 概要 ==
分割ウインドウとは、ウインドウが2つ以上に分割されているウィンドウの事である。<br>
分割ウィンドウとは、ひとつのフレームウィンドウのクライアント領域を複数のペインに分割して表示するウィンドウ構成である。<br>
Visual C++ではそれぞれの分割された個々のウインドウを「ペイン」と呼ぶ。<br>
Microsoft Foundation Classes (MFC) では、<code>CSplitterWnd</code> クラスを使用してこの機能を実現できる。<br>
<br>
各ペインは通常、異なる <code>CView</code> 派生クラス、または 同じ <code>CView</code> 派生クラスのインスタンスを割り当てる。<br>
<br>
MFCの分割ウィンドウには、静的スプリッタと動的スプリッタの2種類が存在する。<br>
<br>
静的スプリッタは <code>CreateStatic()</code> を使用して、作成時に決めた行数と列数のペインを全て生成する。<br>
ユーザはペインのサイズを変更できるが、ペインの数や順序は変更できない。<br>
<br.
動的スプリッタは <code>Create()</code> を使用して、初期状態では1つのペインから始まる。<br>
ユーザがスプリッタボックスを操作すると、フレームワークが新しいペインを動的に作成・破棄する。<br>
<br>
静的スプリッタは異なるビュークラスを組み合わせて、複数の情報を同時に表示する用途に適している。<br>
動的スプリッタは、同じドキュメントを複数の視点やスクロール位置で確認する用途に適している。<br>
<br>
<code>CSplitterWnd</code> クラスは <code>CFrameWnd</code> または <code>CMDIChildWnd</code> の派生クラスに埋め込んで使用する。<br>
<br>
実装の中核は、フレームクラスの <code>OnCreateClient()</code> をオーバーライドして、その中で <code>CreateStatic()</code> または <code>Create()</code> を呼び出すことである。<br>
その後、静的スプリッタの場合は <code>CreateView()</code> を使用して、各ペインにビューを割り当てる必要がある。<br>
<br>
分割ウィンドウは、ドキュメント/ビューアーキテクチャと組み合わせることで、複雑なユーザインターフェースを比較的少ないコードで構築できる。<br>
<br>
<u>ただし、MFCは引き続きサポートされているが、新機能の追加やドキュメントの更新は行われていないことに注意すること。</u><br>
<br><br>
<br><br>


== 左右2分割の分割ウインドウの作成 ==
== 前提条件 ==
MFCでは、分割ウインドウを作成するAPIが標準で用意されている。それが、CSplitterWnd::CreateStatic関数である。<br>
分割ウィンドウを理解するためには、以下に示す知識と環境が必要である。<br>
下図のような1行2列の2ペインを持つ分割ウインドウのプログラムを記述する。<br>
<br>
* C++によるデスクトップ開発ワークロードを有効にする。
* ドキュメント / ビューアーキテクチャの基礎知識
*: <code>CWinApp</code>、<code>CDocument</code>、<code>CView</code>、<code>CFrameWnd</code> の関係を理解していること。
* C++とWindows APIの知識
*: ポインタ、参照、メッセージハンドラ、<code>CRuntimeClass</code> クラス等を理解していること。
* <code>DECLARE_DYNCREATE</code> / <code>IMPLEMENT_DYNCREATE</code> マクロの理解
*: 分割ウィンドウの各ペインに割り当てるViewクラスは、動的生成が可能である必要がある。
<br><br>
 
== 分割ウィンドウの種類 ==
MFCで提供される分割ウィンドウには、静的スプリッタと動的スプリッタの2種類がある。<br>
用途に応じて適切な方を選択すること。<br>
<br>
<center>
{| class="wikitable"
|+ 静的スプリッタと動的スプリッタの比較
! 項目 !! 静的スプリッタ (<code>CreateStatic</code>) !! 動的スプリッタ (<code>Create</code>)
|-
| ペイン数 || 固定 || ユーザ操作で増減
|-
| 最大分割数 || 16行 x 16列 || 2行 x 2列
|-
| ペインのクラス || 異なる <code>CView</code> 派生クラスを割り当て可能 || 基本的に同じ <code>CView</code> 派生クラス
|-
| スクロールバー || 共有スクロールバーを <code>WS_HSCROLL</code> / <code>WS_VSCROLL</code> で追加可能 || 動的に生成されるペインと連動
|-
| 使用例 || Windowsエクスプローラー<br>Visual Studioのグラフィックスエディタ || Microsoft Excel<br>Microsoft Word
|-
| 主な用途 || 異なる情報を同じウィンドウ内に同時表示 || 同じドキュメントを複数視点で確認
|}
</center>
<br>
静的スプリッタを使用する場合、全てのペインを <code>OnCreateClient()</code> 内で明示的に作成する必要がある。<br>
<br>
動的スプリッタを使用する場合、フレームワークがペインの作成と破棄を自動的に行う。<br>
<br><br>
 
== 基本的な実装手順 ==
分割ウィンドウを実装する時の流れを以下に示す。<br>
<br>
# フレームクラスに <code>CSplitterWnd</code> クラスのメンバ変数を追加する。
#: SDIアプリケーションの場合は、<code>CMainFrame</code> クラス、MDIアプリケーションの場合は、<code>CChildFrame</code> クラスに追加する。
# フレームクラスの <code>OnCreateClient()</code> をオーバーライドする。
#: クラスビューから <code>OnCreateClient()</code> を追加する、または、ヘッダファイルで手動で宣言する。
# <code>OnCreateClient()</code> 内で <code>CreateStatic()</code> または <code>Create()</code> を呼び出す。
#: 静的スプリッタの場合は <code>CreateStatic()</code>、動的スプリッタの場合は <code>Create()</code> を使用する。
# 静的スプリッタの場合、全てのペインに <code>CreateView()</code> でビューを割り当てる。
#: 各ペインに対応する <code>CView</code> 派生クラスを <code>RUNTIME_CLASS</code> マクロで指定する。
# 必要に応じて、<code>SetRowInfo()</code> / <code>SetColumnInfo()</code> でサイズを調整する。
#: リサイズ時には、<code>OnSize()</code> ハンドラで再調整することもある。
<br>
下図は、ドキュメント / ビューアーキテクチャにおける分割ウィンドウの位置づけを示している。<br>
<br>
[[ファイル:MFC Splitter Architecture.png|フレームなし|中央]]
<br><br>
 
== 静的分割ウィンドウ ==
静的分割ウィンドウは、<code>CreateStatic()</code> を使用して作成する。<br>
<br>
ユーザはペインの境界線をドラッグしてサイズを変更できるが、ペインの数は増減しない。<br>
<br>
==== 左右2分割の作成 ====
1行2列の分割ウィンドウを作成する例を示す。<br>
<br>
左側のペイン (Pane A) にはプロジェクト作成時に生成されたViewクラスを使用し、右側のペイン (Pane B) には新規に作成したViewクラスを使用する。<br>
左側がPaneA (CPaneAView)、右側がPaneB (CPaneBView) となっている。<br>
<br>
[[ファイル:MFC Splitter 2Pane.png|フレームなし|中央]]
<br>
[[ファイル:MFC Pane 01.jpg|フレームなし|中央]]
[[ファイル:MFC Pane 01.jpg|フレームなし|中央]]
<br>
<br>
まず、各ペインを管理するViewクラスを作成する。<br>
まず、右ペイン用のViewクラスを追加する。<br>
Viewクラスは、ペインの数だけ必要になるので、上図のように左右2つに分割すると、Viewクラスは全部で2つ必要になる。<br>
ソリューションエクスプローラーでプロジェクトを右クリックして、[追加] - [クラス] を選択する。<br>
ペインA(左ペイン)にはプロジェクト作成時で用意されているViewクラスを利用し、ペインB(右ペイン)のViewクラスは新規で作成する。<br>
<br>
* クラスの種類
*: MFCクラス
* クラス名
*: <u>CPaneBView</u>
* 基本クラス
*: <u>CFormView</u> または <u>CView</u>
<br>
<br>
プロジェクトに新規Viewクラスを追加する。<br>
次に、<u>CMainFrame.h</u> <code>CSplitterWnd</code> のメンバ変数を追加する。<br>
流れは以下の通りである。<br>
MDIアプリケーションの場合は <u>CChildFrame.h</u> に同様の変更を行う。<br>
[挿入(I)]->[クラスの新規作成(N)]<br>
  [クラスの種類(T)]   MFCクラスを選択<br>
  [クラス名(N)]     好きなクラス名(ここではCPaneBView)<br>
  [基本クラス(B)]    CViewの派生クラス(ここではCFormViewクラスを使用する)<br>
<br>
<br>
まず、CMainFrm.hファイルを開いて、以下のコードを入力する。<br>
  <syntaxhighlight lang="c++">
'''※MDIアプリケーションの場合は、CChildFrm.hファイルのCChildFrameクラスに同様のコードを追加する。'''<br>
  // CMainFrame.h
  <source lang="c++">
  CMainFrame.h
   
   
  class CMainFrame : public CFrameWndEx
  class CMainFrame : public CFrameWndEx
  {
  {
     // ...
     // ...
    public:
      CSplitterWndEx  m_SplitMain;  // スプリッタウインドウ
public:
    CSplitterWnd m_SplitMain;  // 分割ウィンドウ
     // ...
     // ...
};
</syntaxhighlight>
<br>
次に、<u>CMainFrame.cpp</u> の <code>OnCreateClient()</code> を以下に示すように実装する。<br>
<br>
<syntaxhighlight lang="c++">
// 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;
  }
  }
  </source>
  </syntaxhighlight>
<br>
<code>CreateStatic()</code> の第2引数は行数、第3引数は列数である。<br>
<br>
<br>
次に、CMainFrm.cppファイルのCMainFrameクラスにOnCreateClientメンバ関数をオーバーライドする。<br>
<code>CreateView()</code> の第1引数と第2引数は、それぞれ行番号と列番号を0始まりで指定する。<br>
手順は以下の通りである。<br>
第3引数には、ペインを管理する <code>CView</code> 派生クラスの <code>CRuntimeClass</code> クラスを <code>RUNTIME_CLASS</code> マクロで指定する。<br>
[表示(V)]->[ClassWizard(W)]<br>
第4引数の <code>CSize</code> クラスは、理想サイズを示す。<br>
  [プロジェクト(P)]  PaneSample<br>
第5引数の <code>pContext</code> は、親フレームから渡された作成コンテキストをそのまま渡す。<br>
  [クラス名(N)]    CMainFrame<br>
  [オブジェクトID(I)] CPaneBView<br>
  [メッセージ(G)]   OnCreateClient<br>
<br>
<br>
そして、CMainFrm.cppファイルに、以下のコードを追加する。<br>
==== ネストした3分割の作成 ====
  <source lang="c++">
<code>CreateStatic()</code> は、簡単な <u>n行 x m列</u> のグリッドしか直接作成できない。<br>
  CMainFrm.cpp
<br>
例えば、左側に1つ、右側に上下2つという変則的なレイアウトを作成する場合は、2つの <code>CSplitterWnd</code> クラスをネストする。<br>
具体的には、メインフレームを左右に分割した後、右側のペインをさらに上下に分割する。<br>
<br>
[[ファイル:MFC Splitter 3Pane Nested.png|フレームなし|中央]]
<br>
まず、Pane A、Pane B、Pane C向けのViewクラスをそれぞれ作成する。<br>
<br>
<u>CMainFrame.h</u> には、メインスプリッタとサブスプリッタの2つのメンバ変数を追加する。<br>
<br>
  <syntaxhighlight lang="c++">
  // CMainFrame.h
class CMainFrame : public CFrameWndEx
{
    // ...
   
   
  #include "CPaneDoc.h"   // CPaneAView.hよりも先にインクルード
  public:
  #include "CPaneAView.h"  // CPaneAViewクラスのヘッダをインクルード
    CSplitterWnd m_SplitMain; // メインフレームを左右分割
  #include  "CPaneBView.h"  // CPaneBViewクラスのヘッダをインクルード
    CSplitterWnd m_SplitSub;   // 右ペインを上下分割
    // ...
  };
  </syntaxhighlight>
<br>
次に、<u>CMainFrame.cpp</u> の <code>OnCreateClient()</code> を以下に示すように定義する。<br>
<br>
  <syntaxhighlight lang="c++">
  // CMainFrame.cpp
   
   
  // ...
  #include "CPaneDoc.h"
#include "CPaneAView.h"
#include "CPaneBView.h"
#include "CPaneCView.h"
   
   
  BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)  
  BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
  {
  {
     // ...
     // メインスプリッタを左右2分割
     if(FLASE == m_SplitMain.CreateStatic( // スプリッタウインドウの作成
     if (!m_SplitMain.CreateStatic(this, 1, 2))
                  this,                     // スプリッタウインドウの親ウィンドウを指定
    {
                                              // ここではメインフレームを分割するのでthisを指定
      TRACE0("Failed to create main splitter\n");
                  1,                         // 行の分割数を指定。ここでは縦分割は無いので1を指定
      return FALSE;
                  2)                         // 列の分割数を指定。ここでは左右分割なので2を指定
    }
    // サブスプリッタを右ペインに上下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;
       return FALSE;
     }
     }
   
   
     if(FALSE == m_SplitMain.CreateView(       // ペインA(ペイン場所1行1列目)を作成
    // ペインB (右側上部)
                  0,                         // スプリッタの行番号を指定
     if (!m_SplitSub.CreateView(0, 0, RUNTIME_CLASS(CPaneBView), CSize(320, 120), pContext))
                  0,                         // スプリッタの列番号を指定
                  RUNTIME_CLASS(CPaneAView), // 指定ペインを管理するViewクラスCPaneAViewを指定
                  CSize(20, 0),               // ペインのサイズ
                  pContext)
     {
     {
      TRACE0("Failed to create pane B\n");
       return FALSE;
       return FALSE;
     }
     }
   
   
     if(FALSE == m_SplitMain.CreateView(       // ペインB(ペイン場所1行2列目)を作成。
    // ペインC (右側下部)
                  0,                         // スプリッタの行番号を指定
     if (!m_SplitSub.CreateView(1, 0, RUNTIME_CLASS(CPaneCView), CSize(320, 120), pContext))
                  1,                          // スプリッタの列番号を指定
                  RUNTIME_CLASS(CPaneBView), // paneBを管理するViewクラスCPaneBViewを指定
                  CSize(0, 0),               // ペインのサイズ
                  pContext)
     {
     {
      TRACE0("Failed to create pane C\n");
       return FALSE;
       return FALSE;
     }
     }
    // ...
     return TRUE;
     return TRUE;
  }
  }
  </source>
  </syntaxhighlight>
<br>
サブスプリッタの親ウィンドウには、メインスプリッタのアドレス <u>&m_SplitMain</u> を指定する。<br>
第5引数の子ウィンドウIDには、<u>m_SplitMain.IdFromRowCol(0, 1)</u> を使用して、メインスプリッタの右ペインのIDを取得する。<br>
<br>
<br>
ここで注意するのが、CSplitterWndEx::CreateView関数の第3引数である。<br>
この指定が無いと、サブスプリッタが正しい領域に配置されない。<br>
第3引数において、指定ペインを管理するViewクラスを指定する。<br>
ペインAを管理するのはCPaneAViewなのでこれを指定する。同様に、ペインBを管理するのはCPaneBViewクラスなのでこれを指定する。<br>
'''※MDIアプリケーションの場合も同様に、CChildFrameクラスにOnCreateClient関数をオーバーライドして、'''<br>
''' OnCreateClient関数内に同様のコードを追加する。'''<br>
<br><br>
<br><br>


== 変則分割ペインを持つ分割ウインドウの作成 ==
== 動的分割ウィンドウ ==
下図のように、左側に1つ、右側に2つのウインドウを持つ分割ウインドウを作成する。<br>
動的分割ウィンドウは、<code>Create()</code> を使用して作成する。<br>
[[ファイル:MFC Pane 02.jpg|フレームなし|中央]]
<br>
<br>
CSplitterWnd::CreateStatic関数は、n行m列に分割するしか出来ないので、以下の方法を用いる。<br>
初期状態では1つのペインが表示され、ユーザがスプリッタボックスをドラッグするか、[ウィンドウ] - [分割]メニューを選択することにより、新しいペインが作成される。<br>
左右に2分割した後、分割した右側のペインを更に上下に2分割する。(このように2つのスプリッタが交錯するような構造をネストと呼ぶ)<br>
MFCアプリケーションウィザードで[分割ウィンドウ]オプションを有効にすると、雛形が自動生成される。<br>
<br>
<br>
まず、基本分割ウインドウと同様に、それぞれのペインを管理するViewクラスを作成する。<br>
<u>CMainFrame.h</u> にメンバ変数を追加する。<br>
ペインAの管理にはCPaneAViewクラス、ペインBはCPaneBView、ペインCはCPaneCViewとしてクラスを追加する。<br>
流れは以下の通りである。<br>
[挿入(I)]->[クラスの新規作成(N)]<br>
  [クラスの種類(T)]   MFCクラスを選択<br>
  [クラス名(N)]     CPaneBViewとCPaneCView<br>
  [基本クラス(B)]    CFormViewクラス(CViewの派生クラスならどれでもよい)<br>
<br>
<br>
次に、CMainFrm.hファイルに下記のソースコードを追加する。<br>
  <syntaxhighlight lang="c++">
  <source lang="c++">
  // CMainFrame.h
  CMainFrm.h
   
   
  class CMainFrame : public CFrameWnd
  class CMainFrame : public CFrameWndEx
  {
  {
     // ...
     // ...
    public:
      CSplitterWnd m_SplitMain;  // メインフレームを左右分割する為に使用する分割ウインドウ(メインスプリッタと仮命名)
  protected:
      CSplitterWnd m_SplitSub;   // 左右分割されたウインドウの右ペインを上下分割するために使用する分割ウインドウ(サブスプリッタと仮命名)
    CSplitterWnd m_wndSplitter;
     // ...
     // ...
  }
  };
  </source>
  </syntaxhighlight>
<br>
<br>
次に、CMainFrm.cppファイルにCMainFrame::OnCreateClient関数をオーバーライドして、下記のソースコードを追加する。<br>
<u>CMainFrame.cpp</u> <code>OnCreateClient()</code> を以下に示すように定義する。<br>
手順は以下の通りである。<br>
[表示(V)]->[ClassWizard(W)]<br>
  [プロジェクト(P)]  PaneTest<br>
  [クラス名(N)]    CMainFrame<br>
  [オブジェクトID(I)] CPaneBViewとCPaneCView<br>
  [メッセージ(G)]   OnCreateClient<br>
<br>
<br>
'''※MDIアプリケーションの場合は、CChildFrameクラスに追加すること。'''<br>
  <syntaxhighlight lang="c++">
  <source lang="c++">
  // CMainFrame.cpp
  CMainFrm.cpp
   
   
#include  "CPaneADoc.h"
  BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT /*lpcs*/, CCreateContext* pContext)
#include  "CPaneAView.h"  // CPaneAViewクラスのヘッダをインクルード
#include  "CPaneBView.h"  // CPaneBViewクラスのヘッダをインクルード
#include  "CPaneCView.h"  // CPaneCViewクラスのヘッダをインクルード
// ...
  BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)  
  {
  {
     // ...
     return m_wndSplitter.Create(
    m_SplitMain.CreateStatic(     //  メインスプリッタを作成
       this,
       this,                     //  メインスプリッタの親ウインドウを指定。ここではメインフレームを分割するのでthisを指定
       2, 2,              // 最大行数、最大列数
       1,                         //  メインスプリッタの行分割数を指定
       CSize(10, 10),    // 最小ペインサイズ
      2);                        //  メインスプリッタの列分割数を指定
 
    m_SplitSub.CreateStatic(      // サブスプリッタを作成
      &m_SplitMain,              //   サブスプリッタの親ウィンドウはm_SplitMain
      2,                        //  サブスプリッタの行分割を指定
      1,                        //  サブスプリッタの列分割を指定
      WS_CHILD | WS_VISIBLE,          //  サブスプリッタのスタイルを指定
      m_SplitMain.IdFromRowCol(0,1));  //  子ウィンドウIDを指定。m_SplitMainの行0(=1行目)列1(=2列目)のIDを取得
 
    m_SplitMain.CreateView(        //  ペインAを作成
      0,                          //  メインスプリッタの行番号を指定
      0,                          //  メインスプリッタの列番号を指定
      RUNTIME_CLASS(CPaneAView),  //  ペインAを管理するViewクラスCPaneAViewを指定。
      CSize(20, 0),              //  ペインのサイズ
      pContext);
 
    m_SplitSub(                    //  ペインBを作成
      0,                          //  サブスプリッタの行番号を指定
      0,                          //  サブンスプリッタの列番号を指定
      RUNTIME_CLASS(CPaneBView),  //  ペインBを管理するViewクラスCPaneBViewを指定。
       CSize(100, 0),             //  ペインのサイズ
      pContext);
 
     m_SplitSub(                    //  ペインCを作成。
      1,                          //  サブスプリッタの行番号を指定
      0,                          //  サブンスプリッタの列番号を指定
      RUNTIME_CLASS(CPaneCView),  //  ペインCを管理するViewクラスCPaneCViewを指定。
      CSize(0, 0),                // ペインのサイズ
       pContext);
       pContext);
    // ...
  }
  }
  </source>
  </syntaxhighlight>
<br>
動的スプリッタでは、<code>CreateView()</code> を明示的に呼び出す必要はない。<br>
フレームワークが必要に応じてビューを動的に生成するからである。<br>
<br>
ただし、動的スプリッタの全てのペインは、同じ <code>CView</code> 派生クラスを使用する。<br>
<br><br>
<br><br>


== 管理外Viewクラスからの各ペインへのアクセス方法(SDIの場合) ==
== ペイン間のアクセス ==
ここまで、分割ウインドウを作成して様々なペインを作成したが、各ペインをリンクさせながら動作させる方法を説明する。(ペインAで左クリックされたらペインBにテキストを出力する等)<br>
分割ウィンドウ内のあるペインから、別のペインの View オブジェクトにアクセスしたい場合がある。<br>
ここでは、上記で作成した変則分割ペインを使用する。<br>
例えば、左ペインで選択した項目に応じて、右ペインの内容を更新する場合である。<br>
<br>
<br>
各ペインに描画するには、各ペインを管理しているViewクラスの描画関数(OnDraw関数やOnPaint関数)を用いる。<br>
各ペインは <code>CSplitterWnd::GetPane</code> メンバ関数で取得できる。<br>
SDI アプリケーションの場合、<code>AfxGetMainWnd</code> を使ってメインフレームを取得し、フレームが保持するスプリッタを経由してアクセスする。<br>
<br>
<br>
CPaneAView.hファイルとCPaneAView.cppファイルに以下のソースコードを追加する。<br>
以下は、Pane A の <code>OnDraw</code> 内から Pane B および Pane C にアクセスする例である。<br>
  <source lang="c++">
ネストした3分割の例を前提としている。<br>
  CPaneAView.h
  <syntaxhighlight lang="c++">
  // CPaneAView.h
#include "CPaneBView.h"
#include "CPaneCView.h"
#include "MainFrm.h"
   
   
#include  "CPaneBView.h"
#include  "CPaneCView.h"
#include  "CMainFrm.h"
  class CPaneAView : public CFormView
  class CPaneAView : public CFormView
  {
  {
     // ...
     // ...
    public:
public:
      CPaneBView *pPaneB;  // CPaneBViewクラスへのポインタ
    CPaneBView* pPaneB;
      CPaneCView *pPaneC// CPaneCViewクラスへのポインタ
    CPaneCView* pPaneC;
    // ...
};
  </syntaxhighlight>
<syntaxhighlight lang="c++">
// 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();
    }
     // ...
     // ...
  }
  }
  </source>
  </syntaxhighlight>
  <source lang="c++">  
<br>
  CPaneAView.cpp
<code>GetPane</code> の戻り値は <code>CWnd*</code> なので、使用する際は適切な View クラスにキャストする。<br>
MDI アプリケーションの場合は、<code>AfxGetMainWnd</code> ではなく子フレームへのポインタを取得する必要がある。<br>
<br><br>
== スプリッターバーの固定化 ==
ユーザがスプリッターバーをドラッグしてペインサイズを変更できないようにする方法を説明する。<br>
最も確実な方法は、<code>CSplitterWnd</code> の派生クラスを作成し、<code>HitTest</code> メンバ関数をオーバーライドすることである。<br>
<code>HitTest</code> は、指定された座標がスプリッタのどの部分に該当するかを示す整数値を返す。<br>
戻り値が 0 (<code>noHit</code>) の場合、フレームワークはその位置での操作を無視する。<br>
そのため、特定の方向のスプリッターバーに対して 0 を返すことで、その方向の移動を禁止できる。<br>
<br>
まず、<code>CSplitterWnd</code> の派生クラスを作成する。<br>
<syntaxhighlight lang="c++">
// CMySplitterWnd.h
#pragma once
#include <afxext.h>
class CMySplitterWnd : public CSplitterWnd
{
    DECLARE_DYNAMIC(CMySplitterWnd)
public:
    CMySplitterWnd();
    virtual ~CMySplitterWnd();
    virtual int HitTest(CPoint pt) const;
};
</syntaxhighlight>
<syntaxhighlight lang="c++">
// 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;
}
</syntaxhighlight>
<br>
上記の例では、縦方向のスプリッターバーと交差点での操作を無効化している。<br>
横方向のスプリッターバーだけ有効にしたい場合は、縦方向と交差点のみを 0 にすればよい。<br>
<code>noHit</code>、<code>hSplitterBar1</code>、<code>splitterIntersection1</code> 等の値は <code>CSplitterWnd</code> の実装内で定義されており、必要に応じて独自に定義する。<br>
<br><br>
 
== ウィンドウリサイズ対応 ==
フレームウィンドウがリサイズされたときに、ペインのサイズを一定の比率で保ちたい場合がある。<br>
<br>
その場合、<code>CFrameWnd::OnSize()</code> をオーバーライドして、<code>SetRowInfo()</code> / <code>SetColumnInfo()</code> / <code>RecalcLayout()</code> を呼び出す。<br>
<br>
以下の例では、左右2分割のスプリッタで、右半分を2等分し、左半分も同じ幅に調整している。<br>
<br>
  <syntaxhighlight lang="c++">
  // CMainFrame.cpp
   
   
  #include "CMainFrm.h"
  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();
    }
}
</syntaxhighlight>
<br>
上記の例では、<u>m_bSplitterCreated</u> フラグでスプリッタの作成完了を確認している。<br>
このフラグは <code>OnCreateClient()</code> でスプリッタ作成成功後に <u>TRUE</u> に設定する。<br>
<br>
<code>OnSize()</code> はフレーム作成時にスプリッタがまだ存在しない段階でも呼ばれる可能性があるため、このチェックは必須である。<br>
<br>
ネストしたスプリッタの場合は、メインスプリッタとサブスプリッタの両方で <code>RecalcLayout()</code> を呼び出す必要がある。<br>
<br>
<syntaxhighlight lang="c++">
// ネストしたスプリッタの例
   
   
  CPaneAView::OnDraw(CDC *pDC)
  void CMainFrame::OnSize(UINT nType, int cx, int cy)
  {
  {
     // ...
     CFrameWndEx::OnSize(nType, cx, cy);
     pPaneB = (CPaneBView*)((CMainFrame*)AfxGetMainWnd())->m_SplitSub.GetPane(0, 0);
    pPaneC = (CPaneCView*)((CMainFrame*)AfxGetMainWnd())->m_SplitSub.GetPane(1, 0);
     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();
     }
  }
  }
  </source>
  </syntaxhighlight>
AfxGetMainWnd関数はメインフレームのオブジェクトを返すので、CPaneAtViewクラスの任意の場所でペインBやペインCにアクセス出来る。<br>
<br><br>
また、各ペインのドキュメントには、CWnd::GetDC関数でアクセスできる。例えば、ペインBに"This region is PaneB."と出力する場合は、下記のように記述する。<br>
 
<source lang="c++">
== トラブルシューティング ==
pPaneB->GetDC()->TextOut(0, 0, _T("This region is PaneB."));
==== 問題1: ウィンドウが正しく表示されない ====
</source>
* 原因
*: 静的スプリッタでは、<code>OnCreateClient()</code> が戻る前に全てのペインを <code>CreateView</code> で作成していない。
* 解決方法
*: <code>CreateStatic()</code> 呼び出し後、全ての行 / 列に対して <code>CreateView()</code> を呼び出す。
<br>
==== 問題2: 分割線がドラッグできない ====
* 原因
*: <code>CreateStatic()</code> の第4引数で <code>WS_VISIBLE</code> を指定していない、または、ペインの最小幅 / 高さが大きすぎる。
* 解決方法
*: スタイルに <code>WS_CHILD | WS_VISIBLE</code> を含める。
*: 必要に応じて <code>SetRowInfo()</code> / <code>SetColumnInfo()</code> の最小幅を小さくする。
<br>
==== 問題3: サブスプリッタが正しい領域に表示されない ====
* 原因
*: ネストしたスプリッタの子ウィンドウIDが正しく指定されていない。
* 解決方法
*: 親スプリッタの <code>IdFromRowCol</code> を使用して、配置先のペインIDを取得する。
<br>
==== 問題4: リサイズ時にアプリケーションがクラッシュする ====
* 原因
*: <code>OnSize()</code> 内で <code>RecalcLayout()</code> を呼び出したが、スプリッタがまだ作成されていない。
* 解決方法
*: スプリッタ作成完了を示すフラグ変数を用意して、<code>OnSize()</code> 内でチェックする。
<br>
==== 問題5: 別のペインにアクセスできない ====
* 原因
*: <code>GetPane()</code> の戻り値を適切なViewクラスにキャストしていない、または、MDIで子フレームを取得していない。
* 解決方法
*: <code>static_cast</code> で正しい型にキャストする。
*: MDIの場合は、<code>CMDIChildWnd</code> 経由で子フレームを取得する。
<br><br>
<br><br>
== 主要メソッド一覧 ==
下表に、<code>CSplitterWnd</code> クラスでよく使用されるメソッドを示す。<br>
<br>
<center>
{| class="wikitable"
|+ CSplitterWndクラスの主要メソッド
! メソッド名 !! 説明
|-
| Create || 動的分割ウィンドウを作成する。
|-
| CreateStatic || 静的分割ウィンドウを作成する。
|-
| CreateView || 静的スプリッタの指定位置にビューを作成する。
|-
| SetRowInfo || 行の理想サイズと最小幅を設定する。
|-
| SetColumnInfo || 列の理想サイズと最小幅を設定する。
|-
| GetRowInfo || 行の現在サイズと最小幅を取得する。
|-
| GetColumnInfo || 列の現在サイズと最小幅を取得する。
|-
| GetPane || 指定行 / 列のペイン (CWnd*) を取得する。
|-
| GetRowCount || 現在の行数を取得する。
|-
| GetColumnCount || 現在の列数を取得する。
|-
| IdFromRowCol || 指定行 / 列の子ウィンドウIDを取得する。
|-
| RecalcLayout || 行 / 列サイズ変更後にレイアウトを再計算する。
|-
| GetActivePane || アクティブなペインを取得する。
|-
| SetActivePane || 指定ペインをアクティブにする。
|}
</center>
<br><br>
== 関連情報 ==
* [https://learn.microsoft.com/ja-jp/cpp/mfc/reference/csplitterwnd-class Microsoft Learn - CSplitterWndクラス]
* [https://learn.microsoft.com/en-us/cpp/mfc/tn029-splitter-windows Microsoft Learn - TN029: Splitter Windows]
* [https://github.com/microsoft/VCSamples/tree/master/VC2010Samples/MFC/general/viewex Microsoft VCSamples - VIEWEX]
<br><br>
{{#seo:
|title={{PAGENAME}} : Exploring Electronics and SUSE Linux | MochiuWiki
|keywords=MochiuWiki,Mochiu,Wiki,Mochiu Wiki,Electric Circuit,Electric,pcb,Mathematics,AVR,TI,STMicro,AVR,ATmega,MSP430,STM,Arduino,Xilinx,FPGA,Verilog,HDL,PinePhone,Pine Phone,Raspberry,Raspberry Pi,C,C++,C#,Qt,Qml,MFC,Shell,Bash,Zsh,Fish,SUSE,SLE,Suse Enterprise,Suse Linux,openSUSE,open SUSE,Leap,Linux,uCLnux,電気回路,電子回路,基板,プリント基板
|description={{PAGENAME}} - MFCアプリケーションの分割ウィンドウ (CSplitterWnd) に関する情報 | This page is {{PAGENAME}} in our wiki about electronic circuits and SUSE Linux
|image=/resources/assets/MochiuLogo_Single_Blue.png
}}


__FORCETOC__
__FORCETOC__
[[カテゴリ:MFC]]
[[カテゴリ:MFC]]

2026年6月16日 (火) 17:23時点における最新版

概要

分割ウィンドウとは、ひとつのフレームウィンドウのクライアント領域を複数のペインに分割して表示するウィンドウ構成である。
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 指定ペインをアクティブにする。



関連情報