ホーム | Software Lab | Profile | Favorites | Diary | Links | About This Site | Feedback

 

 

第5回は、 前回のサンプル S2H_Conv のフレームへ Splitter と Pane の実装をしながら、Frame の概念等を説明していきます。

今回のポイント
Frame の概念を理解しよう。Splitter と Pane を使ってみよう!

  • Frame の概要(MainFrm.h)

    さて、今回は Splitter Window と Pane Containeer を追加していくわけですが、じゃあいったいどこに追加するのがいいのか?という疑問にお答えするためにも、MainFrm の中ではどんなことが行われているのか?ということを簡単に解説していきます。

    まず、ヘッダファイル(MainFrm.h)のほうからいきましょう。

    このヘッダファイルでは、CMainFrame というクラスを宣言しています。このクラスは言うまでもなくこれから作成しようとしている S2H_Conv のメインウィンドウ(の枠)を表します。

class CMainFrame : public CFrameWindowImpl<CMainFrame>, public CUpdateUI<CMainFrame>,
public CMessageFilter, public CIdleHandler
{

}

上記が、そのクラスの宣言部分ですが、このクラスはいくつかのクラスを多重継承しています。WTL(ATL)ではこの多重継承がよく使われていますので、多重継承がわからないという方はこの機に多重継承を理解しておいてください。

それぞれ継承されているクラスを説明すると、CFrameWindowImpl<> というテンプレートクラスは atlframe.h で定義されているクラスで、これ自体もテンプレートクラスを継承した・・・それこそ継承やテンプレートを理解していないと、何だコリャ?クラスになっていますが、ツールバー、ステータスバー、クライアントウィンドウ、アクセラレータを管理できるフレームワークのように理解していいと思います(メンバ変数は atlframe.h の386行目あたりに定義されています:WTL7.1)。

この辺の定義がきちんとソースで読めるので理解さえできれば、自分好みに手を加えることも可能なのが、WTL の大きな魅力のひとつでしょう。逆に言えば、WTL で Wizard を使って SDI アプリケーションを作ると、自然にこれらをサポートするフレームワークが提供されることになります。

CUpdateUI はメニュー、ツールバー、ステータスバーなどのボタンの状態やチェックマークなどの状態を更新するためのクラスです。MainFrm.h には BEGIN_UPDATE_UI_MAP というマクロが記述されていて、ここに

UPDATE_ELEMENT(シンボルID, そのシンボルを更新する種類)

という形で登録することで、後述する CUpdateUIBase::UIUpdate 系の(CUpdateUIBase は CUpdateUI の基本クラスで多重継承しています)メソッドを実行したときに関連するものも一緒に更新されるようになっています。

CMessageFilter はメッセージのディスパッチを行います。きちんと理解していないで書くのもアレなんですが、このフレーム内でビューやダイアログを使うときに、その中でキーボードアクセラレータなどを使いたいときに PreTranslateMessage を実行してあげる必要があるわけですが、それを実装するために必要な抽象インタフェースクラスになっています。

意味がおかしかったら突っ込みお願いします・・・

CIdelHandler はアイドル時に行う処理を記述するときに使う、OnIdle()(後述)というメソッドを実装するための抽象インターフェースクラスです。これらは作成したアプリケーションを使うユーザーが何もしていないときに呼び出され、ステータスバーの状態やツールバーの状態を更新したりするときに使います。

この辺は変な説明を聞くよりも MSDN で 「ATL 3.0 ウィンドウクラス:入門」というトピックを読んだり、ソースを眺めたりしてもらったほうがわかるかもしれません。

また、WTL ではテンプレートクラスを継承する際に、継承先のクラス(つまり派生クラス)をテンプレートのメンバとして指定するという方法がよく使われています。最初なんだこりゃ?って感じですごく理解に苦しんだのですが、現在はこう解釈しています。

間違っているようでしたらご指摘ください。
またこの仕組みに名前がついていたら教えてください

ここで例として

class CMainFrame : public CFrameWindowImpl<CMainFrame>

という部分を考えて見ます。CFrameWindowImpl<> というテンプレートクラスは、ユーザーがフレームウィンドウ(枠部分)を作成するときに使うテンプレートなわけです(何を当たり前のことを・・・)。で、当然のことながら CFrameWindowImpl<> はユーザーが定義するフレームウィンドウクラスに継承されることを前提として作られています。また、WTL には 〜 Impl とか 〜 Base というクラスが出てくるのですが、〜 Impl というクラスはメッセージマップを持っていて、さまざまな Window メッセージに対応することができるようになっています。

