EternalWindows
OLE埋め込み / ウインドウ処理

前節では、オブジェクトを作成する方法とオブジェクトから送られる通知の種類について説明しました。 今回は、作成したオブジェクトをコンテナがどのように扱っていくかについて見ていきます。 まず、コンテナはオブジェクトをウインドウ上に表示しなければならないため、 WM_PAINTでオブジェクトを描画する処理を行います。

case WM_PAINT: {
	HDC         hdc;
	PAINTSTRUCT ps;
	CSite       *p = m_pSite;

	hdc = BeginPaint(hwnd, &ps);

	while (p != NULL) {
		p->Draw(hdc);
		p = p->m_pNext;
	}

	EndPaint(hwnd, &ps);

	return 0;
}

オブジェクトは複数個作成されていることがあり、 そうした一連のオブジェクトの先頭はm_pSiteで識別されています。 m_pSiteは常に先頭を指すべきであるため、代わりとしてpという変数を用意し、 これをp->m_pNextとすることで次に参照するオブジェクトを決定します。 Drawという自作メソッドはオブジェクトを描画することが目的であり、 次のような実装になっています。

void CSite::Draw(HDC hdc)
{
	OleDraw(m_pOleObject, DVASPECT_CONTENT, hdc, &m_rc);

	if (m_bShowWindow) {
		HBRUSH hbr;
		hbr = CreateHatchBrush(HS_BDIAGONAL, RGB(128, 128, 128));
		SelectObject(hdc, hbr);
		SetBkMode(hdc, TRANSPARENT);
		Rectangle(hdc, m_rc.left, m_rc.top, m_rc.right, m_rc.bottom);
		DeleteObject(hbr);
	}
	
	if (m_bSelect)
		DrawFocusRect(hdc, &m_rc);
	else
		FrameRect(hdc, &m_rc, (HBRUSH)GetStockObject(BLACK_BRUSH));
}

OleDrawを呼び出すことで、オブジェクトのイメージを第4引数の位置に描画することができます。 m_bShowWindowがTRUEである場合は現在オブジェクトのウインドウが表示されていることを意味し、 そのような時にはオブジェクトにハッチパターンを描画することが推奨されています。 このハッチパターンというのは、ハッチブラシを割り当てた状態でRectangleを呼び出せば描画することができます。 SetBkModeで透過描画に設定しておかないと、OleDrawの結果が完全に上書きされてしまうことに注意してください。 m_bSelectはオブジェクトが現在選択されているかを表しており、 選択されている場合はDrawFocusRectで点線の枠を描画します。 一方、選択されていない場合はFrameRectで黒い線の枠を描画します。 なお、ここで言う選択とはオブジェクトをクリックした状態のことを意味します。

コンテナがオブジェクトをウインドウに表示する理由は、 オブジェクトのウインドウを必要に応じて開いて、それを編集することです。 このため、コンテナはオブジェクトを起動するための処理を用意しておく必要があります。 通常、この起動はダブルクリックを通じて行われるため、WM_LBUTTONDBLCLKを検出することになります。 このメッセージを検出するためには、ウインドクラスのクラススタイルにCS_DBLCLKSを指定しておかなければならないことに注意してください。

case WM_LBUTTONDBLCLK: {
	CSite *p;

	if (GetSiteFormPos(lParam, &p))
		p->RunObject(OLEIVERB_PRIMARY);

	return 0;
}

GetSiteFormPosという自作メソッドは、第1引数の位置に存在するオブジェクトを第2引数に返します。 オブジェクトが返った場合は、RunObjectという自作メソッドを呼び出します。

void CSite::RunObject(LONG lVerb)
{
	HWND        hwnd;
	IViewObject *pViewObject;
	
	m_pOleObject->Advise(static_cast<IAdviseSink *>(this), &m_dwConnection);

	m_pOleObject->QueryInterface(IID_PPV_ARGS(&pViewObject));
	pViewObject->SetAdvise(DVASPECT_CONTENT, 0, static_cast<IAdviseSink *>(this));
	pViewObject->Release();

	g_pContainer->GetWindow(&hwnd);
	m_pOleObject->DoVerb(lVerb, NULL, static_cast<IOleClientSite *>(this), 0, hwnd, &m_rc);
}

