EternalWindows
インプレースアクティベーション / IOleInPlaceObjectの実装

今回から、インプレースアクティベーションに対応したオブジェクトの作成について見ていきます。 コンテナ上に描画されたオブジェクトのイメージをダブルクリックした場合、 OLE埋め込みの章では独立したウインドウを表示していましたが、 今回作成するサンプルでは次のようにウインドウを表示します。

見て分かるように、オブジェクトのウインドウがコンテナ上に表示されています。 また、今回作成するオブジェクトはUIアクティベーションもサポートするため、 オブジェクトの「編集」というメニュー項目や、 オブジェクトのツールバーもコンテナ上に表示されています。 オブジェクトの動作内容はOLE埋め込みの際と同様であり、クリックした位置に図形を描画します。

インプレースアクティベーションの対応に伴って、コンテナが新しいインターフェースを実装することになったように、 オブジェクトもIOleInPlaceObjectという新しいインターフェースを実装することになります。 OLE埋め込みの章ではIOleObjectを実装したCObjectというクラスを定義しましたが、 このクラスがIOleInPlaceObjectを実装するようになればよいでしょう。 インプレースアクティベーションの開始は、IOleObject::DoVerbによって伝えられます。

STDMETHODIMP CObject::DoVerb(LONG iVerb, LPMSG lpmsg, IOleClientSite *pActiveSite, LONG lindex, HWND hwndParent, LPCRECT lprcPosRect)
{
	BOOL bOpen = FALSE;

	if (iVerb == OLEIVERB_SHOW || iVerb == OLEIVERB_PRIMARY || iVerb == OLEIVERB_UIACTIVATE) {
		if (!InPlaceActivate(hwndParent, (LPRECT)lprcPosRect, TRUE))
			bOpen = TRUE;
	}
	else if (iVerb == OLEIVERB_INPLACEACTIVATE) {
		if (!InPlaceActivate(hwndParent, (LPRECT)lprcPosRect, FALSE))
			bOpen = TRUE;
	}
	else if (iVerb == OLEIVERB_OPEN)
		bOpen = TRUE;
	else if (iVerb == OLEIVERB_HIDE)
		InPlaceDeactivate();
	else
		return E_FAIL;

	if (bOpen) {
		m_pClientSite->ShowObject();
		ShowWindow(m_hwnd, SW_SHOW);
		SetForegroundWindow(m_hwnd);
		m_pClientSite->OnShowWindow(TRUE);
	}

	return S_OK;
}

iVerbがOLEIVERB_SHOW、OLEIVERB_PRIMARY、OLEIVERB_UIACTIVATEである場合は、 インプレースアクティベーションと共にUIアクティベーションを行い、 OLEIVERB_INPLACEACTIVATEである場合はインプレースアクティベーションのみを行います。 この区別については、InPlaceActivateの第3引数で行われます。 InPlaceActivateはインプレースアクティベーションを行う自作メソッドであり、 第1引数はインプレース先のウインドウハンドル、第2引数はインプレースの位置を表すRECT構造体になります。 インプレースアクティベーションを行うといっても、コンテナがそれをサポートしていない可能性もあるため、 そのような場合はbOpenをTRUEにして通常のウインドウを開くようにします。 これは、iVerbにOLEIVERB_OPENが指定された場合も同様です。 InPlaceActivateの実装は次のようになっています。

BOOL CObject::InPlaceActivate(HWND hwndParent, LPRECT lprc, BOOL bUIActivate)
{
	HRESULT hr;
	TCHAR   szBuf[256];

	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_hwndInplace = CreateWindowEx(WS_EX_NOPARENTNOTIFY, m_szInplaceClassName, TEXT(""), WS_CHILD | WS_CLIPSIBLINGS | WS_THICKFRAME, lprc->left, lprc->top, lprc->right - lprc->left, lprc->bottom - lprc->top, hwndParent, (HMENU)ID_INPLACEWINDOW, GetModuleHandle(NULL), NULL);

	m_pClientSite->ShowObject();
	ShowWindow(m_hwndInplace, SW_SHOW);
	m_pClientSite->OnShowWindow(TRUE);
	
	if (bUIActivate)
		UIActivate();

	return TRUE;
}

