EternalWindows
OLE埋め込み / サーバーとしての設定

今回から、OLEコンテナに使用されるOLEサーバーを作成していきます。 このようなサーバーは、IOleObjectを実装したオブジェクトを作成しており、 このオブジェクトがコンテナに埋め込まれることになります。 次の図は、今回作成したオブジェクトがExcelに埋め込まれた様子を示しています。

挿入タブの「オブジェクト」という項目を選択した場合は、オブジェクトを埋め込むためのダイアログが表示されます。 今回作成したOLEサーバーをレジストリに登録している場合、 そのダイアログに"Sample Object"という項目が追加されているため、これを選択するようにします。 これにより、OLEサーバーが作成したオブジェクトが、コンテナアプリケーションに埋め込まれることになります。 上図ではコンテナとしてExcelを使用していますが、もちろん前節で作成した自作のコンテナを使用しても問題ありません。 オブジェクトの動作内容としては、クリックした位置に図形を描画するという単純なものです。 「編集」という項目にアクセスすれば、描画する図形を長方形か楕円か選択することができます。 オブジェクトのウインドウを閉じた場合は無条件に描画結果を保存するようにしていますが、 「ファイル」項目の「オブジェクトの更新」という項目からも保存は可能です。 このような項目は、ペイントやワードパッドを埋め込んだ場合も確認することができます。 オブジェクトのウインドウを再び表示したい場合は、 コンテナ上に描画されたオブジェクトのイメージをダブルクリックします。

サーバーがOLEサーバーの条件を満たすためには、何よりもまずオブジェクトを作成しなければなりません。 後の節で説明するように、このオブジェクトはIOleObject、IDataObject、IPersistStorageを実装しておく必要があります。 次に、サーバーはコンテナがオブジェクトを取得できるように、クラスオブジェクトの登録を行います。 これで、コンテナはCoCreateInstanceでオブジェクトを作成できるようになりますが、 通常コンテナはオブジェクトをOleUIInsertObject経由で作成することになっていたはずです。 よって、このダイアログにオブジェクトの名前が追加されるように、レジストリへの登録も行う必要があります。 今回のWinMainでは、オブジェクトの作成やクラスオブジェクトの登録、さらにレジストリへの登録といった作業をまとめて行っています。

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	int           nResult;
	DWORD         dwRegister = 0;
	BOOL          bEmbedding;
	CClassFactory classFactory;

	OleInitialize(NULL);
	
	g_pObject = new CObject;
	if (g_pObject == NULL) {
		OleUninitialize();
		return 0;
	}

	if ((lstrcmpA(lpszCmdLine, "-RegServer") == 0) || (lstrcmpA(lpszCmdLine, "/RegServer") == 0)) {
		if (RegisterServer())
			MessageBox(NULL, TEXT("登録に成功しました。"), TEXT("OK"), MB_OK);
		else
			MessageBox(NULL, TEXT("登録に失敗しました。"), NULL, MB_ICONWARNING);
		OleUninitialize();
		return 0;
	}
	else if ((lstrcmpA(lpszCmdLine, "-UnregServer") == 0) || (lstrcmpA(lpszCmdLine, "/UnregServer") == 0)) {
		UnregisterServer();
		MessageBox(NULL, TEXT("登録を解除しました。"), TEXT("OK"), MB_OK);
		OleUninitialize();
		return 0;
	}
	else if (lstrcmpA(lpszCmdLine, "-Embedding") == 0 || (lstrcmpA(lpszCmdLine, "/Embedding") == 0)) {
		CoRegisterClassObject(CLSID_SampleObject, static_cast<IClassFactory *>(&classFactory), CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &dwRegister);
		nCmdShow = SW_HIDE;
		bEmbedding = TRUE;
	}
	else
		bEmbedding = FALSE;

	nResult = g_pObject->Run(hinst, nCmdShow, bEmbedding);
	g_pObject->Release();

	if (bEmbedding)
		CoRevokeClassObject(dwRegister);

	OleUninitialize();

	return nResult;
}

コンテナがオブジェクトを埋め込むためには、何よりもまずオブジェクトの情報がレジストリに登録されている必要があります。 この登録処理は、コマンドラインに-RegServerや/RegServerを指定することで可能です。 OleUIInsertObjectなどでサーバーが起動された場合は、コマンドラインに-Embeddingが格納されているはずです。 この場合は、オブジェクトがコンテナに埋め込まれるということで、 コンテナがオブジェクトを取得できるようにCoRegisterClassObjectを呼び出します。 この関数は内部でCClassFactory::CreateInstanceを呼び出し、g_pObjectをコンテナに対して返します。 オブジェクトが埋め込まれる場合は、ウインドウを最初から表示すべきではないと思うため、 nCmdShowにSW_HIDEを指定するようにしています。 サーバーが通常のアプリケーションとして起動された場合は特に行うことはありませんが、 オブジェクトの情報を登録するようにRegisterServerを呼び出しています。 つまり、一度サーバーを通常のアプリケーションとして起動しなければ、 オブジェクトを埋め込むことができないようになっています。

