EternalWindows
オートメーション / イベントの受信

これまでオブジェクトを操作する方法について説明してきましたが、今回はオブジェクトからイベントを受信する方法について説明します。 このイベントというのは、たとえばWordなら「新しいドキュメントが作成された」という通知であり、 オブジェクトの現在の状態を把握した場合は便利な存在といえます。 COMにおけるイベント受信の標準的なインターフェースは、IConnectionPointです。 IConnectionPointを実装するオブジェクトはコネクションポイントと呼ばれ、 自身とシンクを接続するためのAdviseというメソッドを提供しています。 シンクとは、クライアントが作成するイベント受信用のオブジェクトのことです。

コネクションポイントを取得するには、それを探すためのオブジェクトが別途必要になります。 このようなオブジェクトはコネクタブルオブジェクトと呼ばれ、 IConnectionPointContainerを実装することになっています。 このインターフェースのFindConnectionPointを呼び出せば、 指定されたIIDで識別されるコネクションポイントを取得することができます。

HRESULT IConnectionPointContainer::FindConnectionPoint(
  REFIID riid,
  IConnectionPoint **ppCP
);

riidは、取得したいコネクションポイントのIIDを指定します。 ppCPは、コネクションポイントを受け取る変数のアドレスを指定します。

コネクションポイントを取得したら、Adviseを呼び出してクライアントのシンクと接続させます。

HRESULT IConnectionPoint::Advise(
  IUnknown *pUnkSink,
  DWORD *pdwCookie
);

pUnkSinkは、クライアントが作成したシンクのアドレスを指定します。 pdwCookieは、確立された接続を識別する値を受け取る変数のアドレスを指定します。 この値は、Unadviseで接続を解除する際に必要です。

コネクションポイントは、シンクのメソッドを呼び出すことでイベントが発生したことを通知します。 このことからシンクは、コネクションポイントが理解できるインターフェースを実装しておく必要がありますが、 これは一体に何になるのでしょうか。 Wordの場合は、IDispatchになります。 Wordのコネクションポイントは、イベントが発生した場合にシンクのIDispatch::Invokeを呼び出し、 その第1引数にイベントの種類を表す値を指定します。 シンクは、この第1引数を参照することで発生したイベントを特定できることになります。

コネクションポイントから通知されるイベントにはどれだけの量が存在するのでしょうか。 WordのようなOfficeアプリケーションならば、MSDNやタイプライブラリから確認することができます。

interface IApplicationEvents2以下に列挙されているのが通知されるイベントの総数です。 上図ではNewDocumentを選択しており、そのidは9になっています。 よって、IDispatch::Invokeの第1引数が9である場合は、 ドキュメントが作成されたこと示す通知であると判断できます。 IApplicationEvents2のような通知用インターフェースは、 コネクションポイントを探すための指標になりますから、 インターフェースのIIDを確認しておく必要があります。

uuidキーワードに指定されている値がIApplicationEvents2のIIDになります。 クライアントは、これをサポートするコネクションポイントを探す必要があります。

今回のプログラムは、Wordを起動して表示します。 このWordで新しいドキュメントを作成した場合は、New Documentイベントがシンクに通知されるため、 これを受信したシンクはその旨をWordのドキュメントに記述しています。

#include <windows.h>
#include <ocidl.h>

const IID IID_IApplicationEvents2 = {0x000209fe, 0x0000, 0x0000, {0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46}};

class CEventSink : public IDispatch
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();
	
	STDMETHODIMP GetTypeInfoCount(UINT *pctinfo);
	STDMETHODIMP GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo);
	STDMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId);
	STDMETHODIMP Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr);

	CEventSink();

private:
	LONG m_cRef;
};