インプレースアクティベーションを開始するにあたって、 それをコンテナのサイトがサポートするのかを確認しなければなりません。 サイトからIOleInPlaceSiteを取得することができ、 さらにCanInPlaceActivateが成功した場合は、 インプレースアクティベーションが可能であると判断します。 OnInPlaceActivateを呼び出して、インプレースアクティベーションの開始をサイトに通知すれば、 オブジェクトのウインドウをコンテナ上に表示する作業に入ります。 CreateWindowExを呼び出しているのは、インプレースアクティベーション用のウインドウを作成するためであり、 これはコンテナのウインドウの子ウインドウという位置づけになることから、 ウインドウスタイルにはWS_CHILDを指定します。 また、第10引数には親ウインドウのハンドルを指定し、第11引数にはウインドウのIDを指定します。 第1引数の拡張スタイルは0でも問題ありませんが、WS_EX_NOPARENTNOTIFYを指定することで、 コンテナのウインドウにWM_PARENTNOTIFYが送られないようにできます。 このメッセージはウインドウの作成や破棄を通知する意味を持ちますが、そうした通知は送る意味は特にありません。 ShowWindowの呼び出しで、ウインドウはコンテナ上に表示されることになるため、 この時点でインプレースアクティベーションは完了したことになります。 bUIActivateがTRUEである場合はUIアクティベーションも行うことになりますが、 これについては次節で説明します。

インプレースアクティベーション用のウインドウを作成しなければならないというのは、 少し疑問に思う部分があるかもしれません。 そもそも、サーバーは起動時に非表示のウインドウを作成しているわけであり、 このウインドウをコンテナの子ウインドウに設定しては何か問題があるのでしょうか。 実は、次のようにSetParentを呼び出せば、 確かにm_hwndはhwndParentの子ウインドウになるのです。

SetParent(m_hwnd, hwndParent);

SetParentによってm_hwndは子ウインドウに設定されることになりますが、 それは子ウインドウとして正しく認識されることを無条件に意味するものではありません。 子ウインドウはIDを持っていなければならないわけですが、 m_hwndにはこのようなIDが設定されていないので、 子ウインドウとして不完全な形になるという問題があります。 m_hwndはトップレベルウインドウであるため、 作成時にIDを指定することもできませんし、動的にIDを設定する手段も用意されていません。 SetWindowLongPtrにはGWL_IDというウインドウのIDを変更する定数がありますが、 これを指定しても関数が成功することはありません。 以上の事から、インプレースアクティベーション用のウインドウを別個作成するという方法を使用しています。

インプレースアクティベーション用のウインドウであるm_hwndInplaceと通常のウインドウであるm_hwndは、 元となるウインドウクラスが互いに異なります。 これは、両者の動作が一部異なるという関係上、 ウインドウプロシージャを別々にしたほうがよいと考えたからです。 m_hwndInplaceのウインドウクラスは、CObject::Runで次のように作成されています。

lstrcpy(m_szInplaceClassName, TEXT("inplace"));
wc.lpfnWndProc   = ::WindowProcInplace;
wc.lpszClassName = m_szInplaceClassName;
if (RegisterClassEx(&wc) == 0)
	return 0;

ウインドウクラスの名前はウインドウの作成時に参照できるようにしたいため、 m_szInplaceClassNameに保存するようにしています。 WindowProcInplaceがインプレースアクティベーション用のウインドウプロシージャであり、 次のような実装を持っています。

