スプリッタウィンドウの生成法及び、各ペインへのアクセス方法

 作業環境:Windows2000, ++ 6.0, MFC AppWizard, MDIor SDI)アプリケーション
 作成プロジェクト名:PaneTest

 これらのプログラムは私が作成して使用しているプログラムですが、実際にお客様方がこれらをコンパイルや実行した際に何らかの悪影響が生じたとしても、管理人・九本麻有巣は一切の責任を負いかねます。御利用は、各人の責任を以ってお願い致します。

 まず始めに。皆さん、スプリットウィンドウと言うものを御存知でしょうか?もしかしたら分割ウィンドウと言う名前ででしたら御存知かもしれません。また、御存じない方の為に説明しますと、ウィンドウが二つないし三つ以上に分割されているウィンドウの事です。例えば、下のような感じにですね。
スプリッタウィンドウの一例(Internet Explorer)  右図は最も簡単なスプリッタウィンドウ。左右に1:1に別けたウィンドウです。左に『お気に入り』フォルダのツリー表示、右にブラウザ画面と言う構成ですね。ネットで検索していても、こう言ったウィンドウを見た事ありますよね?例えば、画面左にリンクページを、右に左で指定したリンクを表示するページを開くようなウィンドウです。ページによっては更に画面下に別のウィンドウを作って三画面分割しているような所もございますね。HTMLではこう言った分割ウィンドウを「フレーム」と呼び、<FRAME>タグとして用意されています。また、VCではそれぞれの分割された個々のウィンドウを『ペイン』と呼ぶ事に注意して下さい。

最も簡単な左右2分割の簡単スプリッタウィンドウを作成
 さてさて、では講義に入りましょう。
 まず1つ質問。実際にアプリケーションを作成する時に、ただ単一のウィンドウにテキストやイメージを張り付けるだけのアプリケーションで満足ですか?左の画面に拡大画像を置いてCGの微調整を行ない、右の画面で全体図を載せるようなペイントソフトを作りたいとか想いませんか?――そうやって思えるだけの技術を持つ方が、こんなページを見ているとも思いませんが――
 Windowsではこう言った作業に適した分割ウィンドウを作成するAPI関数がデフォルトで用意されています。それがCSplitterWnd::CreateStatic関数です。
1x2の2ペインを持つスプリッタウィンドウ  では実際にウィンドウを分割するプログラムを組んでみましょう。作成するスプリッタウィンドウは、右図のような左右分割スプリッタです。
 まず準備段階として、それぞれのペインを管理するViewクラスを作成します。管理Viewクラスは、ペインの数だけ必要になりますから、仮にウィンドウを左図のように左右二つに分割するとしますと、Viewクラスは全部で二つ必要になります。paneAにはプロジェクト作成時点で用意されているViewクラス(今回の場合は、CPaneTestViewクラスですね)を利用するとして、paneBに利用するViewクラスは自分で作らなければなりません。
 ってなワケで、早速プロジェクトに新規ビュークラスを追加しましょう。流れは以下の通りです。
[挿入(I)]->[クラスの新規作成(N)]
  [クラスの種類(T)]   MFCクラスを選択
  [クラス名(N)]     好きなクラス名(ここではCPaneBView)
  [基本クラス(B)]    CViewクラス若しくはその派生クラス
 以上でViewクラスの追加作業は終了です。それでは、実際にプログラミングに移りましょう。
 まず、<MainFrm.h>ヘッダファイルを開いて、以下のコードを打って下さい。


class  CMainFrame : public CFrameWnd
{
    :
public:
  CSplitterWnd    m_SplitWndMain;     // スプリッタウィンドウ。メンバ関数定義
    :
}

 因みに、MDIアプリケーションの場合は、<CChildFrm.h>ヘッダファイルのCChildFrameクラスに同様のコードを追加して下さい。
 次いで、<MainFrm.cpp>ソースファイル内のCMainFrameクラスにOnCreateClientメンバ関数をオーバーライドします。手順は、
[表示(V)]->[ClassWizard(W) Ctrl+W]
  [プロジェクト(P)]  PaneTest
  [クラス名(N)]    CMainFrame
  [オブジェクトID(I)] CPaneBView
  [メッセージ(G)]   OnCreateClient
 そして、<MainFrm.cpp>ソースファイルに、以下のコードを追加。

