作成日: 2003/02/09

MFC でダイアログベースのコントロールを作成する

概要

MFC の [MFC ActiveX Control Wizard] を使うと、非常に簡単に ActiveX コントロールを作成できます。 この上に、ダイアログリソースを利用したコントロールを載せることができればさらに便利です。 ここではその方法を紹介します。なお、この方法では、 コントロールを IE 上でスクロールさせた場合に、 描画がおかしくなってしまいます。 どうも、IE か COleControl のバグくさいのですが、回避方法は見つけていません。 けれど、ボタンのような小さなコントロールを簡単に作成する際には、十分利用できます。 ネットで探せばいくつかこの方法を紹介したページがありますので、もっと詳細な内容を知りたい方は 探してみてください。 なお、以下の説明では VC++ 6 を前提にしています。

プロジェクトを作成する

VC を起動し、メニューから [ファイル] - [新規作成] を選び、表示される [新規作成] ダイアログボックスから [MFC ActiveX Control Wizard] を選択し、適当なプロジェクト名(ここでは howtomfcdlg とします)を入力し、[OK] をクリックします。
[MFC ActiveX Control Wizard - ステップ 1/2] が表示されます。そのまま [次へ] をクリックします。
[MFC ActiveX Control Wizard - ステップ 2/2] が表示されます。これも、そのまま [終了] をクリックします。
[新規プロジェクト情報] が表示されるので、そのまま [OK] をクリックします。

ダイアログを作成する

[Resource View] を右クリックし、[挿入] を選びます。[リソースの挿入] ダイアログが表示されるので、 [Dialog] のところの左横にある + をクリックして、表示されるダイアログ形式一覧の中から、[IDD_FORMVIEW] を選択し、[新規作成] ボタンを押します。
挿入されたダイアログをリソースエディタで編集し、テキストボックスを配置します。 ここでは、[IDC_EDIT1]、[IDC_EDIT2] のリソースIDを持つテキストボックスを2つ挿入しました。 また、ダイアログのリソースIDは挿入されたときのまま、[IDD_FORMVIEW] としています。

ダイアログに対応するクラスを作成する

リソースエディタで作成したダイアログをダブルクリックすると、[MFC Class Wizard] が表示されるとともに、 [クラスの追加] ダイアログボックスが表示されます。[新規クラスの作成] がチェックされているのを確認して [OK] をクリックします。
[クラスの新規作成] ダイアログが表示されるので、クラス名に適当な名前(ここでは CMfcDlg とします)を入力し、 [OK] をクリックします。 生成されたクラスを [ClassView] などで確認してください。

作成したダイアログクラスをコントロールクラスのメンバに追加する

[Class View] を開くと、Cプロジェクト名Ctrl というクラスがあります(ここでは、CHowtomfcdlgCtrlとなります)。 これがコントロール処理のメインとなるクラスで、COleControl を継承しています。COleControl は OLE コントロール (ActiveX コントロールと呼ばれているもの) の作成に特化したクラスで、かなり大きなクラスです。詳細は MSDN に 譲ります(私も詳しくは知らないのです)。 このクラスに先ほど作成したダイアログクラスの変数をメンバとして追加します。
[Class View] でこのクラスを右クリックして、[メンバ変数の追加] を選択すると [メンバ変数の追加] ダイアログが 表示されるので、[変数のタイプ] に CMfcDlg、[変数名] に m_MfcDlg と入力して [OK] をクリックします。 自分で HowtomfcdlgCtl.h を開いて変数を追加しても構いません。が、[メンバ変数の追加] を使うと、変数のタイプに 対応するクラスのヘッダファイルのインクルード文を自動的に追加してくれるので少しだけ便利です。

追加したダイアログを表示する処理を追加する

