EternalWindows
OLE埋め込み / オブジェクトの作成

前節では、オブジェクトを埋め込むことによって、オブジェクトのイメージを表示できることを説明しましたが、 プログラミングの視点で言うとこれはどういうことなのでしょうか。 通常、コンテナはオブジェクトを埋め込むためにOleUIInsertObjectを呼び出しますが、 このときはその埋め込まれたオブジェクトのアドレスが返ることになります。 取得したオブジェクトは、自身がOLEのオブジェクトであることを示すIOleObjectを実装しており、 コンテナはこれを使用することでオブジェクトを操作できるようになります。 つまり、オブジェクトを埋め込むとは、IOleObjectを実装したオブジェクトのアドレスを取得することです。 また、オブジェクトからしても、コンテナに対して何らかの通知を送ることができたら便利であるため、 コンテナもCOMオブジェクトを内部で作成することになっています。 このオブジェクトは一般にサイトと呼ばれ、IOleClientSiteを実装することになっています。 次に、コンテナとオブジェクトの関係を図示します。

上図のA.exeがコンテナアプリケーションであり、 B.exeやC.exeがオブジェクトを実装するサーバーであるとします。 コンテナアプリケーションは、コンテナを表すオブジェクトを1つ作成し、 サイトを表すオブジェクトは埋め込んだオブジェクトの数だけ作成します。 1つのサイトは1つのオブジェクトのポインタを維持することになり、 コンテナは1つ以上のサイトのポインタを維持することになります。

コンテナやサイトのオブジェクトを作成するためには、そのオブジェクトの型となるクラスが必要になります。 今回はコンテナのためにCContainerというクラスを定義し、サイトのためにCSiteというクラスを定義しています。 CContainerの主な実装内容はウインドウ処理とメニューの応答であり、 メニューから「オブジェクトの挿入」という項目が選択された場合は、InsertObjectというメソッドを呼び出すことになっています。 このメソッドでは、オブジェクトを埋め込むためのダイアログを表示します。

BOOL CContainer::InsertObject()
{
	UINT              uResult;
	WCHAR             szFilePath[MAX_PATH];
	WCHAR             szObjectName[256];
	IStorage          *pStorage;
	IOleObject        *pOleObject;
	CSite             *p;
	OLEUIINSERTOBJECT insertObject;

	wsprintfW(szObjectName, L"オブジェクト %d", m_nObjectCount + 1);
	m_pRootStorage->CreateStorage(szObjectName, STGM_READWRITE | STGM_SHARE_EXCLUSIVE, 0, 0, &pStorage);
	if (pStorage == NULL)
		return FALSE;

	szFilePath[0] = '\0';

	ZeroMemory(&insertObject, sizeof(OLEUIINSERTOBJECT));
	insertObject.cbStruct   = sizeof(OLEUIINSERTOBJECT);
	insertObject.dwFlags    = IOF_DISABLEDISPLAYASICON | IOF_SELECTCREATENEW | IOF_CREATENEWOBJECT | IOF_CREATEFILEOBJECT | IOF_DISABLELINK;
	insertObject.hWndOwner  = m_hwnd;
	insertObject.lpszFile   = szFilePath;
	insertObject.cchFile    = MAX_PATH;
	insertObject.iid        = IID_IOleObject;
	insertObject.oleRender  = OLERENDER_DRAW;
	insertObject.lpIStorage = pStorage;
	insertObject.ppvObj     = (void **)&pOleObject;
	
	uResult = OleUIInsertObject(&insertObject);
	if (uResult != OLEUI_OK) {
		m_pRootStorage->DestroyElement(szObjectName);
		return FALSE;
	}

	m_nObjectCount++;

	if (m_pSite == NULL) {
		m_pSite = new CSite;
		p = m_pSite;
	}
	else {
		p = m_pSite;
		while (p->m_pNext != NULL)
			p = p->m_pNext;

		p->m_pNext = new CSite;
		p = p->m_pNext;
	}
	
	p->Initialize(pOleObject, pStorage, szObjectName, NULL);

	return TRUE;
}