サイトがIAdviseSinkを実装している場合は、IOleObject::Adviseを呼び出して自身のアドレスをオブジェクトに渡します。 これにより、オブジェクトからの通知をIAdviseSinkで受け取ることができます。 第2引数の値は登録を解除する際に必要になるため、メンバ変数に保存しておきます。 IViewObject::SetAdviseを呼び出しているのは、オブジェクトの外観が変更された場合を検出するためです。 これによって、コンテナ上に描画されているオブジェクトのイメージを更新することができます。 IOleObject::DoVerbを呼び出せば、実際にオブジェクトを起動することができます。 第1引数はどのように起動するかを表す定数であり、 OLEIVERB_PRIMARYの場合はオブジェクト自身に決定させます。 今回の場合はオブジェクトのウインドウを開くことになりますが、 サイトがIOleInPlaceSiteを実装している場合は、インプレースアクティベーションが発生することになります。 第2引数は、起動の元になったメッセージ(今回はWM_LBUTTONDBLCLK)のMSG構造体を指定できますがNULLで問題ありません。 第3引数はサイトを指定するようにし、第4引数は0で構いません。 第5引数と第6引数はインプレースアクティベーションの際に意味を持つことになるため、 今回の場合はNULLを指定しても構いません。

作成したオブジェクトは、マウスで任意の位置へ移動できると便利であるといえます。 これはマウスの左ボタンを通じて行われるべきものであるため、WM_LBUTTONDOWNを検出します。

case WM_LBUTTONDOWN: {
	CSite *p = NULL;
	BOOL  bChangeSelect = TRUE;

	if (GetSiteFormPos(lParam, &p)) {
		if (p == m_pSiteMoving)
			bChangeSelect = FALSE;
		SetCapture(hwnd);
		m_pSiteMoving = p;
		m_pSiteMoving->SetClickPos(lParam);
	}

	if (bChangeSelect) {
		ChangeSelectObject(p);
		InvalidateRect(m_hwnd, NULL, TRUE);
	}

	return 0;
}

GetSiteFormPosがTRUEを返したということは、第1引数の位置にオブジェクトが存在していることを意味します。 このような場合は、SetCaptureを呼び出してウインドウ外のマウスメッセージも取得できるようにし、 選択したオブジェクト(移動中のオブジェクト)をメンバ変数に保存します。 また、クリックした位置を記憶するためにSetClickPosという自作メソッドも呼び出しておきます。 bChangeSelectは、今回のクリックによって選択されたオブジェクトが変化した場合にTRUEとなります。 具体的には、選択したオブジェクトが保存しておいたm_pSiteMovingと異なる場合、 もしくはオブジェクトが存在しない場所をクリックした場合にTRUEとなります。 このような場合は、以前まで選択されていたオブジェクトのフラグ(m_bSelect)をFALSEにし、 新しく選択されたオブジェクトのフラグをTRUEにするべきであるため、 ChangeSelectObjectという自作メソッドでそれを行います。

WM_LBUTTONDOWNでマウスのキャプチャを行った場合は、 マウスの移動時にオブジェクトの位置を移動させる必要があります。

case WM_MOUSEMOVE:
	if (GetCapture() != NULL) {
		m_pSiteMoving->MoveRect(lParam);
		InvalidateRect(m_hwnd, NULL, TRUE);
	}
	return 0;

GetCaptureがNULLでない値を返した場合は、現在キャプチャが行われていることを意味します。 このときには、m_pSiteMovingに移動中のオブジェクトが格納されているはずですから、 MoveRectという自作メソッドを呼び出してオブジェクトの位置を更新します。

マウスの左ボタンを離した場合は、WM_LBUTTONUPが送られます。

case WM_LBUTTONUP:
	ReleaseCapture();
	m_pSiteMoving = NULL;
	return 0;

左ボタンを離した場合はオブジェクトを移動する必要がなくなるため、 ReleaseCaptureを呼び出してマウスのキャプチャを解除します。 また、移動中のオブジェクトがなくなったということで、m_pSiteMovingにNULLを格納します。

オブジェクト上でマウスの右ボタンが押された場合は、ポップアップメニューを表示することになります。

case WM_RBUTTONDOWN: {
	CSite *p = NULL;
	POINT pt;
	HMENU hmenu;

	if (GetSiteFormPos(lParam, &p)) {
		if (p != m_pSiteMoving) {
			m_pSiteMoving = p;
			ChangeSelectObject(p);
			InvalidateRect(m_hwnd, NULL, TRUE);
		}

		hmenu = CreatePopupMenu();
		p->AddVerbMenu(hmenu, ID_VERBMIN, ID_VERBMAX);
		GetCursorPos(&pt);
		TrackPopupMenu(hmenu, 0, pt.x, pt.y, 0, hwnd, NULL);
		DestroyMenu(hmenu);
	}
	else {
		ChangeSelectObject(p);
		InvalidateRect(m_hwnd, NULL, TRUE);
	}
	
	return 0;
}