LRESULT CObject::WindowProcInplace(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg) {
	
	case WM_PAINT: {
		HDC         hdc;
		PAINTSTRUCT ps;

		hdc = BeginPaint(hwnd, &ps);

		DrawShape(hdc);

		EndPaint(hwnd, &ps);

		return 0;
	}

	case WM_COMMAND: {
		int nId = LOWORD(wParam);

		if (nId == ID_RECTANGLE)
			m_bDrawRectangle = TRUE;
		else if (nId == ID_ELLIPSE)
			m_bDrawRectangle = FALSE;
		else
			;

		return 0;
	}
	
	case WM_LBUTTONDOWN:
		OnLButtonDown(hwnd, LOWORD(lParam), HIWORD(lParam));
		return 0;

	case WM_EXITSIZEMOVE: {
		RECT rc;

		GetWindowRect(hwnd, &rc);

		ScreenToClient(GetParent(hwnd), (LPPOINT)&rc);
		ScreenToClient(GetParent(hwnd), (LPPOINT)&rc + 1);
		m_pInPlaceSite->OnPosRectChange(&rc);

		if (m_bEmbedding && m_pDataAdviseHolder != NULL)
			m_pDataAdviseHolder->SendOnDataChange(static_cast<IDataObject *>(this), 0, DVASPECT_CONTENT);

		return 0;
	}

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

WindowProcと比べて、WM_CREATEやWM_CLOSEを処理していないことに注目してください。 インプレースアクティベーション用のウインドウで必要な動作は、 図形の描画やメニューの反応、及びマウスクリックぐらいで問題ないため、 WindowProcよりも構造が簡素になります。 WM_EXITSIZEMOVEを処理しているのは、 ウインドウスタイルにWS_THICKFRAMEを指定していたためです。 これによって、ウインドウのサイズ変更が可能になるため、 この変更が終わった瞬間をWM_EXITSIZEMOVEで検出し、 変更後のサイズをOnPosRectChangeでサイトに通知しています。 今回のオブジェクトに対応するコンテナでは、 これを機にコンテナ上のオブジェクトのサイズを更新し、 折り返しSetObjectRectsを呼び出します。 SendOnDataChangeを呼び出しているのは、コンテナのデフォルトハンドラがキャッシュしているイメージを更新するためです。 ウインドウのサイズが変更された場合は、このキャッシュも変更されなければなりません。

インプレースアクティベーションを行うオブジェクトは、 IOleInPlaceObjectを実装していなければなりません。 以下、各メソッドの実装を示します。

STDMETHODIMP CObject::GetWindow(HWND *phwnd)
{
	if (m_hwndInplace != NULL)
		*phwnd = m_hwndInplace;
	else
		*phwnd = m_hwnd;

	return S_OK;
}

GwtWindowはIOleWindowのメソッドですが、IOleInPlaceObjectはIOleWindowを継承しているので、 IOleWindowのメソッドも実装する必要があります。 GwtWindowは現在処理の対象となっているウインドウハンドルを返すべきであるため、 インプレースアクティベーション用のウインドウが作成されている場合はm_hwndInplaceを返し、 そうでない場合はm_hwndを返します。

STDMETHODIMP CObject::ContextSensitiveHelp(BOOL fEnterMode)
{
	return E_NOTIMPL;
}

ContextSensitiveHelpもIOleWindowのメソッドですが、基本的に呼ばれることはないのでE_NOTIMPLを返すだけで構いません。

STDMETHODIMP CObject::InPlaceDeactivate()
{
	UIDeactivate();

	if (m_pInPlaceSite != NULL) {
		m_pInPlaceSite->OnInPlaceDeactivate();
		m_pInPlaceSite->Release();
		m_pInPlaceSite = NULL;
	}

	if (m_hwndInplace != NULL) {
		DestroyWindow(m_hwndInplace);
		m_hwndInplace = NULL;
		m_pClientSite->OnShowWindow(FALSE);
	}

	return S_OK;
}

InPlaceDeactivateは、オブジェクトのインプレースアクティベーションを解除する目的で呼ばれます。 インプレースアクティベーションを解除するということは、UIアクティベーションも解除することを意味するため、 まずUIDeactivateを呼び出してそれを行います。 このメソッドの実装については次節で取り上げます。 続いてインプレースアクティベーションの解除ですが、 これはIOleInPlaceSite::OnInPlaceDeactivateを呼び出して解除の通知を送り、 インプレースアクティベーション用のウインドウを破棄すれば問題ありません。 ウインドウが破棄されればウインドウは非表示になりますから、 OnShowWindowでそれを通知しておきます。 なお、インプレースアクティベーションの解除は、IOleObject::DoVerbにOLEIVERB_HIDEを指定して行われることがあるため、 この際にはInPlaceDeactivateを明示的に呼び出すようにしておきます。

STDMETHODIMP CObject::ReactivateAndUndo()
{
	return E_NOTIMPL;
}

ReactivateAndUndoは、基本的に呼ばれることがないためE_NOTIMPLを返すだけで構いません。

STDMETHODIMP CObject::SetObjectRects(LPCRECT lprcPosRect, LPCRECT lprcClipRect)
{
	MoveWindow(m_hwndInplace, lprcPosRect->left, lprcPosRect->top, lprcPosRect->right - lprcPosRect->left, lprcPosRect->bottom - lprcPosRect->top, TRUE);
	
	return S_OK;
}

SetObjectRectsは、オブジェクトのサイズをlprcPosRectの値に設定するために呼び出すことができますが、 どちらかというと、オブジェクトがIOleInPlaceSite::OnPosRectChangeを呼び出した際に折り返し呼ばれることが多いと思われます。 ただし、WordやExcelでは、オブジェクトのデータの変更を検出した際にも呼び出すようになっています。 基本的にはウインドウサイズを変更するだけでよいと思われますが、 WordやExcelの場合は一点注意すべきことがあります。 それは、このサイズ変更によってウインドウのサイズが小さくなっていくという点です。 WordやExcelはオブジェクトのサイズをHIMETRIC単位で受け取り、 それをデバイス単位に変換してSetObjectRectsを呼び出すはずですが、 この変換の方法がコンテナの使用している方法と関連がない場合は、 変換結果に誤差が生じます。 これにより、オブジェクトのサイズは小さくなってしまいます。

Undo操作について

コンテナが実装するIOleInPlaceSite、及びオブジェクトが実装するIOleInPlaceObjectは、 Undo操作に関するメソッドが含まれています。 今回のサンプルではこのようなUndo操作をサポートしていませんが、 実際にどのような使用例があるのかを理解するために、Excelを例に取り上げて説明します。

まず、コンテナがExcelをインプレースアクティベーションしたとします。 これにより、コンテナ上にExcelのワークシートが表示されるため、 どこかの位置に何らかの文字を入力したとします。 このとき、Ctrl + Zを押せば入力した文字はなくなることになりますが、 これはExcelがUndo情報を維持しているから起こることです。 このようなUndo情報を削除したい場合は、IOleObject::DoVerbにOLEIVERB_DISCARDUNDOSTATEを指定します。 Undo操作を行いたい場合は、IOleInPlaceObject::ReactivateAndUndoを呼び出します。

続いて、Excelがオブジェクトをインプレースアクティベーションした例を考えます。 もし、ユーザーがオブジェクトの外をクリックした場合は、 オブジェクトのインプレースアクティベーションが解除されることになりますが、 この後にCtrl + Zを押せば再びアクティベーションが行われるはずです。 これはExcelがUndo情報を保存しているからであり、 これを削除したい場合はIOleInPlaceSite::DiscardUndoStateを呼び出します。 Excelではオブジェクトの作成とインプレースアクティベーションを同時に行いますが、 この時にオブジェクトがIOleInPlaceSite::DeactivateAndUndoを呼び出すと、 インプレースアクティベーションの解除だけでなく、オブジェクトの作成もなかったことにすることができます。



戻る