RegisterServerで書き込まれる情報を次に示します。 HKEY_CLASSES_ROOT以下が対象となるため、管理者でなければ書き込みは失敗することになります。

HKEY_CLASSES_ROOT
  CLISD
    <独自のCLSID> (既定) = オブジェクトの名前
      InprocServer32 (既定) = ole32.dll
      LocalServer32 (既定) = サーバープロセスのフルパス
      ProgID (既定) = 独自のProgID
      Verb
        0 (既定) = &編集,0,2
        1 (既定) = &開く,0,2
 
 <独自のProgID> (既定) = オブジェクトの名前
    CLSID (既定) = オブジェクトのCLSID
        Insertable

OLEサーバーといってもそれはCOMサーバーの一種であり、 インプロセスサーバーかアウトプロセスサーバーかのどちらかの形態をとります。 今回のように、DLLでない1つのプロセスの場合はアウトプロセスサーバーになりますから、 LocalServer32にサーバーのフルパスを書き込むことになります。 ただし、OLEサーバーの場合はコンテナとの通信の際に、デフォルトハンドラという仲介が必要になりますから、 それを実装しているole32.dllをInprocServer32に書き込むようにします。 独自のProgID以下にInsertableキーを作成していれば、オブジェクトの名前がOleUIInsertObjectのダイアログに表示されます。

Verbキー以下には、オブジェクトがサポートするVerbの情報を書き込むようにします。 この場合のVerbというのは、コンテナがオブジェクトを操作するためのメニュー項目であり、 コンテナがオブジェクト上で右クリックを行った場合に確認できます。 VerbはIOleObject::EnumVerbsを実装することで返すことができますが、 この場合はIEnumOLEVERBを実装したオブジェクトが別途必要になるため、できれば避けて通りたいものです。 よって、レジストリにVerbの情報を書き込むようにしています。 Verbの指定はインデックス=Verbの情報という形であり、 情報の順序は、項目の名前、メニューフラグ、Verbフラグという具合になります。 メニューフラグの0はMF_STRING | MF_ENABLED | MF_UNCHECKEDの値であり、 Verbフラグの2はOLEVERBATTRIB_ONCONTAINERMENUの値になります。

OLEサーバーを作成する場合は、オブジェクト(CObject)の実装を考えるだけでなく、 サーバーにどのような機能を持たせるかも考えなければなりません。 今回のサーバーの機能は、マウスクリックが行われた場合に図形を描画するという単純なものであり、 描画する図形はメニューから長方形、または楕円の2種類を選択できます。 もちろん、このような機能を持つサーバーには実用性がありませんから、 実際の開発ではコンテナが埋め込みたくなるような、優れた機能を実装しておく必要があります。 メニュー項目が選択された際の処理は次のようになります。

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

	if (nId == ID_RECTANGLE)
		m_bDrawRectangle = TRUE;
	else if (nId == ID_ELLIPSE)
		m_bDrawRectangle = FALSE;
	else if (nId == ID_EXIT)
		PostMessage(m_hwnd, WM_CLOSE, 0, 0);
	else if (nId == ID_UPDATE)
		SaveObject();
	else
		;

	return 0;
}

ID_RECTANGLEは「Rectangle」という項目のIDであり、 この場合は長方形が描画されるようにTRUEを格納します。 一方、ID_ELLIPSEは「Ellipse」という項目のIDであり、 この場合は楕円が描画されるようにFALSEを格納します。 ID_EXITは「終了」という項目のIDであり、 これはWM_CLOSEでプロセスの終了を促せばよいでしょう。 ID_UPDATEは「xxxの更新」という項目のIDであり、 これはIOleObject::SetHostNamesで動的に追加されることになります。 この場合は、オブジェクトのデータを保存するためにSaveObjectを呼び出しています。

void CObject::SaveObject()
{
	if (m_bEmbedding) {
		m_pClientSite->SaveObject();
		if (m_pAdviseHolder != NULL)
			m_pAdviseHolder->SendOnSave();
	}
}

オブジェクトがコンテナに埋め込まれているかを確認することは重要です。 埋め込まれていない場合はデータを保存する必要がありませんし、 m_pClientSiteも初期化されていないからです。 IClientSite::SaveObjectを呼び出せば、コンテナに保存の処理に入ることを通知でき、 これを検出したコンテナは折り返しオブジェクトのIPersistStorage::Saveを呼び出します。 よって、保存のための具体的な処理はこのSaveで行うことになります。 m_pAdviseHolderがNULLでない場合は、コンテナとアドバイズ接続を確立していることを意味するため、 必要に応じてコンテナに対して通知を送ることになります。 データの保存における通知は、IOleAdviseHolder::SendOnSaveに相当します。