コントロールクラスのメンバとしてダイアログクラスの変数を追加しましたが、追加しただけでは、ダイアログを 表示してくれません(当たり前か。。。)。ダイアログを表示(ウィンドウの作成も)するための処理を追加します。
ダイアログウィンドウを作成・表示するタイミングはコントロールクラスのウィンドウ作成時にあわせて、 WM_CREATE 時に行います。 まず、コントロールクラスの WM_CREATE ハンドラを追加します。[Class View] で CHowtomfcdlgCtrl クラス を右クリックし、[Windows メッセージハンドラの追加] を選びます。 [クラス CHowtomfcdlgCtrl 用の Windows メッセージおよびイベントハンドラの新規作成] ダイアログが 表示されるので、[Windows メッセージ/イベントの新規作成] から WM_CREATE をダブルクリックします。 [既存のメッセージ/イベントハンドラ]欄に WM_CREATE が移動したら、[OK] をクリックします(もし、 [Windows メッセージ/イベントの新規作成] の一覧の中に WM_CREATE がない時は、[クラスで使用可能なメッセージ用 フィルタ] に [ウィンドウ] が選択されているか確認してください。選択されていない場合は、 選択してみてください。WM_CREATE が一覧に表示されるはずです)。 OnCreate(LPCREATESTRUCT lpCreateStruct) メソッドが CHowtomfcdlgCtrl に追加されるので、[Class View] などで確認してください。
次に、OnCreate() にダイアログの作成表示処理を追加します。[Class View] で OnCreate() メソッド をダブルクリックするなどして定義を表示させます。以下のようになっていると思います。

int CHowtomfcdlgCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (COleControl::OnCreate(lpCreateStruct) == -1)
        return -1;

    // TODO: この位置に固有の作成用コードを追加してください

    return 0;
}

これを以下のように変更します。なお、以下では、表示するダイアログをコントロール領域の大きさに 合わせて表示させるようにしています。

int CHowtomfcdlgCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (COleControl::OnCreate(lpCreateStruct) == -1)
        return -1;

    // ダイアログのウィンドウを作成し、コントロールのウィンドウの
    // 大きさにあわせて表示する。
    m_MfcDlg.Create(IDD_FORMVIEW, this);
    m_MfcDlg.MoveWindow(lpCreateStruct->x, lpCreateStruct->y,
                           lpCreateStruct->cx, lpCreateStruct->cy, FALSE);

    m_MfcDlg.ShowWindow(SW_SHOW);
    m_MfcDlg.UpdateWindow();

    return 0;
}

CMfcDlg は自身のダイアログリソース ID を知っているのに、m_MfcDlg.Create() の引数に IDD_FORMVIEW を渡すのは少し気持ちが悪いので、 CMfcDlg に以下のようなコンストラクタを追加します。

BOOL CMfcDlg::Create(CWnd *pParentWnd)
{
    return CDialog::Create(IDD, pParentWnd);
}

その後で、さきほどの m_MfcDlg.Create() のところを、このコンストラクタを使うように変更します。

    //m_MfcDlg.Create(IDD_FORMVIEW, this);
    m_MfcDlg.Create(this);	// すわりはいいか(*_*)/~~???

本筋と全然関係ない話ですみません。。。
とりあえず、これでコントロールのウィンドウが作成されるときに一緒にダイアログを作成し表示できるようになります。 この時点で CHowtomfcdlgCtrl のメソッド OnDraw() の処理は不要になるので、削除したくなるのが人情ですが、 後でちょっとだけ使うので、とりあえず、OnDraw() の中の描画コードのみ削除して、メソッド自体は残しておいてください。

ビルドしてダイアログが表示されるか試してみる

ビルドして動かしてみます。が、その前に作成したコントロールを表示するためのコンテナプログラムが必要に なります。と、いうと大げさですが、単に作成したコントロールを表示するために、貼り付ける場所が必要なだけです。 ここでは、IE を利用します。ATL でコントロールを作成するときと違い (ATL でダイアログベースのコントロールを作成する を参照してください)、MFC のウィザードでは コントロールの表示テストに使える html ファイルを自動生成してくれません。いちから書くのは、めんどくさいので、 ATL で作ったときのものをコピー・修正して使いました(これを読んでる人は以下をカット&ペーストすると楽かもしれません)。 これを適当な名前で保存してください。

