作成日: 2004/08/25

ATL で作成した Active X コントロールのサンプル

概要

ATL を使って、プロパティページ、接続ポイント、スレッドを実装した Active X コントロールを作る機会があったので、自分の覚え書きにサンプルを残しました。
なお、以下の説明では VC++ 6 を使用しています。

コントロールの作成

VC を起動し、メニューから [ファイル] - [新規作成] を選択し、[新規作成] ダイアログを表示します。 [プロジェクト] タブで、[ATL COM AppWizard] を選択し、プロジェクト名を適当に (ここでは atlctrlsample にしました) 入力し、[OK] をクリックします。

新規作成ダイアログ

[ATL COM AppWizard ステップ 1/1] で、サーバータイプを ダイナミック リンク ライブラリ (DLL) にし、MFC のサポートにチェックを入れ (MFC を使わない場合は、当然チェックの必要はありません)、[終了] をクリックします。

ATL COM AppWizard ステップ 1/1

確認のための [新規プロジェクト情報] で [OK] をクリックします。

新規プロジェクト情報

後で作成するコントロールから、接続ポイントを使ってイベント通知する際に使用する ATL オブジェクトを作成します。
メニューから [挿入] - [ATL オブジェクトの新規作成] を選択し [ATL オブジェクトウィザード] を表示します。
[カテゴリ] にオブジェクト、[オブジェクト] にシンプルオブジェクトを選択し、[次へ] をクリックします。

ATL オブジェクトウィザード

表示される [ATL オブジェクトウィザードのプロパティ] の [名前] タブで [ショートネーム] を適当に (ここでは SampleParam としました) 入力し、[アトリビュート] タブで [スレッドモデル] をアパートメントに、[インターフェイス] をデュアル、 [アグリゲーション] を はい にし、[OK] をクリックします。

ATL オブジェクトウィザードのプロパティ - 名前 ATL オブジェクトウィザードのプロパティ - アトリビュート

次にコントロールを作成します。
メニューから [挿入] - [ATL オブジェクトの新規作成] を選択し [ATL オブジェクトウィザード] を表示します。
[コントロール] にオブジェクト、[オブジェクト] にフルコントロールを選択し、[次へ] をクリックします。

ATL オブジェクトウィザード

表示される [ATL オブジェクトウィザードのプロパティ] の [名前] タブで [ショートネーム] を適当に (ここでは SampleCtrl としました) 入力し、[アトリビュート] タブで [スレッドモデル] をアパートメントに、[インターフェイス] をデュアル、 [アグリゲーション] をはいに、[ISupportErrorInfo サポート] と [コネクションポイントのサポート] にチェックを入れ、 [OK] をクリックします。
※[その他]、[ストックプロパティ] はデフォルトのままです。

ATL オブジェクトウィザードのプロパティ - 名前 ATL オブジェクトウィザードのプロパティ - アトリビュート

[ClassView] に作成されたクラスが表示されているのを確認してください。

ClassView

とりあえず、メニューから [ビルド] - [ビルド] を選んで、ビルドしてください。 ライブラリに最新の SDK を使用している場合、

c:\documents and settings\bono\my documents\visual studio projects\atlctrlsample\samplectrl.h(97) :
error C2668: 'InlineIsEqualGUID' : オーバーロード関数の呼び出しを解決することができません。
(新機能 ; ヘルプを参照)

のようなエラーになる場合があります。原因はマイクロソフト サポート技術情報 243298 - [VC60] C2668 エラーが InlineIsEqualGUID の定義部分で発生 でリポートされています。
私の環境でも新しい SDK を参照しているため、このエラーが出ます。サポート技術情報ではいくつかの回避方法が 紹介されていますが、私は下記の方法を選びました。

コントロールのクラスである CSampleCtrl のヘッダ CSampleCtrl.h の ISupportsErrorInfo の実装部分 InterfaceSupportsErrorInfo() の InlineIsEqualGUID() を呼び出している箇所に ATL の InlineIsEqualGUID() を使用するよう、 識別子 ATL:: を付加しました。

    STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid)
    {
        static const IID* arr[] =
        {
            &IID_ISampleCtrl,
        };
        for (int i=0; i<sizeof(arr)/sizeof(arr[0]); i++)
        {
            if (ATL::InlineIsEqualGUID(*arr[i], riid))
                return S_OK;
        }
        return S_FALSE;
    }

編集がすんだら、再度ビルドしてください。
ウィザードでコントロールを作成した際に、プロジェクトのディレクトリ内にコントロールテスト用の HTML が生成されていますので (ここでは SampleCtrl.htm) ビルドが済んだら、このテストページを IE で開いて、以下のように表示されれば OK です。
※IE の設定で Active X コントロールを表示できるようにしておいてください。

IE 上で表示したコントロール
プロパティを追加する

プロパティを追加します。 ここではデフォルトでコントロールに表示されている文字列 "ATL 3.0 : SampleCtrl" をプロパティから設定できるようにします。
最初に IDL へのプロパティの追加と対応する CSampleCtrl のメソッドを追加します。[ClassView] で ISampleCtrl を右クリックし、表示されるポップアップメニューから [プロパティの追加] を選びます。