マウスの左ボタンが押された場合は、新しい図形が描画されるようにします。

case WM_LBUTTONDOWN: {
	int  x, y;
	RECT rc;
	
	if (m_nShapeCount >= m_nShapeMaxCount) {
		MessageBox(hwnd, TEXT("最大数に達しました。"), TEXT("OK"), MB_OK);
		return 0;
	}

	x = LOWORD(lParam);
	y = HIWORD(lParam);
	
	SetRect(&rc, x, y, x + 70, y + 30);
	m_shape[m_nShapeCount].rc = rc;
	m_shape[m_nShapeCount].bRectangle = m_bDrawRectangle;
	m_nShapeCount++;
	
	InvalidateRect(hwnd, &rc, FALSE);
	
	if (m_bEmbedding && m_pDataAdviseHolder != NULL)
		m_pDataAdviseHolder->SendOnDataChange(static_cast<IDataObject *>(this), 0, DVASPECT_CONTENT);
	
	m_bDirty = TRUE;

	return 0;
}

m_nShapeCountは描画された図形の数をカウントしており、これが描画できる最大数に到達している場合は描画しないようにします。 m_shapeは描画された図形の情報を維持する配列であり、rcに描画すべき位置を格納し、bRectangleに長方形かどうかフラグを格納します。 InvalidateRectでWM_PAINTを生成することにより、今回のクリックで作成された図形が描画されることになります。 オブジェクトが埋め込まれていて、さらにアドバイズ接続が確立されている場合は、 SendOnDataChangeでコンテナにデータの変更を通知します。 これにより、デフォルトハンドラがオブジェクトのCObject::GetDataを呼び出すようになり、 オブジェクトのイメージがコンテナ側でキャッシュされるようになるため、 コンテナがOleDrawでイメージを描画できるようになります。 オブジェクトを保存する際には、現在ダーティ状態(データが変更された状態)になっているかを知りたいことがあるため、 m_bDirtyにTRUEを格納しておきます。

WM_PAINTでは、DrawShapeという自作メソッドを呼び出して図形を描画します。

void CObject::DrawShape(HDC hdc)
{
	int  i;
	RECT rc;

	for (i = 0; i < m_nShapeCount; i++) {
		rc = m_shape[i].rc;
		if (m_shape[i].bRectangle)
			Rectangle(hdc, rc.left, rc.top, rc.right, rc.bottom);
		else
			Ellipse(hdc, rc.left, rc.top, rc.right, rc.bottom);
	}
}

図形の数はm_nShapeCountに格納されているため、この数だけ描画を行うことになります。 bRectangleがTRUEの場合はRectangleで長方形を描画し、 FALSEの場合はEllipseで楕円を描画します。

ウインドウのサイズが変更された場合は、WM_EXITSIZEMOVEが送られます。

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

オブジェクトが埋め込まれている場合は、ウインドウのサイズが変更されたことをコンテナに通知するべきといえます。 理由は、ウインドウのサイズが変更された場合は、コンテナ上に描画されたオブジェクトのイメージのサイズも変更されるべきだからです。 IDataAdviseHolder::SendOnDataChangeはサイズ変更を通知するメソッドではありませんが、 これによってコンテナが維持しているキャッシュが更新されるため、 コンテナ上のオブジェクトのイメージも更新されるはずです。

ウインドウが閉じられた場合は、WM_CLOSEが送られます。

case WM_CLOSE:
	if (m_bEmbedding) {
		BOOL bDontSave = (BOOL)wParam;
		if (!bDontSave)
			SaveObject();
		m_pClientSite->OnShowWindow(FALSE);
		if (m_pAdviseHolder != NULL)
			m_pAdviseHolder->SendOnClose();
	}
	break;

bDontSaveがFALSEである場合は、SaveObjectを呼び出してオブジェクトを保存するようにします。 通常、WM_CLOSEのWPARAMは常に0なのですが、 後の節で説明するIOleObject::CloseではWPARAMを1に指定してWM_CLOSEを送信することがあり、 そのような場合はデータを保存しないようにします。 ウインドウの破棄に伴ってウインドウが表示されなくなるため、 IOleClientSite::OnShowWindowにFALSEを指定してこれを通知します。 アドバイズ接続が確立されている場合は、IOleAdviseHolder::SendOnCloseでクローズの通知も行います。

デバイス単位とHIMETRIC単位

OLEにおける埋め込みでは、デバイス単位とHIMETRIC単位の変換が頻繁に生じます。 たとえばコンテナは、オブジェクトのサイズを取得したいときにIOleObject::GetExtentを呼び出しますが、 このメソッドは0.01mmを基準としたHIMETRIC単位でサイズを返すようになっています。 この単位ではオブジェクトを描画する場合などで不都合になるため、 ピクセル数を表すデバイス単位に変換する必要があります。 次に、この例を示します。