<HTML>
<HEAD>
<TITLE>オブジェクト howtomfcdlg 用 テスト ページ</TITLE>
</HEAD>
<BODY>
<OBJECT ID="MfcDlg" CLASSID="CLSID:9E2EDBCC-C893-4478-8AF2-02AD3D365685" WIDTH="500" HEIGHT="500">
</OBJECT>
</BODY>
</HTML>

って、ごめんなさい。単純にカット&ペーストすると駄目でした。このページの下の方にあるサンプルソースで 試されている方はこれでいいのですが、自分で一から作成されている方は、自分の作成したコントロール のクラスIDを CLASSID の箇所に記述する必要があります。 コントロールのクラスIDは、プロジェクトのフォルダの下の プロジェクト名.odl という名前のファイルに 記述されていますので、開いてみてください(ここでは howtomfcdlg.odl になります)。 一番下に以下のような記述があります。uuid() で囲まれた部分が作成されたコントロールのクラスIDになります。

    //  CHowtomfcdlgCtrl のクラス情報

    [ uuid(9E2EDBCC-C893-4478-8AF2-02AD3D365685),
      helpstring("Howtomfcdlg Control"), control ]
    coclass Howtomfcdlg
    {
        [default] dispinterface _DHowtomfcdlg;
        [default, source] dispinterface _DHowtomfcdlgEvents;
    };

ちなみに、odl ファイルから Web の <OBJECT> タグ内にクラス ID をコピー&ペーストするときに 私がよくやってしまうのが、"CLSID:" をうっかり削除してしまう、という間違いでしょうか。 「なんでコントロールが表示されんのだぁ!」とぶつぶついいつつ、「あ、俺か。。。」と反省したことが何度かあります。。。 必ず、CLASSID="CLSID:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" の形式になるようにしてください。
ではビルドして動かしてみます。メニューから [ビルド] - [ビルド] を選択します。ビルドが完了したら、 動かしてみます。メニューから [プロジェクト] - [設定] を選ぶと、[プロジェクトの設定] ダイアログが表示 されるので、[デバッグ] タブを選択します。 [デバッグ] タブで [デバッグセッションの実行可能ファイル] の右横にある矢印をクリックし、 [デフォルト Web ブラウザ] を選択します。[デバッグセッションの実行可能ファイル] 欄にデフォルトの ブラウザへのパスが設定されるので、[OK] をクリックします。
メニューから [ビルド] - [デバッグの開始] - [実行] を選択すると、ブラウザが起動するので、 さきほど作成・保存した html をドラッグ&ドロップして開きます。
コントロールが表示されたでしょうか?。 表示されない場合、以下のことが考えられます。

  • ブラウザが IE ではない。ネットスケープなどではそのままでは Active X コントロールを 表示できません。IE を使ってください。
  • IE の設定で Active X コントロールが実行できないようになっている。この場合、実行できるように ブラウザのオプションを設定してください。
  • CLASSID が間違っている。あるいは私と同じミスをして "CLSID:" 文字列を削ってしまった。

表示できたら、試しに、ダイアログに載せてあるテキストボックスに文字を入力してみてください。 色々やっていると、タブキーがきかないことに気が付くのではないでしょうか。 さらに、表示されている IE のウィンドウを小さくして、スクロールバーでコントロールをスクロールしてみてください。 あまりにもすばやく動かしてしまうとわかりにくいので、ゆっくりとスクロールしてみてください。 コントロールの表示がおかしくなるのが確認できると思います。 私が確認した範囲では、IE 5.0、5.5、6.0 で同じような現象をみています。人に聞いた話では、 コントロールのベースクラスとなっている COleControl のバグだとのことなのですが、私自身は それを記述した MS の技術情報を見つけることができませんでした。
次のステップでは、タブキーがきかないことへの対応と、描画異常への対応をします。