プロパティを追加

[インターフェイスへプロパティを追加] ダイアログが表示されますので、[プロパティの種類] に BSTR、[プロパティ名] に DisplayString を設定し、[OK] をクリックします。

インターフェイスへプロパティを追加

次に、プロパティ値を保存するための変数メンバを追加します。SampleCtrl.h を開き、 クラス定義に CString の変数 m_DisplayString を追加します。

            (rc.left + rc.right) / 2,
            (rc.top + rc.bottom) / 2,
            pszText,
            lstrlen(pszText));

        return S_OK;
    }

private:
    // プロパティ DisplayString の値を保持する。
    CString m_DisplayString;
};

#endif //__SAMPLECTRL_H_

次に、プロパティ値を m_DisplayString に設定/取得するコードを書きます。
さきほど、プロパティを追加したときに CSampleCtrl.cpp に get_DisplayString()、put_DisplayString() が追加されていますので、ここにコードを追加します。

追加されたメソッド

ここでは以下のように編集しました。

STDMETHODIMP CSampleCtrl::get_DisplayString(BSTR *pVal)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())

    *pVal = m_DisplayString.AllocSysString();

    return S_OK;
}

STDMETHODIMP CSampleCtrl::put_DisplayString(BSTR newVal)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())

    m_DisplayString = newVal;
    // コントロールが変更されたことを設定する。
    SetDirty(TRUE);
    // 再描画したいことをコンテナに通知する。
    FireViewChange();

    return S_OK;
}

次に m_DisplayString を表示するようにデフォルトの描画コードを変更します。
CSampleCtrl.h の OnDraw() を以下のように編集しました。

    HRESULT OnDraw(ATL_DRAWINFO& di)
    {
        RECT& rc = *(RECT*)di.prcBounds;
        Rectangle(di.hdcDraw, rc.left, rc.top, rc.right, rc.bottom);

        SetTextAlign(di.hdcDraw, TA_CENTER|TA_BASELINE);
        // プロパティ値を表示するように変更する。
        //LPCTSTR pszText = _T("ATL 3.0 : SampleCtrl");
        LPCTSTR pszText = m_DisplayString;
        TextOut(di.hdcDraw,
            (rc.left + rc.right) / 2,
            (rc.top + rc.bottom) / 2,
            pszText,
            lstrlen(pszText));

        return S_OK;
    }

これで、プロパティを表示できるようになります。と、いいたい所ですが、まだです。 IE などからプロパティを設定したい場合、IPersistPropertyBag を実装する必要があります。 といっても、ATL では IPersistPropertyBagImpl というデフォルトの実装が提供されていますので、 自分のコントロールクラスでこれを継承するだけで済みます。
CSampleCtrl.h のクラス定義の継承するクラスを記述する部分に

public IPersistPropertyBagImpl<CSampleCtrl> // プロパティを扱う

を追加します。

class ATL_NO_VTABLE CSampleCtrl :
    public CComObjectRootEx<CComSingleThreadModel>,
    public IDispatchImpl<ISampleCtrl, &IID_ISampleCtrl, &LIBID_ATLCTRLSAMPLELib>,
    public CComControl<CSampleCtrl>,
    public IPersistStreamInitImpl<CSampleCtrl>,
    public IOleControlImpl<CSampleCtrl>,
    public IOleObjectImpl<CSampleCtrl>,
    public IOleInPlaceActiveObjectImpl<CSampleCtrl>,
    public IViewObjectExImpl<CSampleCtrl>,
    public IOleInPlaceObjectWindowlessImpl<CSampleCtrl>,
    public ISupportErrorInfo,
    public IConnectionPointContainerImpl<CSampleCtrl>,
    public IPersistStorageImpl<CSampleCtrl>,
    public ISpecifyPropertyPagesImpl<CSampleCtrl>,
    public IQuickActivateImpl<CSampleCtrl>,
    public IDataObjectImpl<CSampleCtrl>,
    public IProvideClassInfo2Impl<&CLSID_SampleCtrl, &DIID__ISampleCtrlEvents, &LIBID_ATLCTRLSAMPLELib>,
    public IPropertyNotifySinkCP<CSampleCtrl>,
    public CComCoClass<CSampleCtrl, &CLSID_SampleCtrl>,
    public IPersistPropertyBagImpl<CSampleCtrl>    // プロパティを扱う
{
public:
    CSampleCtrl()
    {

さらに、クラス定義中の BEGIN_COM_MAP(CSampleCtrl) から END_COM_MAP() の間に

COM_INTERFACE_ENTRY(IPersistPropertyBag)

を追加します。

    COM_INTERFACE_ENTRY(IConnectionPointContainer)
    COM_INTERFACE_ENTRY(ISpecifyPropertyPages)
    COM_INTERFACE_ENTRY(IQuickActivate)
    COM_INTERFACE_ENTRY(IPersistStorage)
    COM_INTERFACE_ENTRY(IDataObject)
    COM_INTERFACE_ENTRY(IProvideClassInfo)
    COM_INTERFACE_ENTRY(IProvideClassInfo2)
    COM_INTERFACE_ENTRY(IPersistPropertyBag)
END_COM_MAP()

最後に、プロパティの名称を定義するため、BEGIN_PROP_MAP(CSampleCtrl) から END_PROP_MAP() の間に

PROP_ENTRY("DisplayString", 1, GetObjectCLSID())

を追加します。 なお、第 2 引数の 1 は、ウィザードにより IDL に追加されたプロパティ DisplayString のプロパティ ID です。

BEGIN_PROP_MAP(CSampleCtrl)
    PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)
    PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4)
    PROP_ENTRY("DisplayString", 1, GetObjectCLSID())
    // Example entries
    // PROP_ENTRY("Property Description", dispid, clsid)
    // PROP_PAGE(CLSID_StockColorPage)
