EternalWindows
ActiveX コントロール / コントロールの実装

今回から実際にActiveXコントロールを開発する方法について説明します。 これまで述べてきたように、ActiveXコントロールの正体はコンテナに埋め込まれるOLEオブジェクトであり、 通常はDLL(インプロセスサーバー)の形で作成されます。 MFCでActiveXコントロールを開発した場合は拡張子がocxとして出力されますが、 これはそのファイルがActiveXコントロールであることを明確にしたいという意図であり、 決して拡張子がdllであってはならないというわけではありません。 どちらの拡張子を選ぶかは開発者の自由といえるでしょう。 次に、今回作成することになるコントロールの図を示します。

上図は、IE上で今回作成したコントロールを表示した結果です。 灰色の領域は、コントロールのウインドウのクライアント領域であり、 ウインドウクラスの背景色にGRAY_BRUSHを指定したため、上図のように表示されています。 また、描画されている楕円についてはWM_PAINTでEllipseを呼び出しただけであり、 特に複雑なことは行っていません。

コンテナを開発した際にはコントロールをIOleObjectなどで識別していましたが、 このようなコントロールを識別するインターフェースが、コントロールの実装できるインターフェースということになります。 次に、コントロールが実装することのできるインターフェースの一部を示します。

インターフェース 説明
IOleObject コンテナに埋め込まれるオブジェクトは、このインターフェースを実装しておかなければならない。
IOleInPlaceObject インプレースアクティベーションをサポートする場合に実装する。
IOleControl コンテナの環境プロパティの変更やアクセラレータキーの入力を受信したい場合に実装する。
IDataObject コントロールの外観をメタファイルで返したい場合や、内部データが変化した際に通知を送りたい場合に実装する。
IViewObject2 OleDrawの呼び出しを成功させたい場合や、コントロールのイメージが変化した際に通知を送りたい場合に実装する。
IDispatch コントロールのプロパティやメソッドをコンテナが呼び出せるようにしたい場合に実装する。
IConnectionPointContainer コントロール内で発生したイベントをコンテナに通知したい場合に実装する。 別途、IConnectionPointを実装した接続ポイントが必要になる。
IProvideClassInfo2 コントロールのタイプライブラリをコンテナに公開したい場合に実装する。 タイプライブラリを確認できれば、コントロールのプロパティやメソッドを動的に理解できる。
ISpecifyPropertyPages プロパティダイアログの表示をサポートする場合に実装する。 ページ毎にIPropertyPageを実装したオブジェクトが必要になる。
IRunnableObject コントロールの実行状態を管理したい場合に実装する。
IQuickActivate コントロールの初期化に必要な作業を一括して行う場合に実装する。
IObjectSafety コントロールが安全であるとマークしたい場合に実装する。
永続記憶インターフェース コンテナが何らかのデータを維持するのであれば、 IPersistStreamかIPersistStreamInitは必ず実装しなければならない。 オプションとして、IPersistMemory、IPersistStorage、IPersistPropertyBag、IPersistMonikerも実装できる。

コントロールが実装できるインターフェースは数多く存在しますが、 IOleObjectとIOleInPlaceObjectを実装していれば特に問題なく動作するように思えます。 一応、今回作成するコントロールは、 IDispatch、IQuickActivate、IPersistStreamInit、IPersistPropertyBag、IObjectSafetyも実装しています。

オブジェクトがOLEコントロールであることを示すIOleControlは、 ActiveXコントロールを開発する場合でも基本的に実装しておいたほうがよいでしょう。 次に、各メソッドの処理を順に示します。

STDMETHODIMP CActiveXControl::GetControlInfo(CONTROLINFO *pCI)
{
	return E_NOTIMPL;
}

GetControlInfoは、コンテナがコントロールのアクセラレータ情報を取得する際に呼ばれます。 ここでアクセラレータ情報を返すということは、コントロールがアクセラレータキーの入力を検出したいということですが、 それならばメッセージループでTranslateAcceleratorを呼び出せば済む話ではないでしょうか。 確かにそうとも言えるのですが、これだけではコンテナがフォーカスを持っている場合に入力の検出ができなくなるため、 コンテナがフォーカスを持っている場合でも検出したいアクセラレータ情報をGetControlInfoで返すようにするのです。 今回は検出したいアクセラレータ情報がないため、E_NOTIMPLを返すようにしています。

STDMETHODIMP CActiveXControl::OnMnemonic(MSG *pMsg)
{
	return S_OK;
}

OnMnemonicは、GetControlInfoで返したアクセラレータキーがコンテナ上で検出された場合に呼ばれます。 GetControlInfoでE_NOTIMPLを返した場合は、このメソッドでもE_NOTIMPLを返すはずです。