m_pRootStorage->CreateStorageについては後の節で取り上げるため、 まずはOleUIInsertObjectの呼び出しに必要なOLEUIINSERTOBJECT構造体に注目します。 この構造体の多くのメンバは0で初期化しても問題ありませんが、 一部のメンバには適切な値を指定する必要があります。 cbStructは構造体のサイズを指定します。 dwFlagsはいくつかの定数を指定することができ、IOF_DISABLEDISPLAYASICONという定数を指定した場合は、 「アイコンの表示」というチェックボックスを非表示にできます。 IOF_SELECTCREATENEWで「新規作成」のラジオボタンがデフォルトで選択され、 IOF_CREATENEWOBJECTによって「新規作成」で選択されたオブジェクトが実際に作成されます。 「ファイルから作成」でもオブジェクトを作成できるようにするためには、IOF_CREATEFILEOBJECTも指定します。 IOF_DISABLELINKを指定しているのは、ファイルから作成する際に「リンク」を選択できないようにするためです。 hWndOwnerは、ダイアログの親ウインドウにするハンドルを指定します。 lpszFileにはファイルパスを受け取る変数を指定し、cchFileにはその変数のサイズを指定します。 iidは、オブジェクトに要求するインターフェースのIIDを指定します。 今回はオブジェクトをIOleObjectで識別したいため、IID_IOleObjectを指定しています。 oleRenderは、オブジェクトのレンダリングオプションを指定します。 0を指定した場合はオブジェクトが描画されなくなるため、OLERENDER_DRAWを指定するようにします。 lpIStorageは、オブジェクトのために使用する構造化ストレージを指定します。 事前にm_pRootStorage->CreateStorageを呼び出しているのはこのためです。 ppvObjは、オブジェクトを受け取る変数のアドレスを指定します。

オブジェクトを作成したら、オブジェクトが1つ増えたということでm_nObjectCountをカウントします。 既に述べたように、1つのオブジェクトには1つのサイトオブジェクト(CSite)が必要になりますが、 これは複数個存在する可能性があるため、リスト構造として管理するようにします。 リスト構造の先頭(m_pSite)自体が初期化されていない場合はそれを初期化しますが、 既に先頭が初期化されている場合は次のオブジェクトへの参照を維持していないオブジェクトまで確認し、 そのオブジェクトのm_pNextにCSiteを格納します。 Initializeを呼び出せば、CSiteにオブジェクトの情報を渡すことができます。

void CSite::Initialize(IOleObject *pOlebject, IStorage *pStorage, LPWSTR lpszObjectName, LPRECT lprc)
{
	m_pStorage = pStorage;

	m_pOleObject = pOlebject;
	m_pOleObject->SetClientSite(static_cast<IOleClientSite *>(this));
	m_pOleObject->SetHostNames(L"sample", lpszObjectName);

	if (lprc != NULL)
		m_rc = *lprc;
	else
		UpdateRect();
}

引数として与えられたIOleObjectとIStorageは後で必要になるため、メンバ変数に保存しておきます。 IOleObject::SetClientSiteを呼び出すことで、オブジェクトに対してIOleClientSiteを実装したオブジェクト(CSite)を渡すことができます。 これにより、オブジェクトはIOleClientSiteを通じてCSiteと通信できることになります。 IOleObject::SetHostNamesを呼び出せば、作成されたオブジェクトの名前を第2引数から渡すことができます。 この名前は、オブジェクトのウインドウのメニューで表示されることがあります。 第1引数はコンテナの名前を指定しますが、これはコンテナのウインドウの名前などを指定すればよいと思われます。 m_rcにはオブジェクトの位置を格納しますが、lprcが与えられていない場合はUpdateRectという自作メソッドで初期化します。

void CSite::UpdateRect()
{
	SIZEL sizel;

	m_pOleObject->GetExtent(DVASPECT_CONTENT, &sizel);

	HIMETRICtoDP(&sizel);
	m_rc.right = m_rc.left + sizel.cx;
	m_rc.bottom = m_rc.top + sizel.cy;
}

オブジェクトのサイズは、IOleObject::GetExtentを呼び出すことで取得できます。 ただし、このサイズはHIMETRICという単位で返ることになっているため、 これをデバイス単位に変換するためにHIMETRICtoDPという自作メソッドを呼び出しています。 変換が終了したら、位置とサイズを足すことでrightとbottomを初期化します。

