MFCコントロール - 分割ウインドウ

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

分割ウインドウとは、ウインドウが2つ以上に分割されているウィンドウの事である。
Visual C++ではそれぞれの分割された個々のウインドウを「ペイン」と呼ぶ。


左右2分割の分割ウインドウの作成

MFCでは、分割ウインドウを作成するAPIが標準で用意されている。それが、CSplitterWnd::CreateStatic関数である。
下図のような1行2列の2ペインを持つ分割ウインドウのプログラムを記述する。

MFC Pane 01.jpg


まず、各ペインを管理するViewクラスを作成する。
Viewクラスは、ペインの数だけ必要になるので、上図のように左右2つに分割すると、Viewクラスは全部で2つ必要になる。
ペインA(左ペイン)にはプロジェクト作成時で用意されているViewクラスを利用し、ペインB(右ペイン)のViewクラスは新規で作成する。

プロジェクトに新規Viewクラスを追加する。
流れは以下の通りである。
[挿入(I)]->[クラスの新規作成(N)]
  [クラスの種類(T)]   MFCクラスを選択
  [クラス名(N)]     好きなクラス名(ここではCPaneBView)
  [基本クラス(B)]    CViewの派生クラス(ここではCFormViewクラスを使用する)

まず、CMainFrm.hファイルを開いて、以下のコードを入力する。
※MDIアプリケーションの場合は、CChildFrm.hファイルのCChildFrameクラスに同様のコードを追加する。

 CMainFrame.h
 
 class CMainFrame : public CFrameWndEx
 {
    // ...
    public:
       CSplitterWndEx  m_SplitMain;  // スプリッタウインドウ
    // ...
 }


次に、CMainFrm.cppファイルのCMainFrameクラスにOnCreateClientメンバ関数をオーバーライドする。
手順は以下の通りである。
[表示(V)]->[ClassWizard(W)]
  [プロジェクト(P)]  PaneSample
  [クラス名(N)]    CMainFrame
  [オブジェクトID(I)] CPaneBView
  [メッセージ(G)]   OnCreateClient