STDMETHODIMP CActiveXControl::OnAmbientPropertyChange(DISPID dispID)
{
	return S_OK;
}

OnAmbientPropertyChangeは、コンテナのアンビエントプロパティが変更された場合に呼ばれます。 引数のdispIDは変更されたアンビエントプロパティを識別しており、 これをIDispatch::Invokeに指定すれば変更後の新しいアンビエントプロパティを取得できます。 IDispatchに関しては、IOleObject::SetClientSiteに渡されたインターフェースから取得すればよいでしょう。 実際にIEをコンテナにしてみて分かったことですが、IEはOnAmbientPropertyChangeを呼び出すことが滅多にないように思えます。 たとえば、「表示」メニューから「文字のサイズ」を選択してフォントを変更した場合は、 DISPID_AMBIENT_FONTを引数としたOnAmbientPropertyChangeが呼ばれると思ったのですが、 そのようなことはないようです。 今回のコントロールは、コンテナのアンビエントプロパティに関心がないため特に何も行っていません。

STDMETHODIMP CActiveXControl::FreezeEvents(BOOL bFreeze)
{
	return E_NOTIMPL;
}

FreezeEventsは、コントロールがイベントを生成することを中断または再開させるために呼ばれます。 コンテナはIConnectionPoint::FindConnectionPointを呼び出して、 特定のIIDに関連するイベントの通知を受け取ることができるわけですが、 FreezeEvents(TRUE)が呼ばれた場合はFreezeEvents(FALSE)が呼ばれるまで通知を送るべきではないと思われます。 今回はIConnectionPointを実装しておらず、コンテナに通知するイベントもないということで、 このメソッドを考慮する必要はありません。

コンテナを開発する際に説明したように、 コンテナはコントロールのインプレースアクティベーションを行う前に次のメソッドを呼び出します。

STDMETHODIMP CActiveXControl::GetMiscStatus(DWORD dwAspect, DWORD *pdwStatus)
{
	*pdwStatus = OLEMISC_ACTIVATEWHENVISIBLE | OLEMISC_INSIDEOUT;

	return S_OK;
}

STDMETHODIMP CActiveXControl::SetClientSite(IOleClientSite *pClientSite)
{
	if (m_pClientSite != NULL)
		m_pClientSite->Release();

	m_pClientSite = pClientSite;

	if (m_pClientSite != NULL)
		m_pClientSite->AddRef();

	return S_OK;
}

GetMiscStatusは、コントロールの情報ステータスを返すようにします。 OLEMISC_ACTIVATEWHENVISIBLEを返せば、コントロールは直ちにインプレースアクティベーションされるように思えますが、 これだけではIOleObject::GetExtentで返した範囲にカーソルが入った場合に、 インプレースアクティベーションが行われるようです。 OLEMISC_INSIDEOUTも共に指定することで、直ちにインプレースアクティベーションが行われます。 SetClientSiteは、コンテナのインターフェースが引数として渡されます。 これは後で必要になるのでメンバ変数に保存しておきます。 ちなみに、IEではIOleClientSite::GetContainerで取得できるインターフェースからIHTMLDocument2を照会できます。

コンテナはGetMiscStatusやSetClientSiteを直接呼び出すこともできますが、 これらをIQuickActivate::QuickActivateで一括して行いたいと思うこともあります。

HRESULT CActiveXControl::QuickActivate(QACONTAINER *pQaContainer, QACONTROL *pQaControl)
{
	GetMiscStatus(DVASPECT_CONTENT, &pQaControl->dwMiscStatus);
	SetClientSite(pQaContainer->pClientSite);
	pQaControl->dwViewStatus = 0;
	pQaControl->dwEventCookie = 0;
	pQaControl->dwPropNotifyCookie = 0;
	pQaControl->dwPointerActivationPolicy = 0;

	return S_OK;
}

QuickActivateでは、QACONTROL構造体にコントロールの情報を返す必要があります。 よって、GetMiscStatusやSetClientSiteを呼び出すだけでなく、 QACONTROL構造体のメンバも初期化する必要があります。 基本的には0で問題ありませんが、コンテナにイベントの通知を行う場合はdwEventCookieを調整します。

コントロールの初期化を終えたコンテナは、コントロールをインプレースアクティベーションするためにIOleObject::DoVerbを呼び出します。

STDMETHODIMP CActiveXControl::DoVerb(LONG iVerb, LPMSG lpmsg, IOleClientSite *pActiveSite, LONG lindex, HWND hwndParent, LPCRECT lprcPosRect)
{
	if (iVerb == OLEIVERB_INPLACEACTIVATE){
		if (!InPlaceActivate(hwndParent, (LPRECT)lprcPosRect))
			return E_FAIL;
		m_nState = READYSTATE_COMPLETE;
	}
	else if (iVerb == OLEIVERB_HIDE)
		InPlaceDeactivate();
	else
		;

	return S_OK;
}