#include  "PaneTestDoc.h"    // "PaneTestView.h"ヘッダよりも先にインクルードする。理由は不明だが、順序を逆するとコンパイルエラーが発生する。
#include  "PaneTestView.h"   // CPaneTestViewクラスのヘッダをインクルード
#include  "PaneBView.h"      // CPaneBViewクラスのヘッダをインクルード

    :
    :

BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) 
{
    :
    :
  m_SplitWndMain.CreateStatic(  // スプリッタウィンドウの作成
      this,                     //   スプリッタウィンドウの親ウィンドウを指定。ここではメインフレームを分割するのでthisを指定。
      1,                        //   行の分割数を指定。ここでは縦分割は無いので1を指定
      2);                       //   列の分割数を指定。ここでは左右に二つ分割なので2を指定
  m_SplitWndMain.CreateView(          // paneA(ペイン場所1x1)を作成。
      0,                              //   スプリッタの行番号を指定
      0,                              //   スプリッタの列番号を指定
      RUNTIME_CLASS(CPaneTestView),   //   指定ペインを管理するViewクラスCPaneTestViewを指定
      CSize(20,0),                    //   ペインのサイズ
      pContext);                      //   ???。私の腐った脳味噌では何か理解出来ません
  m_SplitWndMain.CreateView(       // paneB(ペイン場所1x2)を作成。
      0,                           //   スプリッタの行番号を指定
      1,                           //   スプリッタの列番号を指定
      RUNTIME_CLASS(CPaneBView),   //   paneBを管理するViewクラスCPaneBViewを指定
      CSize(0,0),                  //   ペインのサイズ
      pContext);                   //   ???
    :
    :
}

 ここで注意して欲しいのは、CSplitterWnd::CreateViewの第1,2引数、及び第3引数。
 先ず、第1,2引数。ここでペインを作る箇所を指定。例えば、paneAは1行1列目の場所に作成するので、1x1のペインを指定。但し、数値は0から始まるので、0,0を指定。paneBは同様に0,1を指定。もしも3行5列目のペインを指定するなら、2,4を指定する事になる。
 そして、第3引数。ここは、指定ペインを管理するViewクラスを指定します。paneAを管理するのはCPaneTestViewですのでこれを指定。同様にpaneBを管理するのはCPaneBViewクラスなので、CPaneBViewを指定しました。RUNTIME_CLASS()については、私は詳細は知りません。知りたい方は、独自に調べて下さい(そして、理解した事を私にメールしてくれればHAPPYになれます(笑))。
2x3の6ペインを持つスプリッタウィンドウ  尚、MDIアプリケーションの場合も同様です。CChildFrameクラスに、SDIアプリケーションの時同様、OnCreateClientメンバ関数をオーバーライドして、OnCreateClientメンバ関数内に同様のコードを追加して下さい。

6ペインのスプリッタウィンドウを作成
 これでスプリッタウィンドウの作成は終わりです。もしも望むならば、2x3の6ペインを持つ右図のようなスプリッタウィンドウも作成可能です。
 作り方は、1x2の時同様、必要な分だけViewクラスを追加します。例えば、CPaneBView, CPaneCView, CPaneDView, CPaneEView, CPaneFViewの五つのViewクラスを追加作成します(手順は上記参照)。paneAの管理にはCPaneTestViewを使用する事も同じです。
 そして、後も1x2と同様にコードを追加します。勿論、CMainFrame(MDIアプリケーションならばCChildFrame)のOnCrateClientメンバ関数内に、です。


#include  "PaneTestDoc.h"
#include  "PaneTestView.h"   // CPaneTestViewクラスのヘッダをインクルード
#include  "PaneBView.h"      // CPaneBViewクラスのヘッダをインクルード
#include  "PaneCView.h"      // CPaneCViewクラスのヘッダをインクルード
#include  "PaneDView.h"      // CPaneDViewクラスのヘッダをインクルード
#include  "PaneEView.h"      // CPaneEViewクラスのヘッダをインクルード
#include  "PaneFView.h"      // CPaneFViewクラスのヘッダをインクルード

    :
    :

BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) 
{
    :
    :
  m_SplitWndMain.CreateStatic(this,2,3);         //  2x3のスプリッタウィンドウの作成
  m_SplitWndMain.CreateView(0,0,RUNTIME_CLASS(CPaneTestView), CSize(100,0), pContext);
  m_SplitWndMain.CreateView(0,1,RUNTIME_CLASS(CPaneBView), CSize(150,70), pContext);
  m_SplitWndMain.CreateView(0,2,RUNTIME_CLASS(CPaneCView), CSize(0,0), pContext);
  m_SplitWndMain.CreateView(1,0,RUNTIME_CLASS(CPaneDView), CSize(0,0), pContext);
  m_SplitWndMain.CreateView(1,1,RUNTIME_CLASS(CPaneEView), CSize(0,0), pContext);
  m_SplitWndMain.CreateView(1,2,RUNTIME_CLASS(CPaneFView), CSize(0,0), pContext);
    :
    :
}

 以上です。簡単でしょ?

変則分割ペインを持つスプリッタウィンドウを作成
変則3ペインを持つスプリッタウィンドウ  では次。変則的なスプリッタウィンドウを作ってみましょう。右図のように、左側に1つ、右側に2つのウィンドウを持つようなスプリッタウィンドウを例にあげてみます。
 これを作ろうと思うと、上述の方法だけでは無理です。何故ならば、CSplitterWnd::CreateStatic関数は、n×mにウィンドウをぶった切る事しか出来ないからです。試しに挑戦してみて下さい。第1,2引数をどう弄くっても変則3ペインを持ったスプリッタウィンドウを作れないでしょう?
 では、どうすれば右図のような変則3ペインスプリッタウィンドウを作成できるのでしょうか?答えは、言ってしまえば簡単な事。まず始めに左右に2分割して、分割した右側のペインを更に上下に2分割するだけなんですね、これが。因みに、こう言う風に2つのスプリッタが交錯するような作りを、ネストと呼びます。『巣』と関係があるのかどうかは知りませんが、議論の価値はないと思われます。
 では追ってその方法を解説しましょう。まず基本スプリッタウィンドウ同様、それぞれのペインを管理するViewクラスを作成します。paneAの管理には相変わらずCPaneTestViewクラスを利用するとして、新たにCPaneBView, CPaneCViewを追加します。
 次に<MainFrm.h>ヘッダファイルにコード追加。


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

 んで、サクサクと次段階。<MainFrm.cpp>ソースファイルにCMainFrame::OnCreateClient関数をオーバーライドして、コードを追加。


#include  "PaneTestDoc.h"
#include  "PaneTestView.h"   // CPaneTestViewクラスのヘッダをインクルード
#include  "PaneBView.h"      // CPaneBViewクラスのヘッダをインクルード
#include  "PaneCView.h"      // CPaneCViewクラスのヘッダをインクルード

    :
    :

BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) 
{
    :
    :
  m_SplitWndMain.CreateStatic(  // メインスプリッタを作成
      this,                     //   メインスプリッタの親ウィンドウを指定。ここではメインフレームを分割するのでthisを指定
      1,                        //   メインスプリッタの行分割数を指定
      2);                       //   メインスプリッタの列分割数を指定

  m_SplitSub.CreateStatic(                 // サブスプリッタを作成
      &m_SplitWndMain,                      //   サブスプリッタの親ウィンドウはm_wndSplitMain
      2,                                    //   サブスプリッタの行分割を指定
      1,                                    //   サブスプリッタの列分割を指定
      WS_CHILD | WS_VISIBLE,                //   サブスプリッタのスタイルを指定
      m_SplitWndMain.IdFromRowCol(0,1));    //   子ウィンドウIDを指定。m_SplitWndMainの行0(=1行目)列1(=2列目)のIDを取得してます。多分...

  m_SplitWndMain.CreateView(           // paneAを作成。
      0,                               //   メインスプリッタの行番号を指定
      0,                               //   メインスプリッタの列番号を指定
      RUNTIME_CLASS(CPaneTestView),    //   paneAを管理するViewクラスCPaneTestViewを指定。
      CSize(20,0),                     //   ペインのサイズ。
      pContext);                       //   ???

  m_SplitWndSub(                     // paneBを作成。
      0,                             //   サブスプリッタの行番号を指定
      0,                             //   サブンスプリッタの列番号を指定
      RUNTIME_CLASS(CPaneBView),     //   paneBを管理するViewクラスCPaneBViewを指定。
      CSize(100,0),                  //   ペインのサイズ。
      pContext);                     //   ???

  m_SplitWndSub(                     // paneCを作成。
      1,                             //   サブスプリッタの行番号を指定
      0,                             //   サブンスプリッタの列番号を指定
      RUNTIME_CLASS(CPaneCView),     //   paneCを管理するViewクラスCPaneCViewを指定。
      CSize(0,0),                    //   ペインのサイズ。
      pContext);                     //   ???
    :
    :
}

 以上です。あや?簡単だな、こうやって書いてみると。もっとこう、複雑だったようなイメージがあるんだけどなァ……。ま、良いか。あ、あと、もう付け加えるのも面倒だけどMDIアプリケーションの場合は、CChildFrameクラスに追加していって下さい。