そして、CMainFrm.cppファイルに、以下のコードを追加する。

 CMainFrm.cpp
 
 #include  "CPaneDoc.h"   // CPaneAView.hよりも先にインクルード
 #include  "CPaneAView.h"  // CPaneAViewクラスのヘッダをインクルード
 #include  "CPaneBView.h"  // CPaneBViewクラスのヘッダをインクルード
 
 // ...
 
 BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) 
 {
    // ...
    if(FLASE == m_SplitMain.CreateStatic(  // スプリッタウインドウの作成
                   this,                      // スプリッタウインドウの親ウィンドウを指定
                                              // ここではメインフレームを分割するのでthisを指定
                   1,                         // 行の分割数を指定。ここでは縦分割は無いので1を指定
                   2)                         // 列の分割数を指定。ここでは左右分割なので2を指定
    {
       return FALSE;
    }
 
    if(FALSE == m_SplitMain.CreateView(        // ペインA(ペイン場所1行1列目)を作成
                   0,                          // スプリッタの行番号を指定
                   0,                          // スプリッタの列番号を指定
                   RUNTIME_CLASS(CPaneAView),  // 指定ペインを管理するViewクラスCPaneAViewを指定
                   CSize(20, 0),               // ペインのサイズ
                   pContext)
    {
       return FALSE;
    }
 
    if(FALSE == m_SplitMain.CreateView(        // ペインB(ペイン場所1行2列目)を作成。
                   0,                          // スプリッタの行番号を指定
                   1,                          // スプリッタの列番号を指定
                   RUNTIME_CLASS(CPaneBView),  // paneBを管理するViewクラスCPaneBViewを指定
                   CSize(0, 0),                // ペインのサイズ
                   pContext)
    {
       return FALSE;
    }
    // ...
    //return CFrameWndEx::OnCreateClient(lpcs, pContext);
    return TRUE;
 }


ここで注意するのが、CSplitterWndEx::CreateView関数の第3引数である。
第3引数において、指定ペインを管理するViewクラスを指定する。
ペインAを管理するのはCPaneAViewなのでこれを指定する。同様に、ペインBを管理するのはCPaneBViewクラスなのでこれを指定する。
※MDIアプリケーションの場合も同様に、CChildFrameクラスにOnCreateClient関数をオーバーライドして、
 OnCreateClient関数内に同様のコードを追加する。


変則分割ペインを持つ分割ウインドウの作成

下図のように、左側に1つ、右側に2つのウインドウを持つ分割ウインドウを作成する。

MFC Pane 02.jpg


CSplitterWnd::CreateStatic関数は、n行m列に分割するしか出来ないので、以下の方法を用いる。
左右に2分割した後、分割した右側のペインを更に上下に2分割する。(このように2つのスプリッタが交錯するような構造をネストと呼ぶ)

まず、基本分割ウインドウと同様に、それぞれのペインを管理するViewクラスを作成する。
ペインAの管理にはCPaneAViewクラス、ペインBはCPaneBView、ペインCはCPaneCViewとしてクラスを追加する。
流れは以下の通りである。
[挿入(I)]->[クラスの新規作成(N)]
  [クラスの種類(T)]   MFCクラスを選択
  [クラス名(N)]     CPaneBViewとCPaneCView
  [基本クラス(B)]    CViewの派生クラス(ここではCFormViewクラスを使用する)

次に、CMainFrm.hファイルに下記のソースコードを追加する。

 CMainFrm.h
 
 class CMainFrame : public CFrameWndEx
 {
    // ...
    public:
       CSplitterWnd  m_SplitMain;  // メインフレームを左右分割する為に使用する分割ウインドウ(メインスプリッタと命名)
       CSplitterWnd  m_SplitSub;   // 左右分割されたウインドウの右ペインを上下分割するために使用する分割ウインドウ(サブスプリッタと命名)
    // ...
 }


次に、CMainFrm.cppファイルにCMainFrame::OnCreateClient関数をオーバーライドして、下記のソースコードを追加する。
手順は以下の通りである。
[表示(V)]->[ClassWizard(W)]
  [プロジェクト(P)]  PaneSample
  [クラス名(N)]    CMainFrame
  [オブジェクトID(I)] CPaneBViewとCPaneCView
  [メッセージ(G)]   OnCreateClient

※MDIアプリケーションの場合は、CChildFrameクラスに追加すること。

 CMainFrm.cpp
 
 #include  "CPaneDoc.h"
 #include  "CPaneAView.h"  // CPaneAViewクラスのヘッダをインクルード
 #include  "CPaneBView.h"  // CPaneBViewクラスのヘッダをインクルード
 #include  "CPaneCView.h"  // CPaneCViewクラスのヘッダをインクルード
 
 // ...
 
 BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) 
 {
    // ...
    if(FALSE == m_SplitMain.CreateStatic(     //  メインスプリッタを作成
                   this,                      //  メインスプリッタの親ウインドウを指定。ここではメインフレームを分割するのでthisを指定
                   1,                         //  メインスプリッタの行分割数を指定
                   2))                        //  メインスプリッタの列分割数を指定
    {
       return FALSE;
    }
 
    if(FALSE == 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を取得
    {
       return FALSE;
    }
 
    if(FALSE == m_SplitMain.CreateView(        //  ペインAを作成
                   0,                          //  メインスプリッタの行番号を指定
                   0,                          //  メインスプリッタの列番号を指定
                   RUNTIME_CLASS(CPaneAView),  //  ペインAを管理するViewクラスCPaneAViewを指定。
                   CSize(320, 240),            //  ペインのサイズ
                   pContext))
    {
       return FALSE;
    }
 
    if(FALSE == m_SplitSub(                    //  ペインBを作成
                   0,                          //  サブスプリッタの行番号を指定
                   0,                          //  サブンスプリッタの列番号を指定
                   RUNTIME_CLASS(CPaneBView),  //  ペインBを管理するViewクラスCPaneBViewを指定。
                   CSize(320, 120),            //  ペインのサイズ
                   pContext))
    {
       return FALSE;
    }
 
    if(FALSE == m_SplitSub(                     //  ペインCを作成。
                   1,                           //  サブスプリッタの行番号を指定
                   0,                           //  サブスプリッタの列番号を指定
                   RUNTIME_CLASS(CPaneCView),   //  ペインCを管理するViewクラスCPaneCViewを指定。
                   CSize(320, 120),             //  ペインのサイズ
                   pContext))
    {
       return FALSE;
    }
    // ...
    //return CFrameWndEx::OnCreateClient(lpcs, pContext);
    return TRUE;
 }



管理外Viewクラスからの各ペインへのアクセス方法(SDIの場合)

ここまで、分割ウインドウを作成して様々なペインを作成したが、各ペインをリンクさせながら動作させる方法を説明する。
(ペインAで左クリックされたらペインBにテキストを出力する等)
ここでは、上記で作成した変則分割ペインを使用する。

各ペインに描画するには、各ペインを管理しているViewクラスの描画関数(OnDraw関数やOnPaint関数)を用いる。

CPaneAView.hファイルとCPaneAView.cppファイルに以下のソースコードを追加する。

 CPaneAView.h
 
 #include  "CPaneBView.h"
 #include  "CPaneCView.h"
 #include  "CMainFrm.h"

 class CPaneAView : public CFormView
 {
    // ...
    public:
       CPaneBView *pPaneB;  // CPaneBViewクラスへのポインタ
       CPaneCView *pPaneC;  // CPaneCViewクラスへのポインタ
    // ...
 }
 
 CPaneAView.cpp
 
 #include "CMainFrm.h"
 
 // ...
 
 CPaneAView::OnDraw(CDC *pDC)
 {
    // ...
    pPaneB = (CPaneBView*)((CMainFrame*)AfxGetMainWnd())->m_SplitSub.GetPane(0, 0);
    pPaneC = (CPaneCView*)((CMainFrame*)AfxGetMainWnd())->m_SplitSub.GetPane(1, 0);
    // ...
 }

AfxGetMainWnd関数はメインフレームのオブジェクトを返すので、CPaneAtViewクラスの任意の場所でペインBやペインCにアクセス出来る。
また、各ペインのドキュメントには、CWnd::GetDC関数でアクセスできる。
例えば、ペインBに"This region is PaneB."と出力する場合は、下記のように記述する。

 CPaneAView.cpp
 
 pPaneB->GetDC()->TextOut(0, 0, _T("This region is PaneB."));



分割ウインドウの固定化

分割ウインドウを手動で変更できないようにする方法がある。(分割を固定にする)

まず、CSplitterWndExの派生クラスを作成して、OnLButtonDownハンドラをオーバーライドして
CSplitterWndEx::OnLButtonDown関数を呼ばないようにすれば可能である。
また、スプリッターバー上でマウスカーソルを変更しないようにするには、OnMouseMoveハンドラをオーバーライドして
CSplitterWndEx::OnMouseMove関数を呼ばないようにすれば可能である。

上記よりも簡単な方法がある。
それは、CSplitterWndExの派生クラスを作成して、OnNcHitTestハンドラをオーバーライドして
CSplitterWndEx::OnNcHitTest関数で無条件にHTBORDERを返せば、マウスカーソルも変更されない。


分割ウインドウにて、仕切り位置を固定し、ユーザーに位置変更をさせない方法がある。
また、カーソルが乗っても移動用のカーソルに変更しないようにする。

まず、CSpritterWndまたはCSpritterWndExの派生クラスを作成する。(ここではCMySplitterWndEx)
CMySplitterWndExにてHitTestハンドラをオーバーライドする。
HitTestハンドラは、下記のように宣言されている。

virtual int HitTest(CPoint pt) const;


HitTestの戻り値は下記の通りで、操作禁止にしたい方向のときに"0"を返すようにする。

 enum HitTestValue
 {
    noHit                   = 0,
    vSplitterBox            = 1,
    hSplitterBox            = 2,
    bothSplitterBox         = 3,          // just for keyboard
    vSplitterBar1           = 101,        // 横方向の上下に動かすバー
    vSplitterBar15          = 115,
    hSplitterBar1           = 201,        // 縦方向の左右に動かすバー
    hSplitterBar15          = 215,
    splitterIntersection1   = 301,        // 縦と横が交わる点
    splitterIntersection225 = 525
 };
 int CMySplitterWndEx::HitTest(CPoint pt) const
 {
    int ht = CSplitterWndEx::HitTest(pt);  // CSpritterWndExは、テーマの変更処理が追加されただけなのでCSplitterWndでも可
 
    if(ht == 201 || ht == 301)
    {  // 横方向のバーだけ上下に移動可
       ht = 0;  // ヒットしてないことにする
    }
 
    return ht;
 }

上記の例では、横方向のバーだけ上下に移動出来るようになり、縦方向や交差している点ではカーソルも変更されない。
enumのHitTestValueはcppファイル内で宣言されているため、使用する場合は各自で追加する。


リサイズ変更

SetRowInfo and SetColumnInfo are responsible for managing the size of the splitters. Add a handler for the WM_ONSIZE message and add the following code:

Hide Copy Code
void CMainFrame::OnSize(UINT nType, int cx, int cy) {

   CFrameWnd::OnSize(nType, cx, cy);
   CRect cr;
   GetWindowRect(&cr);
   if (  m_bInitSplitter && nType != SIZE_MINIMIZED )
   {
       m_mainSplitter.SetRowInfo( 0, cy, 0 );
       m_mainSplitter.SetColumnInfo( 0, cr.Width() / 2, 50);
       m_mainSplitter.SetColumnInfo( 1, cr.Width() / 2, 50);
       m_mainSplitter.RecalcLayout();
   }

}
In the above code, we first check that the splitters have been initialized by checking the boolean value. This check is required since I think a WM_SIZE message is passed to the frame before the create method is ran - therefore, the object won't exist the first time this code runs and would crash if you didn't check for its existence.

The application should now run and provide something resembling the screenshot at the start of the article! I will probably add a second article in this series to add further nested splitters and other views based on forms! Stay tuned...