WTL Wizard によって作成された SDI アプリケーションの雛形はこちらで何も定義していないのにもかかわらず、メニューがあって、ツールバー( Rebar ) があって、サイズを変更したときにも正しくツールバーが描画されています。SDK などでは自分で WM_SIZE メッセージを拾って、クライアントサイズを取得して、ツールバーのサイズを調整して・・・といったことを行う必要があるのですが、この辺の共通する部分はすべてフレームワークつまりこの場合 CFrameWindowImpl<> の中で定義されています(atlframe.h 1249 行目あたり)。

ところがテンプレート側ではユーザーが定義したウィンドウにどんな子ウィンドウが定義されているか?WM_SIZE を呼ばれたときにどんなことをしてほしいのかわかりません。また、元の OnSize は仮想関数(virtual)として定義されているわけではないので、オーバーライドすることができません。

では、どのようにして呼んでいるのかというと、自分のポインタをこのテンプレートクラス(CFrameWindowImpl<>)に渡したクラスのポインタにキャストして UpdateLayout() 呼び出しています。このメソッドは実際には自分で定義するのではなく、CFrameWindowImplBase<>( CFrameWindowImpl<> の継承元) に定義されていて、ここで先ほど説明した一連の操作が行われています。

LRESULT OnSize(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled)
{
    if(wParam != SIZE_MINIMIZED)
    {
        T* pT = static_cast<T*>(this);
        pT->UpdateLayout();
    }
    bHandled = FALSE;
    return 1;
}

なぜ WTL ではこのような複雑なことをしているのでしょうか?

これはおそらく MFC のような CObject からすべてが継承されるクラスモデルの場合には、仮想関数を定義することによって起こる VTABLE のオーバーヘッドが大きかったためだろうと思われます。WTL の仕組みでは仮想関数を使わないので、VTABLE を経由することなく仮想関数的な動作を行うことができるから・・・だと思っています。

前置きが長くなりました、しかも解説に自信なし

  • Frame の概要(MainFrm.cpp)

    今後は cpp 側のほうを見てみましょう。

    まず PreTranslateMessage は継承元の PreTranslateMessage を呼び出しています。アクセラレータが登録されていた場合に TranslateAccelerator を呼び出しています(atlframe.h 856 行目)。そしてその後は View の PreTranslateMessage を呼び出しています。

    OnIdle ではアイドル時に行う動作を定義しています。デフォルトではツールバーを更新しています。

    OnCreate() は今回の講座のメインです。ここではコマンドウィンドウの作成、メニューのロード、ツールバーの作成、そしてクライアントになる View の作成と割り当て、ステータスバーの作成、ユーザーインタフェースの状態の設定、メッセージループへの登録、といったようなことが行われています。

    つまり、ここはこのフレームがはじめて作成されるときに呼び出され(WM_CREATE)、表示するのに必要なオブジェクトを初期化したりする場所です。

    残りの5つはコマンドハンドラと呼ばれていて、WM_COMMAND メッセージが通知されたときに呼び出されるようになっています。実際には、終了、ファイルの作成、ツールバー、またはステータスバーの表示、About ダイアログの表示のメニューまたはアクセラレータを実行したときに呼び出されます。

  • Splitter と Pane を実装する

    では長い説明も飽きてきたので、そろそろメインの Splitter と Pane の実装に入ります。ここではまず、どのような Window の関係になっているのかを図で理解してみましょう。

    ここでは各ウィンドウの親子関係に気をつけてください。つまり、Frame Window の子は CSplitterWindow、CSplitterWindow の右側のウィンドウの子ウィンドウが CHorSplitterWindow そしてそれぞれの子に View が配置されます。

    View はまだ作っていませんが、ここでは便宜上デフォルトの S2H_ConvView を使います。全部で3つの View が必要なので、MainFrm.h に3つの View を定義します。名前もわかりやすいようにつけてみました。

CS2H_ConvView       m_cFolderView;
CS2H_ConvView       m_cSourceView;
CS2H_ConvView       m_cHtmlView;

CSplitterWindow の左側に配置する View は現在のフォルダの一覧を表示し、右上に配置する View はユーザーが選択したソースファイルの内容を表示し、右下に配置する View はそれを HTML に変換した内容を表示するということにします。

また、上記の絵にはありませんが、Explorer のようにフォルダツリーの上にペインと呼ばれるテキストを表示する領域を追加してみましょう。これは CPaneContainer と呼ばれています(atlctrlx.h)。

これらを利用するためにヘッダファイルに以下の宣言を追加しておきます。

CSplitterWindow       m_cSplitterWindow;
CHorSplitterWindow    m_cHorSplitterWindow;
CPaneContainer       m_cPaneContainer;

またこれらを使うので、stdafx.h に atlctrlx.h と atlsplit.h を追加しておいてください。

(略)
extern CAppModule _Module;

#include <atlwin.h>