HRESULT Invoke(IDispatch *pDispatch, LPOLESTR lpszName, WORD wFlags, VARIANT *pVarArray, int nArgs, VARIANT *pVarResult);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	IDispatch                 *pApplication;
	CLSID                     clsid;
	HRESULT                   hr;
	VARIANT                   var;
	DWORD                     dwCookie;
	IConnectionPointContainer *pConnectionPointContainer;
	IConnectionPoint          *pConnectionPoint;
	CEventSink                *pEventSink;

	CoInitialize(NULL);
	
	hr = CLSIDFromProgID(L"Word.Application", &clsid);
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}
	
	hr = CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&pApplication));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}

	pApplication->QueryInterface(IID_PPV_ARGS(&pConnectionPointContainer));
	hr = pConnectionPointContainer->FindConnectionPoint(IID_IApplicationEvents2, &pConnectionPoint);
	pConnectionPointContainer->Release();
	if (FAILED(hr)) {
		pApplication->Release();
		CoUninitialize();
		return 0;
	}
	
	pEventSink = new CEventSink();
	hr = pConnectionPoint->Advise(pEventSink, &dwCookie);
	if (FAILED(hr)) {
		pConnectionPoint->Release();
		pEventSink->Release();
		pApplication->Release();
		CoUninitialize();
		return 0;
	}
	
	var.vt = VT_I4;
	var.lVal = 1;
	Invoke(pApplication, L"Visible", DISPATCH_PROPERTYPUT, &var, 1, NULL);
	
	MessageBox(NULL, TEXT("終了します。"), TEXT("OK"), MB_OK);

	pConnectionPoint->Unadvise(dwCookie);
	pConnectionPoint->Release();
	pEventSink->Release();
	pApplication->Release();
	CoUninitialize();
	
	return 0;
}

HRESULT Invoke(IDispatch *pDispatch, LPOLESTR lpszName, WORD wFlags, VARIANT *pVarArray, int nArgs, VARIANT *pVarResult)
{
	DISPPARAMS dispParams;
	DISPID     dispid;
	DISPID     dispidName = DISPID_PROPERTYPUT;
	HRESULT    hr;
	
	hr = pDispatch->GetIDsOfNames(IID_NULL, &lpszName, 1, LOCALE_USER_DEFAULT, &dispid);
	if (FAILED(hr))
		return hr;
	
	dispParams.cArgs = nArgs;
	dispParams.rgvarg = pVarArray;
	if (wFlags & DISPATCH_PROPERTYPUT) {
		dispParams.cNamedArgs = 1;
		dispParams.rgdispidNamedArgs = &dispidName;
	}
	else {
		dispParams.cNamedArgs = 0;
		dispParams.rgdispidNamedArgs = NULL;
	}

	hr = pDispatch->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, wFlags, &dispParams, pVarResult, NULL, NULL);

	return hr;
}


// CEventSink


CEventSink::CEventSink()
{
	m_cRef = 1;
}

STDMETHODIMP CEventSink::QueryInterface(REFIID riid, void **ppvObject)
{
	*ppvObject = NULL;

	if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IDispatch) || IsEqualIID(riid, IID_IApplicationEvents2))
		*ppvObject = static_cast<IDispatch *>(this);
	else
		return E_NOINTERFACE;

	AddRef();
	
	return S_OK;
}

STDMETHODIMP_(ULONG) CEventSink::AddRef()
{
	return InterlockedIncrement(&m_cRef);
}

STDMETHODIMP_(ULONG) CEventSink::Release()
{
	if (InterlockedDecrement(&m_cRef) == 0) {	
		delete this;
		return 0;
	}

	return m_cRef;
}

STDMETHODIMP CEventSink::GetTypeInfoCount(UINT *pctinfo)
{
	*pctinfo = 0;
	
	return S_OK;
}

STDMETHODIMP CEventSink::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo)
{
	return E_NOTIMPL;
}

STDMETHODIMP CEventSink::GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId)
{
	return E_NOTIMPL;
}

STDMETHODIMP CEventSink::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
	IDispatch *pDocument;
	IDispatch *pRange;
	VARIANT   var;
	VARIANT   varResult;

	if (dispIdMember == 0x00000009) {
		if (pDispParams->rgvarg[0].vt & VT_DISPATCH) {
			pDocument = pDispParams->rgvarg[0].pdispVal;

			VariantInit(&varResult);
			::Invoke(pDocument, L"Content", DISPATCH_PROPERTYGET, NULL, 0, &varResult);
			pRange = varResult.pdispVal;
		
			var.vt = VT_BSTR;
			var.bstrVal = SysAllocString(L"New Documentイベントを検出しました。");
			::Invoke(pRange, L"Text", DISPATCH_PROPERTYPUT, &var, 1, NULL);
			SysFreeString(var.bstrVal);
			
			pRange->Release();
		}
	}

	return S_OK;
}

グローバルに定義されているIID_IApplicationEvents2は、IApplicationEvents2のIIDを格納しています。 CEventSinkというクラスは、コネクションポイントと接続するシンクの型です。 Wordのコネクションポイントは、シンクがIDispatchを実装していることを前提にしているため、 CEventSinkはIDispatchを継承している必要があります。 オブジェクトを実装する際の基本的な事項はCOMサーバーの章を参照してください。