void CSite::HIMETRICtoDP(LPSIZEL lpSizel)
{
	HDC hdc;
	const int HIMETRIC_INCH = 2540;

	hdc = GetDC(NULL);
	lpSizel->cx = lpSizel->cx * GetDeviceCaps(hdc, LOGPIXELSX) / HIMETRIC_INCH;
	lpSizel->cy = lpSizel->cy * GetDeviceCaps(hdc, LOGPIXELSY) / HIMETRIC_INCH;
	ReleaseDC(NULL, hdc);
}

これは、インチをベースにした計算方法です。 1インチは24.5mmで表すことができ、これをHIMETRIC単位で表すと2540になります。 lpSizelにはHIMETRIC単位のサイズが格納されており、これを2540で割ったならば、 lpSizel->cx(cy)が何インチに相当するかを求めることができます。 ただし、インチだけではそれが何ピクセルに相当するのかが分かりませんから、 1インチ当たりのピクセル数を表すLOGPIXELSX(Y)を掛けるようにします。 なお、上記コードを基に、デバイス単位からHIMETRIC単位へ変換するコードは次のようになります。

void CObject::DPtoHIMETRIC(LPSIZEL lpSizel)
{
	HDC hdc;
	const int HIMETRIC_INCH = 2540;
	
	hdc = GetDC(NULL);
	lpSizel->cx = lpSizel->cx * HIMETRIC_INCH / GetDeviceCaps(hdc, LOGPIXELSX);
	lpSizel->cy = lpSizel->cy * HIMETRIC_INCH / GetDeviceCaps(hdc, LOGPIXELSY);
	ReleaseDC(NULL, hdc);
}

HIMETRICtoDPとの変更点は、HIMETRIC_INCHとGetDeviceCapsの順番が逆になっている点のみです。

通常、デバイス単位とHIMETRIC単位の変換は、上記した方法が使用されるものであると思われますが、 WordやExcelの場合は残念ながら異なる方法が使用されているようです。 WordやExcelのオブジェクトを埋め込む場合や、WordやExcelに埋め込まれる場合は、次に示す方法を使用することになります。

void CSite::HIMETRICtoDP(LPSIZEL lpSizel)
{
	HDC hdc;
	int nWidthMM, nHeightMM, nWidthPixel, nHeightPixel;
	
	hdc = GetDC(NULL);
	nWidthMM = GetDeviceCaps(hdc, HORZSIZE);
	nHeightMM = GetDeviceCaps(hdc, VERTSIZE);
	nWidthPixel = GetDeviceCaps(hdc, HORZRES);
	nHeightPixel = GetDeviceCaps(hdc, VERTRES);
	ReleaseDC(NULL, hdc);
	
	lpSizel->cx = lpSizel->cx * nWidthPixel / (nWidthMM * 100);
	lpSizel->cy = lpSizel->cy * nHeightPixel / (nHeightMM * 100);
}

これは、ディスプレイの幅(及び高さ)をベースにした計算方法です。 GetDeviceCapsにHORZSIZEを指定すればディスプレイの幅をmm単位で取得することができ、 これに100を掛ければディスプレイの幅をHIMETRIC単位で表すことができます。 nWidthMM * 100がディスプレイの幅ということは、同じHIMETRIC単位のlpSizel->cxがそれを上回ることはないはずであり、 もし両者を割って答えが1になるならば、 lpSizel->cxはディスプレイの幅をピクセル単位で表した値と同一であると考えてよいはずです。 よって、ピクセル単位の幅であるnWidthPixelを掛けるようにしています。 上記コードを基に、デバイス単位からHIMETRIC単位へ変換するコードは次のようになります。

void CObject::DPtoHIMETRIC(LPSIZEL lpSizel)
{
	HDC hdc;
	int nWidthMM, nHeightMM, nWidthPixel, nHeightPixel;
	
	hdc = GetDC(NULL);
	nWidthMM = GetDeviceCaps(hdc, HORZSIZE);
	nHeightMM = GetDeviceCaps(hdc, VERTSIZE);
	nWidthPixel = GetDeviceCaps(hdc, HORZRES);
	nHeightPixel = GetDeviceCaps(hdc, VERTRES);
	ReleaseDC(NULL, hdc);
	
	lpSizel->cx = lpSizel->cx * (nWidthMM * 100) / nWidthPixel;
	lpSizel->cy = lpSizel->cy * (nHeightMM * 100) / nHeightPixel;
}

HIMETRICtoDPとの変更点は、nWidthMM * 100とnWidthPixelの順番が逆になっている点のみです。



戻る