iVerbがOLEIVERB_INPLACEACTIVATEである場合はインプレースアクティベーションを行うということなので、 InPlaceActivateという自作メソッドを呼び出しています。 一方、OLEIVERB_HIDEの場合はウインドウを非表示にすることを意味し、 このときにはインプレースアクティベーションを解除してもよいと思われるため、 InPlaceDeactivateを呼び出しています。 インプレースアクティベーションには親ウインドウのハンドルと、ウインドウの表示位置が必要となるため、 InPlaceActivateにはそれらの引数を指定しています。 InPlaceActivateの内部は次のようになっています。

BOOL CActiveXControl::InPlaceActivate(HWND hwndParent, LPRECT lprc)
{
	HRESULT hr;

	hr = m_pClientSite->QueryInterface(IID_PPV_ARGS(&m_pInPlaceSite));
	if (FAILED(hr))
		return FALSE;
	
	hr = m_pInPlaceSite->CanInPlaceActivate();
	if (FAILED(hr)) {
		m_pInPlaceSite->Release();
		m_pInPlaceSite = NULL;
		return FALSE;
	}

	m_pInPlaceSite->OnInPlaceActivate();

	m_hwnd = CreateWindowEx(WS_EX_NOPARENTNOTIFY, g_szClassName, TEXT(""), WS_CHILD | WS_CLIPSIBLINGS, lprc->left, lprc->top, lprc->right - lprc->left, lprc->bottom - lprc->top, hwndParent, (HMENU)ID_WINDOW, g_hinstDll, NULL);
	SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this);

	m_pClientSite->ShowObject();
	ShowWindow(m_hwnd, SW_SHOW);
	m_pClientSite->OnShowWindow(TRUE);

	return TRUE;
}

まず、IOleInPlaceSite::CanInPlaceActivateを呼び出して、 コンテナがインプレースアクティベーションをサポートしているかを確認します。 これが成功した場合は、IOleInPlaceSite::OnInPlaceActivateでインプレースアクティベーションの開始を通知し、 コントロールのウインドウを作成します。 このウインドウは親ウインドウ(コンテナのウインドウ)の子ウインドウであるため、 ウインドウスタイルにWS_CHILDを指定し、親ウインドウのハンドルとしてhwndParentを指定します。 使用するウインドウクラスはDllMainで初期化されることになっており、 クラス名はg_szClassNameで識別されます。 SetWindowLongPtrを呼び出してthisポインタを設定しているのは、 ウインドウプロシージャでクラスを参照できるようにするためです。 ShowWindowを呼び出せば作成したウインドウは表示されますが、 その前後でコンテナに通知を送っておくようにします。

コントロールがプロパティやメソッドを持っている場合は、 IDispatch::Invokeを実装することでそれらの要求に応えなければなりません。 本来ならば、プロパティやメソッドはタイプライブラリを通じて公開し、 各プロパティにDISPIDを割り当てておくべきなのですが、 そうした処理を行っていなくても、ここで何らかの値を返すことは可能です。 理由は、ストックプロパティと呼ばれる既定のプロパティが定義されているからです。

STDMETHODIMP CActiveXControl::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
	if (wFlags == DISPATCH_PROPERTYGET) {
		if (dispIdMember == DISPID_ENABLED) {
			pVarResult->vt = VT_BOOL;
			pVarResult->boolVal = VARIANT_TRUE;
		}
		else if (dispIdMember == DISPID_READYSTATE) {
			pVarResult->vt = VT_I4;
			pVarResult->lVal = m_nState;
		}
		else if (dispIdMember == DISPID_FORECOLOR) {
			pVarResult->vt = VT_I4;
			pVarResult->lVal = m_crEllipse;
		}
		else
			;
	}

	return S_OK;
}

wFlagsがDISPATCH_PROPERTYGETである場合は、dispIdMemberで識別されるプロパティを返すようにします。 DISPID_ENABLEDの場合はコントロールが有効であるかどうかを返すべきかであり、 今回は常に有効ということでVARIANT_TRUEを返しています。 DISPID_READYSTATEの場合は、コントロールの現在の状態を返すようにします。 m_nStateは現在の状態を表す定数を格納しており、 たとえばデータのロードを完了した時点では、READYSTATE_COMPLETEになっています。 DISPID_FORECOLORはIEで指定されることはありませんが、 今回のコントロールは楕円の色に関するメンバを持っているので、これをプロパティとして返しています。


戻る