END_PROP_MAP()

ビルドが済んだら、プロパティ値を設定するようにコントロールテスト用の HTML (SampleCtrl.htm) を以下のように編集します。

<HTML>
<HEAD>
<TITLE>オブジェクト SampleCtrl 用 ATL 3.0 テスト ページ</TITLE>
</HEAD>
<BODY>
<OBJECT ID="SampleCtrl" CLASSID="CLSID:B94F2CEA-B317-4CEC-8448-B9E733BE663A">
<PARAM NAME="DisplayString" VALUE="こんにちわ">
</OBJECT>
</BODY>
</HTML>

編集が済んだら、テストページを IE で開いてください。以下のように表示されれば OK です。

IE で表示したコントロール

なお、プロパティを実装すると、以下のようなダイアログが出始めるようになります。

警告ダイアログ

これは、コントロールが安全であるとマーキングされていないためです。 これをマークするために IObjectSafety を実装します。ATL には IObjectSafetyImpl が提供されていますので、 通常は、これをコントロールクラスで継承するだけです。
CSampleCtrl.h のクラス定義の継承するクラスを記述する部分に

public IObjectSafetyImpl<CSampleCtrl, INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA>

を追加します。ここでは、スクリプトの安全性と初期化時のデータの安全性を保証しています。