管理外Viewクラスからの各ペインへのアクセス方法(SDIの場合)
 さて、ここからが本題。ここまででスプリッタウィンドウを作成して沢山のペインを表示できましたが、これらをリンクさせながら動かせますか?例えば、paneAでマウスが左クリックされたらpaneBにテキストを出力。例えばpaneAで線を描画したらpaneBに縦反転、paneCで横反転画像を出力、と言う風に。
 普通、paneAがアクティブなら使用されるViewはCPaneTestViewであり、paneBがアクティブならCPaneBView、paneCならCPaneCView...と言う風な感じになっています。そして、各ペインに描画するには、各ペインを管理しているViewクラスの描画関数です(普通はOnDraw関数かOnPaint関数でしょう)。ですから、例えばCPaneTestView::OnLButtunDownメンバをオーバーライドして「左クリックを押したら"Test View"とテキスト出力」と言うプログラムを組もうとしても、CPaneBViewやCPaneCViewクラスにアクセスしていない為、飽く迄もpaneAにしか出力する事が出来ない。もしもこれらを相互アクセス無しに行なおうと思えば、まずpaneA上でマウスの左クリックが為されたら「左クリックが押されましたよ」と言う情報を、何らかの形でグローバル変数等に保持し、paneBがアクティブになった時にそのグローバル変数の中身を確認して、paneAで左クリックが為されていると確認したらテキスト出力をする、と言う風に、ユーザ側でアクセスしてやらなければならない(但し、私の知る限りにおいてはです)。
 アプリケーションとして、これ程面倒臭い物は無い。これらの作業をアプリケーション側で全て行なって、非アクティブペインへの描画等も行ないたい。そう思うのは、別に我侭でも何でも無いだろう。
 そこで、今からそれらの各ペインへのアクセス方法を説明したい。使用ウィンドウなどは、折角だから先刻作成した3ペインウィンドウを使用しよう。
 条件として、差し当たりアクティブなペインはpaneAと仮定する。これは別に、アクティブペインがpaneBだろうとpaneCだろうと構いはしません。実際にアプリケーションを作られる際は、御自分の好きなように設定して下さい。
 まずはSDIの場合。
 これは簡単。<CPaneTestView.h>ヘッダファイルに以下のコードを追加して下さい。


#include  "RightBottomView.h"
#include  "RightTopView.h"
#include  "MainFrm.h"

    :
    :

class CPaneTestView : public CView
{
    :
    :
public:
        CPaneBView*   p_paneB;      // CPaneBViewクラスへのポインタ
        CPaneCView*   p_paneB;      // CPaneCViewクラスへのポインタ
    :
    :
}

 続いて<CPaneTestView.cpp>ソースファイルに以下のコードを追加。


// 
#include "MainFrm.h"

    :
    :

CPaneTestView::OnDraw(CDC *pDC)
{
    :
    :
  p_paneB = (CPaneBView*)((CMainFrame*)AfxGetMainWnd())->m_wndSplitSub.GetPane(0,0);
  p_paneC = (CPaneCView*)((CMainFrame*)AfxGetMainWnd())->m_wndSplitSub.GetPane(1,0);
    :
    :
}

 ここで、AfxGetMainWnd()関数はメインフレームのオブジェクトを返します。
 これで後は、CPnaneTestViewクラスの任意の場所でpaneB及びpaneCにアクセス出来ます。因みに、各ペインのドキュメントには、CWnd::GetDC()でアクセスできます。例えばpaneBに「This region is PaneB.」と出力したければ、