CSiteはIOleClientSiteでオブジェクトからの通知を受け取るため、 その通知がどのような意味を持つのかを理解しておく必要があります。 各メソッドの処理を順に見ていきます。

STDMETHODIMP CSite::SaveObject()
{
	HWND hwnd;

	Save();

	UpdateRect();
	g_pContainer->GetWindow(&hwnd);
	InvalidateRect(hwnd, NULL, TRUE);

	return S_OK;
}

SaveObjectは、オブジェクトのウインドウを閉じた場合や、メニューからオブジェクトを更新した場合に呼ばれます。 ここでは、オブジェクトに対して保存処理を行うように指示することができるため、 Saveという自作関数でそれを行っています。 この関数の内部については後で取り上げます。 自作関数のUpdateRectを呼び出しているのは、オブジェクトのサイズを更新するためです。 オブジェクトのサイズは、オブジェクトのウインドウを開いた際に更新される可能性があるため、 ここでそれを確認するようにしています。 オブジェクトのサイズが変化している場合は、コンテナのクライアント領域に描画された図形も更新しなければならないため、 InvalidateRectで再描画を促します。

STDMETHODIMP CSite::GetMoniker(DWORD dwAssign, DWORD dwWhichMoniker, IMoniker **ppmk)
{
	return E_NOTIMPL;
}

GetMonikerは、コンテナ内のオブジェクトの識別するモニカを取得する場合に呼ばれます。 今回は実装しないということで、E_NOTIMPLを返しています。

STDMETHODIMP CSite::GetContainer(IOleContainer **ppContainer)
{
	*ppContainer = NULL;

	return E_NOINTERFACE;
}

GetContainerは、IOleObject::SetClientSiteの内部で呼ばれることが多いようです。 コンテナ(CContainer)がIOleContainerを実装している場合はそのアドレスをppContainerに格納することができますが、 今回のコンテナはIOleContainerを実装していません。 よって、ppContainerにNULLを格納してE_NOINTERFACEを返しています。 IOleContainerを実装するようになれば、 コンテナに埋め込まれたオブジェクトの列挙をサポートできます。

STDMETHODIMP CSite::ShowObject()
{
	return S_OK;
}

ShowObjectは、オブジェクトのウインドウを表示する段階になると呼ばれます。 基本的には、IOleObject::DoVerbの内部で呼ばれていると考えてよいでしょう。 特に行うことがない場合はS_OKを返すだけで構いません。

STDMETHODIMP CSite::OnShowWindow(BOOL fShow)
{
	HWND hwnd;
	
	m_bShowWindow = fShow;

	g_pContainer->GetWindow(&hwnd);
	InvalidateRect(hwnd, NULL, TRUE);

	return S_OK;
}

OnShowWindowは、ウインドウの表示または非表示が完了した場合に呼ばれます。 ShowObjectが制御を返せばウインドウが表示され始めるはずですが、 それが完了した場合にOnShowWindowが呼ばれることになります。 InvalidateRectで再描画を促しているのは、オブジェクトのイメージにハッチパターンを描画するためです。 現在の表示状態をメンバ変数に保存している理由については、次節で説明します。

STDMETHODIMP CSite::RequestNewObjectLayout()
{
	return E_NOTIMPL;
}

RequestNewObjectLayoutは、呼ばれることがないようなのでE_NOTIMPLを返しています。

サイトがIOleClientSiteを実装するのはオブジェクトからの通知を受け取るためですが、 より多くの通知を必要とする場合はIAdviseSinkを実装することもできます。 以下、IAdviseSinkのメソッドを順に示します。

STDMETHODIMP_(void) CSite::OnDataChange(FORMATETC *pFormatetc, STGMEDIUM *pStgmed)
{
}

OnDataChangeは、オブジェクトの内部データ(Excelにおけるセルの値など)が変更された場合に呼ばれます。 これを検出する場合は、事前にIDataObject::DAdviseを呼び出しておく必要があります。 IDataObjectは、m_pOleObject->QueryInterfaceから取得することができます。