class ATL_NO_VTABLE CSampleCtrl :
    public CComObjectRootEx<CComSingleThreadModel>,
    public IDispatchImpl<ISampleCtrl, &IID_ISampleCtrl, &LIBID_ATLCTRLSAMPLELib>,
    public CComControl<CSampleCtrl>,
    public IPersistStreamInitImpl<CSampleCtrl>,
    public IOleControlImpl<CSampleCtrl>,
    public IOleObjectImpl<CSampleCtrl>,
    public IOleInPlaceActiveObjectImpl<CSampleCtrl>,
    public IViewObjectExImpl<CSampleCtrl>,
    public IOleInPlaceObjectWindowlessImpl<CSampleCtrl>,
    public ISupportErrorInfo,
    public IConnectionPointContainerImpl<CSampleCtrl>,
    public IPersistStorageImpl<CSampleCtrl>,
    public ISpecifyPropertyPagesImpl<CSampleCtrl>,
    public IQuickActivateImpl<CSampleCtrl>,
    public IDataObjectImpl<CSampleCtrl>,
    public IProvideClassInfo2Impl<&CLSID_SampleCtrl, &DIID__ISampleCtrlEvents, &LIBID_ATLCTRLSAMPLELib>,
    public IPropertyNotifySinkCP<CSampleCtrl>,
    public CComCoClass<CSampleCtrl, &CLSID_SampleCtrl>,
    public IPersistPropertyBagImpl<CSampleCtrl>,    // プロパティを扱う
    public IObjectSafetyImpl<CSampleCtrl, INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA>
{
public:
    CSampleCtrl()
    {

さらに、クラス定義中の BEGIN_COM_MAP(CSampleCtrl) から END_COM_MAP() の間に

COM_INTERFACE_ENTRY(IObjectSafety)

を追加します。

    COM_INTERFACE_ENTRY(IConnectionPointContainer)
    COM_INTERFACE_ENTRY(ISpecifyPropertyPages)
    COM_INTERFACE_ENTRY(IQuickActivate)
    COM_INTERFACE_ENTRY(IPersistStorage)
    COM_INTERFACE_ENTRY(IDataObject)
    COM_INTERFACE_ENTRY(IProvideClassInfo)
    COM_INTERFACE_ENTRY(IProvideClassInfo2)
    COM_INTERFACE_ENTRY(IPersistPropertyBag)
    COM_INTERFACE_ENTRY(IObjectSafety)
END_COM_MAP()
プロパティページの追加

プロパティページとは VB などからコントロールを利用する際に、 プロパティ値をダイアログから入力できるようにしたものです。
下は VB でカレンダーコントロールのプロパティページを表示した例です。

カレンダーコントロールのプロパティページ

メニューから [挿入] - [ATL オブジェクトの新規作成] を選択し [ATL オブジェクトウィザード] を表示します。
[カテゴリ] にコントロール、[オブジェクト] にプロパティページを選択し、[次へ] をクリックします。

ATL オブジェクトウィザード

表示される [ATL オブジェクトウィザードのプロパティ] の [名前] タブで [ショートネーム] を適当に (ここでは SampleCtrlPropPage としました) 入力し、[ストリング] タブで [タイトル] に サンプルプロパティ、[ドキュメントの文字] に サンプルのプロパティページ、 [ヘルプファイル] を空に設定し、[OK] をクリックします。
※[アトリビュート] はデフォルトのままです。

ATL オブジェクトウィザードのプロパティ - 名前 ATL オブジェクトウィザードのプロパティ - ストリング

プロジェクトにプロパティページ用のダイアログとクラスが挿入されます。
挿入されたダイアログをみてみます。[ResourceView] で [Dialog] ツリーの下の IDD_SAMPLECTRLPROPPAGE を開いてください。
プロパティページ用のダイアログが追加されています。 ちなみに、プロパティページでは 1 つのダイアログが実行時に表示されるプロパティページの 1 つタブに相当します。 例えば、上のカレンダーコントロールのプロパティページのように [全般]、[フォント]、[色] の 3 つのタブを作成したい場合は、3 つのプロパティページ (ダイアログ) を作成する必要があります。

ダイアログエディタでの表示

これを編集して、DisplayString 用のテキストボックスを作成します。
作成したテキストボックスの ID は IDC_DISPLAY_STRING にしました。

テキストボックスとプロパティ

次にプロパティ DisplayString の内容をダイアログから編集できるようにコードを書きます。
[ClassView] から CSampleCtrlPropPage を右クリックし、表示されるポップアップメニューから [Windows メッセージハンドラの追加] を選択します。

Windows メッセージハンドラの追加

表示された [CSampleCtrlPropPage クラスに新規ウィンドウメッセージとイベントハンドラを追加] ダイアログで、 [クラスで使用可能にするメッセージのフィルタ] に ダイアログ を選択し、[新規ウィンドウメッセージ/イベント] で WM_INITDIALOG をダブルクリックして [既存のメッセージ/イベントハンドラ] へ移動させ、[OK] をクリックします。

CSampleCtrlPropPage クラスに新規ウィンドウメッセージとイベントハンドラを追加

追加された OnInitDialog() を以下のように編集します。

    LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        AFX_MANAGE_STATE(AfxGetStaticModuleState());

        if (m_nObjects <= 0) {
            // 当プロパティページに関連付けされているオブジェクトがない場合、
            return 0;
        }

        // プロパティの値を CSampleCtrl から取得し、
        // ダイアログ上のコントロールに反映する。
        CComQIPtr<ISampleCtrl, &IID_ISampleCtrl> pSampleCtrl(m_ppUnk[0]);

        CComBSTR displayString;

        if (SUCCEEDED(pSampleCtrl->get_DisplayString(&displayString.m_str))) {
            SetDlgItemText(IDC_DISPLAY_STRING, CString(displayString));
        }

        // 変更フラグを初期化しておく。
        SetDirty(FALSE);

        return 0;
    }

次にプロパティページで [適用] または [OK] がクリックされたときのハンドラ Apply() を以下のように編集します。

    STDMETHOD(Apply)(void)
    {
        AFX_MANAGE_STATE(AfxGetStaticModuleState());

        // ダイアログコントロールの値を、当プロパティページに
        // 関連付けられている CSampleCtrl オブジェクトすべてに反映する。
        BOOL bFailure = FALSE;

        for (UINT i = 0; i < m_nObjects; i++) {

            CComQIPtr<ISampleCtrl, &IID_ISampleCtrl> pSampleCtrl(m_ppUnk[i]);

            CComBSTR displayString;

            GetDlgItemText(IDC_DISPLAY_STRING, displayString.m_str);
            if (FAILED(pSampleCtrl->put_DisplayString(displayString))) {
                bFailure = TRUE;
            }
        }

        if (bFailure) {
            // 失敗があったときは、エラー情報を取得し表示する。
            CComPtr<IErrorInfo> pError;
            CComBSTR strError;
            GetErrorInfo(0, &pError);
            pError->GetDescription(&strError);
            MessageBox(CString(strError), _T("プロパティの保存に失敗しました。"), MB_OK | MB_ICONEXCLAMATION);
            return E_FAIL;
        }

        m_bDirty = FALSE;
        return S_OK;
    }

テキストボックスが変更されたときに変更フラグを設定するようにコードを追加します。
[ClassView] から CSampleCtrlPropPage を右クリックし、表示されるポップアップメニューから [Windows メッセージハンドラの追加] を選択します。
表示される [CSampleCtrlPropPage クラスに新規ウィンドウメッセージとイベントハンドラを追加] ダイアログで、 [クラスまたはオブジェクト] から IDC_DISPLAY_STRING を選択し、[新規ウィンドウメッセージ/イベント] に テキストボックス用のメッセージが表示されたら、その中から EN_CHANGE をダブルクリックします。
[メンバ関数の追加] ダイアログが表示されるので、[メンバ関数名] に OnChangeDisplayString と入力し、 [OK] をクリックします。

メンバ関数の追加

[既存のメッセージ/イベントハンドラ] に EN_CHANGE が移動するので、そのまま [OK] をクリックします。

CSampleCtrlPropPage クラスに新規ウィンドウメッセージとイベントハンドラを追加

追加された OnChangeDisplayString() を以下のように編集します。

    LRESULT OnChangeDisplayString(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
    {
        AFX_MANAGE_STATE(AfxGetStaticModuleState());
        SetDirty(TRUE);
        return 0;
    }

CSampleCtrl のプロパティとこのプロパティページを関連付けるために、CSampleCtrl のクラス定義の BEGIN_PROP_MAP() から END_PROP_MAP の間の

PROP_ENTRY("DisplayString", 1, GetObjectCLSID())

を、

PROP_ENTRY("DisplayString", 1, CLSID_SampleCtrlPropPage)

のように書き換えます。書き換えが済んだらビルドしてください。
ビルドが済んだら、さっそくコントロールをテストしてみます。ここでは、VB を使っています。
作成したコントロールをフォームに載せてプロパティページを表示し、DisplayString の値を変更し、 適用をクリックしてみてください。コントロールに設定した文字列が変更されるのが確認できると思います。

プロパティページ
VB デザイナでの表示
接続ポイントの追加

最初に接続ポイントを使ってイベント通知する際に使用する ATL オブジェクトを完成させます。
イベント通知で使用するオブジェクトとは、先にコントロールの作成 で定義だけしてあった SampleParam です。
SampleParam のプロパティとしてイベント通知で通知したい内容を定義します。何でも良いのですが、ここでは、 文字列を渡すようにしたいと思いますので、文字列を保持する Msg プロパティを追加します。
[ClassView] で ISampleParam を右クリックし、表示されるポップアップメニューから [プロパティの追加] を選びます。 [インターフェイスへプロパティを追加] ダイアログが表示されるので、[プロパティの種類] に BSTR、[プロパティ名に] Msg と設定し、[OK] をクリックしてください。

インターフェイスへプロパティを追加

CSampleParam のクラス定義にプロパティ Msg の内容を保持するメンバ m_Msg を定義します。

    // プロパティ Msg の内容を保持する。
    CString m_Msg;
};

#endif //__SAMPLEPARAM_H_

ウィザードによって追加された get_Msg、put_Msg の内容を以下のように編集します。

STDMETHODIMP CSampleParam::get_Msg(BSTR *pVal)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())

    *pVal = m_Msg.AllocSysString();

    return S_OK;
}