コネクションポイントとシンクを接続させるためには、まずコネクタブルオブジェクトを取得しなければなりませんが、 WordにおけるApplicationオブジェクトはこの役割も兼ねています。 つまり、IConnectionPointContainerを実装しているので、 ApplicationオブジェクトをIConnectionPointContainerで識別することができます。

pApplication->QueryInterface(IID_PPV_ARGS(&pConnectionPointContainer));
hr = pConnectionPointContainer->FindConnectionPoint(IID_IApplicationEvents2, &pConnectionPoint);
pConnectionPointContainer->Release();

pConnectionPointContainerの型がIConnectionPointContainerであれば、QueryInterfaceは成功します。 IConnectionPointContainerで行うべきことは、IApplicationEvents2をサポートするコネクションポイントを探すことですから、 IID_IApplicationEvents2を指定してFindConnectionPointを呼び出すことになります。 IConnectionPointContainerは、不要になったら直ぐに開放しても構いません。

コネクションポイントを取得したら、それに接続するためのシンクを作成することになります。

pEventSink = new CEventSink();
hr = pConnectionPoint->Advise(pEventSink, &dwCookie);

シンクの型はCEventSinkで表すことになっていますから、この型のインスタンスを作成することになります。 IConnectionPoint::Adviseが成功すれば、コネクションポイントとシンクは接続されたことになります。 つまり、IApplicationEvents2のメソッドのIDを基に通知が行われるようになります。 アプリケーションを終了する際には、必ずUnadviseを呼び出すようにしてください。

CEventSinkは、IUnknownのメソッドとIDispatchメソッドを持っています。 IUnknown関連で注意しなければならないのは、QueryInterfaceの実装です。

STDMETHODIMP CEventSink::QueryInterface(REFIID riid, void **ppvObject)
{
	*ppvObject = NULL;

	if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IDispatch) || IsEqualIID(riid, IID_IApplicationEvents2))
		*ppvObject = static_cast<IDispatch *>(this);
	else
		return E_NOINTERFACE;

	AddRef();
	
	return S_OK;
}

QueryInterfaceでは、オブジェクトが実装しているインターフェースを明確にしておかなければなりません。 今回作成するシンクはIUnknownとIDispatchを実装していますから、 これらのIIDが指定された場合はオブジェクトのアドレスを返すようにします。 そして最も重要なのが、IApplicationEvents2のIIDが指定された場合でも、 オブジェクトのアドレスを返すという点です。 シンクはIApplicationEvents2を実装しているわけではないのですが、 このIIDには応答しなければならないようです。

IDispatchのGetTypeInfoCountはタイプ情報の数を返す必要がありますが、 今回はこれをサポートしていないので、アドレスに0を格納してS_OKを返せば問題ありません。 GetTypeInfoとGetIDsOfNamesについては、そもそも実装しないということでE_NOTIMPLを返します。 興味のあるメソッドはイベントが通知された時に呼ばれるInvokeですから、 これを意味のある形で実装することになります。

STDMETHODIMP CEventSink::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
	IDispatch *pDocument;
	IDispatch *pRange;
	VARIANT   var;
	VARIANT   varResult;

	if (dispIdMember == 0x00000009) {
		if (pDispParams->rgvarg[0].vt & VT_DISPATCH) {
			pDocument = pDispParams->rgvarg[0].pdispVal;

			VariantInit(&varResult);
			::Invoke(pDocument, L"Content", DISPATCH_PROPERTYGET, NULL, 0, &varResult);
			pRange = varResult.pdispVal;
		
			var.vt = VT_BSTR;
			var.bstrVal = SysAllocString(L"New Documentイベントを検出しました。");
			::Invoke(pRange, L"Text", DISPATCH_PROPERTYPUT, &var, 1, NULL);
			SysFreeString(var.bstrVal);
			
			pRange->Release();
		}
	}

	return S_OK;
}

既に述べたように、Invokeの第1引数にはイベントの種類を表す値が格納されます。 9という値はNew Documentイベントを意味しますから、このif文は新しいドキュメントが作成された際に実行されることになります。 New DocumentイベントにはDocumentオブジェクトへのポインタが引数としてありますから、 pDispParams->rgvarg[0]にはそれが格納されていることになります。 rgvargはVARIANT構造体であり、オブジェクトのポインタを格納する場合はvtメンバにVT_DISPATCHを格納しています。 この場合は、pdispValメンバからIDispatchへのポインタを参照できますから、 これを使用してDocumentオブジェクトを操作することができます。 今回はドキュメントに文字列を書き込むということで、 Contentプロパティを呼び出してRangeオブジェクトを取得し、 Textプロパティに文字列を指定しています。


戻る