#include <atlframe.h>
#include <atlctrls.h>
#include <atldlgs.h>
#include <atlctrlw.h>
#include <atlctrlx.h>
#include <atlsplit.h>

上記をすべて追加した状態のウィンドウの親子関係は以下のようになります(上が親)

大体感じをつかめたでしょうか?

それでは、MainFrm.cpp の OnCreate を変更していきます。最初に二つの SplitterWindow と Pane を作ってしまいましょう。m_cSplitterWindow の親には CMainFrame の m_hwnd を、m_cHorSplitterWindow の親には m_cSplitterWindow の m_hWnd を指定してることに注意してください。PaneContaineer のタイトルはフォルダとしました。

次に、デフォルトの OnCreate では CMainFrame の子ウィンドウは m_view つまり CS2H_ConvView だったのですが、これを CSplitterWindow に変更します。

LRESULT CMainFrame::OnCreate
                                        (UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
    (略)

    CreateSimpleStatusBar();

    /// SplitterWindow の作成
    m_cSplitterWindow.Create(m_hWnd, rcDefault, NULL, 
                                                  WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS);
    m_cHorSplitterWindow.Create(m_cSplitterWindow.m_hWnd, rcDefault, NULL, 
                                                  WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS);

    /// Pane Windows の作成
    m_cPaneContainer.Create(m_cSplitterWindow, _T("フォルダ"));

    /// 基礎となる Window の設定
    m_hWndClient = m_cSplitterWindow.m_hWnd;

また、3つの View も作成しておきます。パラメータは今はデフォルトの View と同じものを使いますが、親となるウィンドウハンドルに注意してください。そして、それぞれの SplitterWindow に子ウィンドウを割り当てて行きます。

m_cSplitterWindow(左右に分割したウィンドウ)の左側の子ウィンドウは CPaneWindow、右側は m_cHorSplitterWindow (上下に分割したウィンドウ)になります。そして、m_cHorSplitterWindow の上側の子ウィンドウは m_cSourceView、下側は m_cHtmlView となります。

この割り当てを行うには SplitterWindow の SetSplitterPanes というメソッドを使います。SetSplitterPane というメソッドを使うと個別に割り当てることもできます。

    (続き)

    /// 各子ウィンドウ(View)の作成
    m_cFolderView.Create(m_cPaneContainer.m_hWnd, rcDefault, NULL, 
                            WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, WS_EX_CLIENTEDGE);
    m_cSourceView.Create(m_cHorSplitterWindow.m_hWnd, rcDefault, NULL, 
                            WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, WS_EX_CLIENTEDGE);
    m_cHtmlView.Create(m_cHorSplitterWindow.m_hWnd, rcDefault, NULL, 
                            WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, WS_EX_CLIENTEDGE);

    /// Splitter Windows のクライアント(子ウィンドウ)の設定
    m_cSplitterWindow.SetSplitterPanes ( m_cPaneContainer, m_cHorSplitterWindow );
    m_cHorSplitterWindow.SetSplitterPanes ( m_cSourceView, m_cHtmlView );

次に Pane Containeer の子ウィンドウを設定します。これには SetClient メソッドを使います。ここでは Pane の子は CFolderView ですので、そのように指定します。また、Pane Containeer には×ボタンが表示されますが、今は消されては困るので消してみました。このようなウィンドウを消せるようにする場合は、メニューからそれぞれの子ウィンドウ(この場合だと CFolderView) を表示させることができるようにしないと、二度と表示できないので注意が必要です。

UpdateLayout は各 Window の領域を更新します。

その後、Splitter Window の幅の設定を行います。最初に上下、左右の Splitter Window のそれぞれの Window の最小値を決定します。ここではそれぞれ 200 と 100 に設定してみました。

また、初期表示のときにどのくらいの比で分割されているかを設定するには SetSplitterPos メソッドで指定します。

    (続き)

    /// Pane のクライアント(子ウィンドウ)の設定
    m_cPaneContainer.SetClient(m_cFolderView);
    m_cPaneContainer.SetPaneContainerExtendedStyle ( PANECNT_NOCLOSEBUTTON );

    UpdateLayout();

    /// Splitter の最低サイズの設定
    m_cSplitterWindow.m_cxyMin = 200;
    m_cHorSplitterWindow.m_cxyMin = 100;

    /// Splitter の初期の分割比の設定
    m_cSplitterWindow.SetSplitterPos(200);
    m_cHorSplitterWindow.SetSplitterPos(400);

    UIAddToolBar(hWndToolBar);
    (略)
}

以上で、Splitter と Pane を使ったウィンドウが作成できました。

ここまでの内容はこちらからダウンロードできます(12 KB)

次回はフォルダの内容を表示する CFolderView を作成したいと思います。

第6回へ

最終更新日 : 2005/04/13