STDMETHODIMP CSampleParam::put_Msg(BSTR newVal)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())

    m_Msg = newVal;

    return S_OK;
}

これで、SampleParam は完成です。
次に接続ポイントを追加します。[ClassView] で _ISampleCtrlEvents を右クリックし、 表示されるポップアップメニューから [メソッドの追加] を選択します。
表示される [インターフェイスへメソッドを追加] で [メソッド名] に SampleMethod、 パラメータに [in] ISampleParam* sampParam を設定し、[OK] をクリックします。

インターフェイスへメソッドを追加

追加したら、ビルドしてください。エラーがなければ、[ClassView] で CSampleCtrl を右クリックし、 表示されるポップアップメニューから [接続ポイントのインプリメント] を選択します。
[コネクションポイントのインプリメント] ダイアログが表示されるので、 [インターフェイス] に表示されている _ISampleCtrlEvents にチェックを入れ、[OK] をクリックします。

コネクションポイントのインプリメント

追加が済んだら、再度ビルドしてください。環境によるのかもしれませんが、ここで

c:\documents and settings\bono\my documents\visual studio projects\atlctrlsample\samplectrl.h(83) :
 error C2065: 'IID__ISampleCtrlEvents' : 定義されていない識別子です。
c:\documents and settings\bono\my documents\visual studio projects\atlctrlsample\samplectrl.h(83) :
 error C2440: 'static_cast' : 'class CSampleCtrl *' から 'class ATL::_ICPLocator *' に変換することはできません。
 (新しい動作 ; ヘルプを参照)
        指示された型は関連がありません; 変換には reinterpret_cast、 C スタイル キャストまたは関数スタイルのキャストが必要です。
c:\documents and settings\bono\my documents\visual studio projects\atlctrlsample\samplectrl.h(83) :
 fatal error C1903: 直前のエラーを修復できません; コンパイルを中止します。

のようなエラーが出る場合、CSampleCtrl クラスの定義内の

BEGIN_CONNECTION_POINT_MAP(CSampleCtrl)
    CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink)
    CONNECTION_POINT_ENTRY(IID__ISampleCtrlEvents)
END_CONNECTION_POINT_MAP()

BEGIN_CONNECTION_POINT_MAP(CSampleCtrl)
    CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink)
    CONNECTION_POINT_ENTRY(DIID__ISampleCtrlEvents)
END_CONNECTION_POINT_MAP()

のように (IID__ISampleCtrlEvents の頭に D を付加する) 編集してください。ビルドできるようになります。
ちなみに、これはウィザードの問題のような気もするのですが、私の探し方が悪いのか、 マイクロソフトのサポート技術情報での報告を見つけることができませんでした。

メソッドの追加