STDMETHODIMP_(void) CSite::OnViewChange(DWORD dwAspect, LONG lindex)
{
	HWND hwnd;
	
	UpdateRect();

	g_pContainer->GetWindow(&hwnd);
	InvalidateRect(hwnd, NULL, TRUE);
}

OnViewChangeは、オブジェクトの外観が変更された場合に呼ばれます。 このときには、コンテナ上に描画されたオブジェクトのイメージを再描画するために、 InvalidateRectを呼び出すようにしています。 オブジェクトのウインドウのサイズが変更されているかもしれないため、 UpdateRectでサイズも更新しておきます。 OnViewChangeを検出する場合は、事前にIViewObject::SetAdviseを呼び出しておく必要があります。

STDMETHODIMP_(void) CSite::OnRename(IMoniker *pmk)
{
}

STDMETHODIMP_(void) CSite::OnSave()
{
}

STDMETHODIMP_(void) CSite::OnClose()
{
}

OnRenameは、リンクオブジェクトが以前と異なる場所に保存された際に呼ばれます。 今回はオブジェクトをリンクするのではなく、埋め込むことを目的としているため、 このメソッドが呼ばれることはないはずです。 OnSaveは、オブジェクトが保存された際に呼ばれます。 これは、IOleClientSite::SaveObjectが制御を返した後に相当します。 OnCloseは、オブジェクトのウインドウが閉じられた際に呼ばれます。 これらのメソッドの呼び出しを検出する場合は、事前にIOleObject::Adviseを呼び出す必要があります。

既に示したIOleClientSite::SaveObjectでは、Saveという自作関数を呼び出していました。 この関数では、オブジェクトに対してデータを保存する機会を与えています。

void CSite::Save()
{
	IPersistStorage *pPersistStorage;

	m_pOleObject->QueryInterface(IID_PPV_ARGS(&pPersistStorage));

	OleSave(pPersistStorage, m_pStorage, FALSE);

	pPersistStorage->SaveCompleted(NULL);
	pPersistStorage->Release();
}

オブジェクトにデータを保存させるためには、IPersistStorage::Saveを呼び出す必要があります。 オブジェクトはIPersistStorageを実装しているので、 m_pOleObjectに対してQueryInterfaceを取得すれば問題なく取得することができます。 IPersistStorage::SaveではなくOleSaveを呼び出しているのは、 この関数がIPersistStorage::Saveを呼び出すと同時に、 IPersist::GetClassIDとWriteClassStgを呼び出してくれるからです。 つまり、データの保存とオブジェクトのCLSIDの書き込みを同時に行えます。 第1引数はIPersistStorageインターフェースであり、第2引数はデータの保存先とする構造化ストレージを指定します。 第3引数は、データを一から全て保存することを示すFALSEでよいでしょう。 OleSaveの呼び出しが終われば、保存が完了したことを伝えるためにSaveCompletedを呼び出します。

コンテナのウインドウが閉じられる場合は、オブジェクトのプロセスも終了するべきといえます。 このような場合は、Closeという自作メソッドが呼ばれます。

void CSite::Close()
{
	if (OleIsRunning(m_pOleObject)) {
		m_pOleObject->Close(OLECLOSE_PROMPTSAVE);
		m_pOleObject->Unadvise(m_dwConnection);
	}
}

OleIsRunningは、内部でIRunnableObject::IsRunningを呼び出すことによって、オブジェクトが実行中であるかを確認します。 実行中でないということはプロセスが起動されていないということであり、 そのような場合は終了処理を行うわけにはいきません。 実行中である場合はIOleObject::Closeを呼び出すことで、プロセスを終了させるようにします。 第1引数はプロセスの終了に伴ってデータを保存するかを示す定数を指定でき、 OLECLOSE_SAVEIFDIRTYはオブジェクトがダーティ状態である場合にデータを保存することを意味します。 また、OLECLOSE_NOSAVEはデータを保存しないことを意味し、 OLECLOSE_PROMPTSAVEは保存するかを確認するためのUIを促します。 上記ではOLECLOSE_NOSAVEを指定していますが、多くの場合はこれが妥当であると思われます。 理由は、このメソッドが呼ばれる時点でコンテナが終了する段階に来ており、 そのような場合にオブジェクトのデータが構造化ストレージに保存されても意味を持たないからです。 今回の場合、構造化ストレージの内容がファイルに出力されるのはファイルメニューから「保存」が選択された場合のみであるため、 これが行えないことには構造化ストレージを更新しても意味がありません。 IOleObject::Unadviseはアドバイズ接続を解除するため、 これをCloseの前に呼び出した場合はIAdviseSink::OnCloseが送られないことになります。