p_paneB->GetDC()->TextOut(0,0,"This region is PaneB.");    // CView::TestOut関数は基本でしょう。説明端折る。

 SDIの場合ならメインウィンドウのすぐ下にスプリッタウィンドウがあるので、この簡単なコードでアクセス可能です。しかし!! MDIになるとこれでは無理。まず始めに、このままだと「m_wndSplitSubがCMainFrameクラスのメンバじゃない」とコンパイルエラーが発生します。ならばとばかりに(CMainFrame*)では無く(CChildFrame*)に強引にキャストすると構文とかに不備が無いのでコンパイルは通るようになります。が、これで安心してアプリケーションを実行すると、
p_paneB = (CPaneBView*)((CChildFrame*)AfxGetMainWnd())->m_wndSplitSub.GetPane(0,0);
の部分に差し掛かったところでアプリケーションが強制終了します。多分ですが、「メインウィンドウのすぐ下には子ウィンドウがいて、その下にスプリッタウィンドウがいるのに、メインウィンドウのすぐ下でスプリッタウィドウを探しに行っている」事が問題だと思う(これはかなり自信が無いです。違っていたら御免なさい。ま、強制終了すると言う事実だけ覚えておいて下さい)。
 では、MDIの場合は一体どうしたら良いのだろう?はい、唯今からそれを説明していきましょう。

管理外Viewクラスからの各ペインへのアクセス方法(MDIの場合)
 まず始めに、SDIの時と同じように、アクセス用のポインタをメンバ定義しましょう。因みにここでもCPaneTestViewからのアクセスを想定しますので、<CPaneTestView.h>ヘッダファイルにコードを入力します。


#include  "RightBottomView.h"
#include  "RightTopView.h"
#include  "MainFrm.h"

    :
    :

class CPaneTestView : public CView
{
    :
    :
public:
        CPaneBView*   p_paneB;      // CPaneBViewクラスへのポインタ
        CPaneCView*   p_paneB;      // CPaneCViewクラスへのポインタ
    :
    :
}

 続いて<CPaneTestView.cpp>ソースファイルへ移動して、以下のコードを入力。


// 
    :
    :

CPaneTestView::OnDraw(CDC *pDC)
{
  POSITION pos;    // POSITION型の変数。詳細は勉強不足により不明
  CView*   pView;  // CView*型のポインタ変数
    :
    :
  pos = pDoc->GetFirstViewPosition();    // 先頭にあるViewの位置を検索。但し、「先頭にある」Viewの位置が決してpaneAでは無い事に注意。
                                         // 戻り値はウィンドウの位置。
  while (pos != NULL)  // posがNULLになるまで、つまり最後のView位置まで検索
  {
    pView = (CView*)(pDoc->GetNextView(pos));               // View位置を1つずつずらす
    if (pView->IsKindOf(RUNTIME_CLASS(CPaneBView)))         // CWndBViewクラスを用いるViewの位置にいるならば、
      p_paneB=(CPaneBView*)pView;                           //   p_paneBにpViewが持つポインタを渡す
    else if (pView->IsKindOf(RUNTIME_CLASS(CPaneCView)))    // CWndCViewクラスを用いるViewの位置にいるならば、
      p_paneC=(CPaneCView*)pView;                           //   p_paneCにpViewが持つポインタを渡す
  }
    :
    :
}

 もしもペイン数が多くなれば、それに応じてwhile文にViewポインタを追加して行って下さい。
 因みに、CView::IsKindOf関数についてはよく知らないんで、コメント文は「こんな感じかな?」と言う予想で書いています。あまり鵜呑みにしないようにあと、
pView = (CView*)(pDoc->GetNextView(pos));
のコードは、while文の最後に入れた方が良いかも知れません。私のプログラムでは上記コードで正常に動作していますが、これを改めて見てみると、「一番最初のView位置を見てないんじゃ無いのか?!」と言う危惧が生まれています。まぁ、各人で御確認下さい。で、解答を見出せましたら、是非ともメールなどを下さいませ。
 さて、以上でペインサクセスの方法への講義は終了。それでは皆さん、然様ならああぁぁぁぁ〜〜〜〜〜〜。

Visual C++ プログラミング講座

Return to Top-page    Return to Extra-Index