接続ポイントのテストを兼ねて、コントロールにメソッドを追加し、 メソッドのコードから接続ポイントを使ってイベントを発生させます。
まず、コントロールにメソッドを追加します。
[ClassView] で ISampleCtrl を右クリックし、ポップアップメニューからメソッドの追加を選択します。
[インターフェイスへメソッドを追加] ダイアログが表示されるので、[メソッド名] に MyCallMethod と設定し、 [OK] をクリックします。

インターフェイスへメソッドを追加

次に、イベント通知する際に使用する ISampleParam インタフェースポインタを生成/破棄するコードを追加します。
CSampleCtrl のクラス定義に

    ISampleParam* m_pSampleParam;

を追加します。

    // ISampleParam を保持する。
    ISampleParam* m_pSampleParam;
};

#endif //__SAMPLECTRL_H_

CSampleCtrl のコンストラクタを以下のように編集します。

    CSampleCtrl() :
        m_pSampleParam(0)
    {
    }

CComObjectRootEx::FinalConstruct() をオーバーライドし、以下のように追加します。

    HRESULT FinalConstruct()
    {
        AFX_MANAGE_STATE(AfxGetStaticModuleState());

        // イベント通知に使用するインタフェースを作成する。
        HRESULT hr;
        hr = CoCreateInstance(CLSID_SampleParam,
                         0,
                         CLSCTX_INPROC_SERVER,
                         IID_ISampleParam,
                         (void **) &m_pSampleParam);
        if (FAILED(hr)) {
            TRACE("FAILURE: FinalConstruct() - CoCreateInstance(): IID_ISampleParam\n");
        }

        return hr;
    }

CComObjectRootEx::FinalRelease() をオーバーライドし、以下のように追加します。

    void FinalRelease()
    {
        AFX_MANAGE_STATE(AfxGetStaticModuleState());

        // イベント通知に使用するインタフェースを解放する。
        if (m_pSampleParam) {
            m_pSampleParam->Release();
        }
    }

ウィザードにより CSampleCtrl に追加された MyCallMethod を以下のように編集します。

STDMETHODIMP CSampleCtrl::MyCallMethod()
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())

    CString msg = _T("はろー");
    if (m_pSampleParam) {
        m_pSampleParam->put_Msg(msg.AllocSysString());
        Fire_SampleMethod(m_pSampleParam);
    }

    return S_OK;
}

編集が済んだら、ビルドしてください。
ビルドが済んだら、またまた VB を使ってテストしてみます。 フォームにコントロール (SampleCtrl1) とボタン (Command1) を追加します。 ボタンをダブルクリックして Command1_Click イベントを、コントロールをダブルクリックして SampleCtrl1_SampleMethod イベントを追加し、以下のように編集します。

Private Sub Command1_Click()
    SampleCtrl1.MyCallMethod
End Sub

Private Sub SampleCtrl1_SampleMethod(ByVal sampParam As ATLCTRLSAMPLELibCtl.ISampleParam)
    MsgBox sampParam.Msg
End Sub

実行して Command1 をクリックし、"はろー" とメッセージボックスに表示されれば OK です。

VB での実行例
スレッドの実装

コントロールのメソッドの中で時間のかかる処理を実行したいことがあります。
メソッド内で時間のかかる処理を行うと、コンテナ側 (テストで使った VB や IE などコントロールの親となるアプリケーション) のメッセージループが回らなくなり、UI が固まってしまいます。
試しに、CSampleCtrl::MyCallMethod() の中に適当な Sleep() を入れて、さきほどの "はろー" を表示させるテストプログラムを実行してみるとわかります。 MyCallMethod() を呼び出したとたん UI が応答しなくなるのがわかると思います。

MyCallMethod() の中でメッセージループをまわすことで、応答しなくなるのを防ぐこともできますが、 例えば MyCallMethod() が UI とまったく関係ない処理をするメソッドだった場合、 コードの中に UI のためのメッセージループが紛れ込むのはあまり美しくなく、うれしいことではないと思います。

ということで、スレッド化してみます。スレッド化するにあたり注意することは、

  • スレッドのはじまりで CoInitialize() を呼び、終わりで CoUninitialize() を呼ぶこと。
  • スレッドにインタフェースポインタを渡すときはマーシャリングすること。
  • コンテナ (コントロールの親となるアプリケーション) が終了した際にスレッドを正常に終了できるようにしておくこと。

の 3 点でしょうか。
なお、以下のサンプルでは時間のかかる処理の代わりに Sleep() を使っています。

まず、スレッドとして動作する静的メソッドをクラスに追加します。 [ClassView] で CSampleCtrl を右クリックし、ポップアップメニューで、[メンバ関数の追加] を選びます。 [メンバ関数の追加] ダイアログで、[関数の型] に UINT、[関数の宣言] に TestThread(VOID *pParam)、 [Static] にチェックを入れ、[OK] をクリックしてください。
※ここでは、AfxBeginThread() を使ってスレッドを作成するため、スレッド関数のプロトタイプは

UINT MyControllingFunction(LPVOID pParam);

のようになります。

メンバ関数の追加

次に、スレッド側に渡すインタフェースをマーシャリングするのに使用する IStream へのポインタ変数を CSampleCtrl のメンバ変数として追加します。

    // ISampleParam マーシャリング用
    IStream* m_IStreamSampleParam;
    // 接続ポイントマーシャリング用
    CArray<IStream*, IStream*> m_IStreamDispatchArray;
};