まず、対象となるオブジェクトを選択状態にし、次にp->AddVerbMenuで初期化したポップアップメニューをTrackPopupMenuで表示します。 AddVerbMenuの実装は次のようになっています。

void CSite::AddVerbMenu(HMENU hmenu, UINT uIdVerbMin, UINT uIdVerbMax)
{
	HMENU hmenuTmp;
	
	OleUIAddVerbMenu(m_pOleObject, NULL, hmenu, 0, uIdVerbMin, uIdVerbMax, FALSE, 0, &hmenuTmp);
}

ポップアップメニューに追加すべき項目は、オブジェクトがサポートするVerb(実行できるコマンド)でなければなりません。 このVerbはIOleObject::EnumVerbsを呼び出すことで取得できますが、OleUIAddVerbMenuを呼び出せば、 Verbの取得もメニュー項目の作成もまとめて行うことができます。 第4引数はオブジェクトに関する項目をメニューのどの位置に追加するかであり、先頭でよいのであれば0で問題ありません。 第5引数は追加する最初の項目のIDであり、2つ目以降の項目はこのIDを加算していったものになります。 ただし、そのIDは第6引数を超えることはありません。 追加される項目は基本的にオブジェクト間で共通となっており、 おそらく「編集」と「開く」という2つの項目を確認できるはずです。 ID_VERBMINは0として定義しているため、 「編集」という項目のIDは0になり、「開く」という項目のIDは1になります。 項目が選択された場合は、WM_COMMANDが送られます。

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

	if (nId == ID_OPEN)
		LoadObject();
	else if (nId == ID_SAVE)
		SaveObject();
	else if (nId == ID_INSERT)
		InsertObject();
	else if (nId >= ID_VERBMIN || nId <= ID_VERBMAX)
		m_pSiteMoving->RunObject(nId);
	else
		;
	InvalidateRect(m_hwnd, NULL, TRUE);
	
	return 0;
}

通知されたIDがID_VERBMINとID_VERBMAXの間であれば、ポップアップメニューの項目が選択されたことになるため、 項目のIDをRunObjectに指定します。 これにより、内部でIOleObject::DoVerbが実行されることになります。

ファイルからの埋め込み

コンテナアプリケーションがオブジェクトを埋め込む方法は、 OleUIInsertObjectの呼び出し以外にも存在します。 たとえば、ファイルパスを持っている場合はOleCreateFromFileを呼び出すことができますし、 IDataObjectを持っている場合はOleCreateFromDataを呼び出すことができます。 例として、D&Dされたファイルを埋め込む方法を取り上げます。

case WM_DROPFILES: {
	UINT  i, uCount;
	HDROP hDrop;
	WCHAR szFilePath[MAX_PATH];
	
	hDrop = (HDROP)wParam;
	uCount = DragQueryFileW(hDrop, (UINT)-1, NULL, 0);
	for (i = 0; i < uCount; i++) {
		DragQueryFileW(hDrop, i, szFilePath, sizeof(szFilePath) / sizeof(WCHAR));
		InsertObject(szFilePath);
	}
	DragFinish(hDrop);
	InvalidateRect(m_hwnd, NULL, TRUE);
	return 0;
}

アプリケーションがDragAcceptFilesを呼び出していれば、ファイルがドロップされた際にWM_DROPFILESが送られます。 ドロップされたファイルのパスはDragQueryFileで取得することができ、 このファイルパスをInsertObjectに指定して埋め込みを行います。

BOOL CContainer::InsertObject(LPWSTR lpszFilePath)
{
	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;
	
	if (lpszFilePath != NULL) {
		HRESULT hr;
		hr = OleCreateFromFile(CLSID_NULL, lpszFilePath, IID_IOleObject, OLERENDER_DRAW, NULL, 0, pStorage, (void **)&pOleObject);
		if (FAILED(hr)) {
			m_pRootStorage->DestroyElement(szObjectName);
			return FALSE;
		}
	}
	else {
		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;
}

このInsertObjectの実装は基本的に前節と同じですが、ファイルパスが指定された場合はOleCreateFromFileを呼び出しています。 これが成功した場合はIOleObjectを取得することができますから、後の処理はダイアログでオブジェクトを埋め込んだ場合と同様になります。



戻る