タブキーが使えるようにする

タブキーの問題への対応方法は MS のサポート技術情報 187988 に公開されており、 ActiveX コントロールがモーダレスダイアログボックスの親である場合に起こるようです。 これによると、タブキーがきかない原因は、ActiveX コントロールが自身のメッセージポンプ (たぶんメッセージループのことだと思う)を持っておらず、コンテナアプリケーション (ここでの場合 IE がコンテナになります)がメッセージポンプを所有しているために、 すべてのキーストロークメッセージがコンテナアプリケーションに取られてしまい、 モーダレスダイアログに送られないため、となっています。
公開されている対処方法のソースでは、フックプロシージャを用意して、メッセージを横取りし、 キーストロークメッセージをコントロール側の PreTranslateMessage() へ送るように しています。 ただし、この方法をそのまま適用するとダイアログ上のテキストボックスに入力した日本語が 化けてしまうので、必要最低限のキーメッセージのみ処理することにし、また、横取りしたメッセージは コントロールの PreTranslateMessage() へ送るのではなく、IsDialogMessage() を使って 処理します。
最初に、フックプロシージャのハンドルと、ダイアログのウィンドウハンドルを保持するための 変数を定義します。記事では、ハンドルを保持する変数をグローバルに設けていましたが、 ここではダイアログクラスのスタティックなメンバ変数として定義します。ただ、このやり方だと、 このダイアログクラスからは、ひとつのインスタンスしか作成できません。グローバル変数でやっても 同じことではありますが。。。
では、MfcDlg.h を開いて、クラスの宣言の中の適当な位置に以下の記述を追加してください。

    static HWND m_hDlgWnd;
    static HHOOK m_hHook;

さらに、MfcDlg.cpp を開いて適当なところに以下の記述を追加してください。

HHOOK CMfcDlg::m_hHook = 0;		// フックプロシージャのハンドルを保持する。
HWND CMfcDlg::m_hDlgWnd = 0;	// ダイアログのウィンドウハンドルを保持する。

次に、フックプロシージャを作成します。これも記事ではグローバル関数として実装されていましたが、 ここではクラスのスタティックメソッドとして実装します。 [Class View] で CMfcDlg を右クリックして表示されるポップアップメニューから [メンバ関数の追加] を選んでください。表示される [メンバ関数の追加] ダイアログで、[関数の型] に LRESULT CALLBACK [関数の宣言] に GetMfcDlgMsgProc(int code, WPARAM wParam, LPARAM lParam) と入力し、 [Static] にチェックを入れてください。[アクセス制御] は何でも良いです(って、またいいかげんな。。。)。 [OK] をクリックするとメンバ関数が追加されますので、定義を表示し、以下のよう修正します。

LRESULT CALLBACK CMfcDlg::GetMfcDlgMsgProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    LPMSG lpMsg = (LPMSG) lParam;

    if (nCode == HC_ACTION && wParam == PM_REMOVE) {
        switch (lpMsg->message) {
        // ※ここではダイアログに必要最低限のキーメッセージ
        //   しか処理していない。不具合が出る場合、
        //   MS のサポート技術情報 187988 を参照して、
        //   必要なキーおよび処理を追加すること。
        case WM_KEYDOWN:
            if (::IsDialogMessage(m_hDlgWnd, lpMsg)) {
                lpMsg->message = WM_NULL;
                lpMsg->lParam  = 0;
                lpMsg->wParam  = 0;
            }
            break;
        }
    }

    return CallNextHookEx(m_hHook, nCode, wParam, lParam);
}