#endif //__SAMPLECTRL_H_

また、CArray を使用するために StdAfx.h に

#include <afxtempl.h>

を追加します。

#include <atlcom.h>
#include <atlctl.h>
#include <afxtempl.h>

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ は前行の直前に追加の宣言を挿入します。

#endif // !defined(AFX_STDAFX_H__55503BB6_11A0_4AE2_ABC9_C341A38E43F7__INCLUDED)

次に、コンテナ終了時にスレッドがまだ動作していた場合、スレッドの終了を指示するフラグと、 スレッドの終了を待ち合わせするために、スレッドハンドルを退避しておく変数を CSampleCtrl のメンバ変数として追加します。

    // 終了フラグ
    BOOL m_bAbort;
    // スレッド終了待ち合わせ監視用
    HANDLE m_hThread;
};

#endif //__SAMPLECTRL_H_

上記で追加したメンバ変数の初期化を CSampleCtrl のコンストラクタに追加します。

    CSampleCtrl() :
        m_pSampleParam(0),
        m_IStreamSampleParam(0),
        m_bAbort(FALSE),
        m_hThread(0)
    {

MyCallMethod を以下のように編集します。

STDMETHODIMP CSampleCtrl::MyCallMethod()
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())

    HRESULT hr;
    IStream* pStream;

    try {

        if (m_hThread) {
            // 以前に退避したスレッドハンドルが残っていれば削除する。
            CloseHandle(m_hThread);
            m_hThread = 0;
        }

        //
        // 接続インタフェースをマーシャリングし、m_IStreamDispatchArray に格納する。
        //
        m_IStreamDispatchArray.RemoveAll();

        CProxy_ISampleCtrlEvents<CSampleCtrl>* pT = static_cast<CProxy_ISampleCtrlEvents<CSampleCtrl>*>(this);
        int nConnectionIndex;
        int nConnections = pT->m_vec.GetSize();

        // 接続インタフェースをすべて取得し、マーシャリング後、配列に格納する。
        for (nConnectionIndex = 0; nConnectionIndex < nConnections; nConnectionIndex++) {
            Lock();
            CComPtr<IUnknown> sp = pT->m_vec.GetAt(nConnectionIndex);
            Unlock();
            IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p);
            if (pDispatch != NULL) {
                // 取得した IDispatch を IStream へ変換し格納する。
                pStream = 0;
                hr = CoMarshalInterThreadInterfaceInStream(IID_IDispatch,
                            pDispatch, (IStream**) &pStream);
                if (FAILED(hr)) {
                    TRACE("FAILURE: CSampleCtrl::MyCallMethod() - CoMarshalInterThreadInterfaceInStream(): IID_IDispatch\n");
                } else {
                    ASSERT(pStream);
                    m_IStreamDispatchArray.Add(pStream);
                }
            }
        }

        //
        // ISampleParam インタフェースをマーシャリングし、格納する。
        //
        m_IStreamSampleParam = 0;

        if (m_pSampleParam) {
            // マーシャリングして格納する。
            pStream = 0;
            hr = CoMarshalInterThreadInterfaceInStream(IID_ISampleParam,
                        m_pSampleParam, (IStream**) &pStream);
            if (FAILED(hr)) {
                TRACE("FAILURE: CSampleCtrl::MyCallMethod() - CoMarshalInterThreadInterfaceInStream(): IID_ISampleParam\n");
            } else {
                ASSERT(pStream);
                m_IStreamSampleParam = pStream;
            }
        }

        //
        // スレッドを作成し、開始する。
        //

        // スレッドを作成する。
        CWinThread* pThread = AfxBeginThread(
                        TestThread,
                        this,
                        THREAD_PRIORITY_NORMAL,
                        0,
                        CREATE_SUSPENDED,
                        0);
        if (!pThread) {
            return Error(_T("スレッドを作成できませんでした。"));
        }

        // スレッド終了待ち合わせのための、スレッドハンドルを複製する。
        DuplicateHandle(
            GetCurrentProcess(),    // 元プロセス
            pThread->m_hThread,        // オリジナルハンドル
            GetCurrentProcess(),    // 先プロセス
            &m_hThread,                // 複製したハンドルがコピーされる
            0,                        // アクセス権(無視される)
            FALSE,                    // 継承しない
            DUPLICATE_SAME_ACCESS    // 複製先ハンドルは、複製元ハンドルと同じアクセス権を持つ
        );

        m_bAbort = FALSE;

        // スレッドを開始する。
        pThread->ResumeThread();


    } catch (CException* e) {
        e->Delete();
        return Error(_T("致命的なエラーが発生しました。"));
    }

    return S_OK;
}