デフォルトハンドラとデータキャッシュ

OLEにおけるオブジェクトの埋め込みとは、別のアプリケーションで作成されたオブジェクトへのポインタを維持し、 そのオブジェクトのイメージやウインドウを自作のアプリケーションで表示することを意味します。 この事を考えると、アプリケーションがオブジェクトを埋め込んでいる間は、 埋め込み元となるオブジェクトが常に存在しなければならないように思えますが、実際にはそうとは限りません。 既に述べたようにオブジェクトは休止状態になることができ、 その際にはオブジェクトを作成したアプリケーションは既に終了しています。 このようなとき、アプリケーションが維持しているポインタは一体何を指しているのかと思えますが、 これはデフォルトハンドラが作成したオブジェクトです。

実を言うと、アプリケーションが識別しているのは常にデフォルトハンドラのオブジェクトです。 デフォルトハンドラの正体はole32.dllであり、オブジェクトはOleCreateDefaultHandlerで作成されています。 アプリケーションがIOleObjectで操作できるのは、このデフォルトハンドラのオブジェクトであり、 このオブジェクトが埋め込み元のオブジェクトのIOleObjectに処理を通知するのです。 デフォルトハンドラという仲介が存在する理由は、オブジェクトが常に埋め込まれているように見せかけるためです。 アプリケーションが完全にオブジェクトを識別できなくなってしまうと、 先ほどまで埋め込んでいたオブジェクトのイメージを表示できなくなりますし、 オブジェクトを再び実行することもてきなくなります。 しかし、デフォルトのハンドラのオブジェクトを常に識別するようにしていれば、 このオブジェクトを通じて、埋め込み元のオブジェクトを再び実行することができます。 こうした要件を満たすために、デフォルトハンドラのオブジェクトは、埋め込みオブジェクトが本来実装すべきインターフェースに加え、 IRunnableObjectとIViewObject2を実装するようになっています。 IRunnableObjectは埋め込み元オブジェクトの実行や既に実行されているかの確認に使用され、 IViewObject2はオブジェクトのイメージの描画に使用されます。 こうしたインターフェースをIOleObjectから照会できるという事実は、 アプリケーションが維持しているIOleObjectが、埋め込み元のオブジェクトを直接識別していないことを物語っています。

デフォルトハンドラのオブジェクトのIViewObject2は、オブジェクトのイメージを描画できなければなりませんが、 既に埋め込み元オブジェクトが終了しているのにも関わらず、これはどのように実現すればよいのでしょうか。 実は、ここではCreateDataCacheで作成されるデータキャッシュと呼ばれるオブジェクトが使用されています。 データキャッシュに埋め込み元のIDataObjectを指定した場合、IDataObject::GetDataが呼ばれてイメージがキャッシュされ、 データキャッシュのIViewObjectを使用してイメージを描画できるようになります。

オブジェクトを埋め込めるといっても、常にデフォルトハンドラのオブジェクトが使用されるとは限りません。 オブジェクトをアウトプロセスサーバーとしてレジストリに登録する際には、デフォルトのハンドラとしてole32.dllを指定しますが、 代わりに独自のハンドラを指定することも許されています。 このような独自のハンドラを作成すれば、イメージのキャッシュなどを拡張することができるようになるでしょう。 また、オブジェクトをインプロセスサーバーとして作成する場合は、ハンドラという概念自体がなくなります。 理由は、コンテナアプリケーションのアドレス空間にオブジェクトのDLLがロードされるからであり、 これによってオブジェクトの実体がコンテナ上で作成されるからです。 ActiveXコントロールを作成する場合は、オブジェクトをインプロセスサーバーとして実装することになります。



戻る