次に、フックプロシージャのインストールとアンインストールを行う処理を追加します。 フックプロシージャのインストールは CMfcDlg の WM_INITDIALOG ハンドラで行い、 アンインストールは CMfcDlg の WM_DESTROY ハンドラで行います。
では、ハンドラを追加します。[Class View] で CMfcDlg を右クリックし、[Windowsメッセージハンドラの追加] を使って、WM_INITDIALOG と WM_DESTROY のハンドラをそれぞれ追加してください。追加したら、 定義を開き、以下のように修正します。

BOOL CMfcDlg::OnInitDialog()
{
    CDialog::OnInitDialog();
    // タブキーを有効にするために、フックプロシージャをインストールする。
    m_hDlgWnd = GetSafeHwnd();    // フックプロシージャで使用するので保存しておく。
    m_hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMfcDlgMsgProc, NULL, GetCurrentThreadId());

    return TRUE;  // コントロールにフォーカスを設定しないとき、戻り値は TRUE となります
                  // 例外: OCX プロパティ ページの戻り値は FALSE となります
}

void CMfcDlg::OnDestroy()
{
    CDialog::OnDestroy();

    // フックプロシージャをアンインストールする。
    UnhookWindowsHookEx(m_hHook);
}

さらに、CHowtomfcdlgCtrl クラスに OnClose メソッドを追加します。ただし、ここで追加する OnClose は WM_CLOSE のハンドラとしてではなく、COleControl の仮想関数 OnClose をオーバーライドしたメソッドです。
[Class View] で CHowtomfcdlgCtrl を右クリックし、[メンバ関数の追加] を選択し、表示される [メンバ関数の追加] ダイアログで、[関数の型] に void、[関数の宣言] に CHowtomfcdlgCtrl::OnClose(DWORD dwSaveOption) と入力し、[Virtual] にチェックをいれ、[OK] をクリックします。作成された定義を表示し、 以下のようにコードを追加します。

void CHowtomfcdlgCtrl::OnClose(DWORD dwSaveOption)
{
    // コントロールが破棄されるタイミングでモーダルダイアログ
    // ボックスを破棄する。
    m_MfcDlg.DestroyWindow();
    COleControl::OnClose(dwSaveOption);
}

実は、ここで追加する OnClose がなくても、CMfcDlg::OnDestroy() は呼び出されているようです。 が、お行儀が悪いような気がして、コントロールが破棄されるときに自前で作ったダイアログを 明示的に破棄しておこうと思い、追加しました。
では、修正が済んだら、ビルドして動かしてみてください。タブキーが処理されるのが分かると思います。

描画異常への対応をする (不完全)

描画異常への対応は、あちこちと探してみたのですが良い方法を見つけることができませんでした。 仕方なく、ダイアログに対し、常に全領域を描画させる方法を選びました。 なお、この方法だと、コントロールをスクロールさせると、ダイアログがちらついてしまい、 見栄えが良くありませんが、ご容赦ください。
CHowtomfcdlgCtrl の OnDraw() を開き、以下のように修正します。

void CHowtomfcdlgCtrl::OnDraw(
            CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)
{
    m_MfcDlg.Invalidate(); // 常に全描画させる。
}
その他

ちらつきは表示面の大きなコントロールでは目立ちますが、小さなコントロールであれば、あまり目立ちませんので (ちらつかないということではない)、 ボタンなどのコントロールを簡単に作成する時には便利なのではないかと思いますのでお試しください。 といってもあまりに大きなボタンを作成してしまうと、やっぱりちらつきが目立ってしまいます。。。 また、もっと良い描画異常への対応方法もあるかもしれませんので、ぜひ探してみてください。
なお、ここで作成したコントロールをレジストリから削除するには、コマンドプロンプトを起動し、 作成された ocx が存在するフォルダに移動し、

regsvr32 /u howtomfcdlg.ocx

としてください。

サンプルソース
howtomfcdlg.zip (16.9 KB)