オーバーライドした FinalRelease() 以下のように編集します。

    void FinalRelease()
    {
        AFX_MANAGE_STATE(AfxGetStaticModuleState());

        // スレッドの終了状態待ち合わせ用の
        // 複製されたスレッドハンドルがあれば、
        // 待ち合わせを行ったあと、削除する。
        try {
            m_bAbort = TRUE;
            if (m_hThread) {
                // NOTE: タイムアウトを INFINITE にする勇気がないので、
                //       適当な時間を設定している。
                WaitForSingleObject(m_hThread, 10000);
                CloseHandle(m_hThread);
            }
        } catch (CException* e) {
            e->Delete();
        }

        // イベント通知に使用するインタフェースを解放する。
        if (m_pSampleParam) {
            m_pSampleParam->Release();
        }
    }

最後に TestThread() を以下のように編集します。

UINT CSampleCtrl::TestThread(VOID *pParam)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());

    // COM ライブラリを初期化し、新たなアパートメントスレッドに入る。
    if (FAILED(CoInitialize(0))) {
        return 1;
    }

    // クラスインスタンスへのポインタを取り出す。
    CSampleCtrl* pSampleCtrl = (CSampleCtrl*) pParam;
    if (!pSampleCtrl) {
        return 1;
    }

    // 時間のかかる処理をする。
    for (int i = 0; i < 10; ++i) {
        if (pSampleCtrl->m_bAbort) {
            break;
        }
        Sleep(1000);
    }

    if (!pSampleCtrl->m_bAbort) {
        // コンテナ終了時以外は通知する。
        HRESULT hr;
        IStream* pStream;
        int i;

        //
        // コンテナにイベント通知する。
        //
        // アンマーシャリング後の接続ポイントを格納する。
        CArray<IDispatch*, IDispatch*> dispatchArray;

        // アンマーシャリング後の ISampleParam インタフェースを格納する。
        ISampleParam* pSampleParam = 0;

        //
        // 接続インタフェースをアンマーシャリングし配列に格納する。
        //
        for (i = 0; i < pSampleCtrl->m_IStreamDispatchArray.GetSize(); ++i) {
            pStream = pSampleCtrl->m_IStreamDispatchArray[i];
            IDispatch* pDispatch = 0;
            hr = CoGetInterfaceAndReleaseStream(pStream,
                    IID_IDispatch, (void**) &pDispatch);
            if (FAILED(hr)) {
                TRACE("FAILURE: CSampleCtrl::TestThread() - CoGetInterfaceAndReleaseStream(): IID_IDispatch\n");
            } else {
                ASSERT(pDispatch);
                dispatchArray.Add(pDispatch);
            }
        }

        //
        // ISampleParam インタフェースをアンマーシャリングし、格納する。
        //
        pSampleParam = 0;

        pStream = pSampleCtrl->m_IStreamSampleParam;
        hr = CoGetInterfaceAndReleaseStream(pStream,
                    IID_ISampleParam, (void**) &pSampleParam);
        if (FAILED(hr)) {
            TRACE("FAILURE: CSampleCtrl::TestThread() - CoGetInterfaceAndReleaseStream(): IID_IResultEndPrint\n");
        } else {
            ASSERT(pSampleParam);
        }


        // ISampleParam インタフェースに値を設定しイベントを発生する。
        if (pSampleParam) {
            CComVariant varResult;
            CString msg = _T("ハロー");
            pSampleParam->put_Msg(msg.AllocSysString());

            CComVariant* pvars = new CComVariant[1];
            for (i = 0; i < dispatchArray.GetSize(); ++i) {
                VariantClear(&varResult);
                IDispatch* pDispatch = dispatchArray[i];
                pvars[0] = pSampleParam;
                DISPPARAMS disp = { pvars, NULL, 1, 0 };
                pDispatch->Invoke(0x1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &disp, &varResult, NULL, NULL);
            }
            delete[] pvars;
            // varResult.scode の値は捨てる。
        }
    }

    // COM ライブラリを解放する。
    CoUninitialize();

    return 0;
}

コンテナ終了時に通知しないようにしているのは、タイミングによっては、親側のインスタンスが消滅している可能性があり、 通知を行うことによってアクセス違反が起きるためです。
編集が済んだら、ビルドしてさきほどのように VB などからテストしてみてください。
UI の応答が固まることもなく、かつ、スレッド処理が終了したら、イベントが発生するのが確認できると思います。

なお、このサンプルではスレッド内のフラグの排他処理やスレッドの二重起動などには一切対処していません。 実際に業務で使用する場合は、それらを考慮する必要があると思われますので、お気をつけください。

※上のサンプルを HTML に埋め込み、スレッドから発生させたイベント通知の中で javascript などから window.close() を行うと、時々、異常終了してしまうことを確認しています。
原因はよく分かりませんが、アクセス違反が原因のようですので、スレッド終了前に、スレッドを起動した側のインスタンスが 無効になっているのだと思います。
javascript の setTimeout() を使い、setTimeout("exitfunc", 2000) のようにして、 イベントハンドラの制御をコントロールに戻してから、exitfunc() 内で window.close() するようにすれば、 スレッドをなんとか正常終了させることができるようで、アクセス違反は起こらなくなります。 ただこれは、スレッドの終了タイミングに依存した回避策なので完全ではありませんし、 本来はコントロール側で対処するべきなのですが、根本的な解決策を見つけられていません。 もし、HTML に埋め込んで利用になる場合は十分ご注意ください。

サンプルソース
atlctrlsample.zip (35.2 KB)