EternalWindows
バンドオブジェクト / ウインドウ処理

日頃IEを使用していると、Microsoft以外の開発者によって作成されたウインドウを見かけることがあります。 これは、バンドオブジェクトと呼ばれるDLLによって提供されており、 IEの機能や操作性を拡張することを目的に設計されています。 ウインドウがツールバーのような役割を果たす場合は、IEツールバーと呼ばれるようなこともありますが、 そのIEツールバーと呼ばれるものの正体がバンドオブジェクトということになります。 こうしたウインドウの表示/非表示は、IEの「表示」メニューから行うことができます。

バンドオブジェクトにもいくつかの種類があり、 「ツール バー」から確認できるバンドオブジェクトはツールバンドと呼ばれます。 チェックマークが付いているオブジェクトは現在表示されていることを意味し、 たとえば上記ではGoogleツールバーが表示されていることを確認できます。 「エクスプローラ バー」から確認できるオブジェクトはインフォバンドと呼ばれ、 ウインドウの左端か下端に表示されます。 バンドオブジェクトにはデスクバンドと呼ばれる種類もありますが、これはタスクバー上の「ツール バー」メニューに列挙されるものを指します。 バンドオブジェクトの種類はウインドウを表示する位置を規定するものであり、 ウインドウがどのような動作を行うかは開発者が自由に決めることができます。

今回作成するバンドオブジェクトはツールバンドとし、 次のような外観を持つものとします。

「保存」という名前のボタンが押されるとダイアログが表示され、 そこで選択したファイルに現在のWebページがビットマップとして保存されます。 エディットコントロールには既定の保存先とするフォルダパスを入力できますが、 空白でも問題ありません。

バンドオブジェクトの正体はCOMオブジェクトを実装したDLLであり、 このDLLがIEのプロセスにロードされることによって、バンドオブジェクトのウインドウが表示される仕組みになっています。 バンドオブジェクトが実装すべきインターフェースは、IDeskBand、IObjectWithSite、IPersistStreamとされているため、 これら3つを継承したクラスを定義します。

class CToolBand : public IDeskBand, public IObjectWithSite, public IPersistStream, public IInputObject
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();
	
	STDMETHODIMP GetWindow(HWND *phwnd);
	STDMETHODIMP ContextSensitiveHelp(BOOL fEnterMode);
	STDMETHODIMP ShowDW(BOOL bShow);
	STDMETHODIMP CloseDW(DWORD dwReserved);
	STDMETHODIMP ResizeBorderDW(LPCRECT prcBorder, IUnknown *punkToolbarSite, BOOL fReserved);
	STDMETHODIMP GetBandInfo(DWORD dwBandID, DWORD dwViewMode, DESKBANDINFO *pdbi);
	
	STDMETHODIMP SetSite(IUnknown *pUnkSite);
	STDMETHODIMP GetSite(REFIID riid, void **ppvSite);

	STDMETHODIMP GetClassID(CLSID *pClassID);
	STDMETHODIMP IsDirty();
	STDMETHODIMP Load(IStream *pStm);
	STDMETHODIMP Save(IStream *pStm, BOOL fClearDirty);
	STDMETHODIMP GetSizeMax(ULARGE_INTEGER *pcbSize);

	STDMETHODIMP UIActivateIO(BOOL fActivate, MSG *pMsg);
	STDMETHODIMP HasFocusIO(VOID);
	STDMETHODIMP TranslateAcceleratorIO(LPMSG lpMsg);

	// 独自のメソッドとメンバ変数は省略
};

IDeskBandのメソッドはGetBandInfoだけですが、このインターフェースはIOleWindowとIDockingWindowを継承しているので、 それらのメソッドも実装する必要があります。 SetSiteとGetSiteはIObjectWithSiteのメソッドです。 IsDirtyからGetSizeMaxまでがIPersistStreamのメソッドであり、 このインターフェースはIPersistを継承しているのでGetClassIDも実装します。 UIActivateIOからTranslateAcceleratorIOはIInputObjectのメソッドです。 このインターフェースは必須ではありませんが、今回作成するバンドオブジェクトにとっては意味があります。

バンドオブジェクトはIEの中で動作するオブジェクトであり、 ときとしてIEの機能を利用したいことがあります。 これを可能にするには、IEがオブジェクトに対してIEのインターフェースを渡せなければならないため、 オブジェクトはIObjectWithSiteを実装しておくことになります。 これにより、IEはIObjectWithSite::SetSiteを通じて自身のインターフェースを渡せるようになります。

STDMETHODIMP CToolBand::SetSite(IUnknown *pUnkSite)
{
	HRESULT hr = S_OK;
	
	if (m_pSite != NULL){
		m_pSite->Release();
		m_pSite = NULL;
	}

	if (pUnkSite != NULL) {
		HWND hwndParent;
		
		IUnknown_QueryService(pUnkSite, SID_SWebBrowserApp, IID_PPV_ARGS(&m_pWebBrowser2));
		if (!IsIE()) {
			m_pWebBrowser2->Release();
			return E_FAIL;
		}

		IUnknown_GetWindow(pUnkSite, &hwndParent);
		RegisterAndCreateWindow(hwndParent);

		hr = pUnkSite->QueryInterface(IID_PPV_ARGS(&m_pSite));
	}
	else {
		if (m_pWebBrowser2 != NULL) {
			m_pWebBrowser2->Release();
			m_pWebBrowser2 = NULL;
		}
		
		if (m_haccel != NULL) {
			DestroyAcceleratorTable(m_haccel);
			m_haccel = NULL;
		}
	}

	return hr;
}

m_pSiteは、IEから渡されたpUnkSiteを保存しているメンバ変数です。 2回目以降にSetSiteが呼ばれた場合は、保存しておいたSiteを開放すべきとされているためReleaseを呼び出しています。 pUnkSiteがNULLでない場合は、バンドオブジェクトの作成を行うべきことを意味しますが、 その前にIEを操作するIWebBrowser2を取得しておきます。 このためには、pUnkSiteからIServiceProviderを取得し、 IServiceProvider::QueryServiceで別のオブジェクト(IWebBrowser2)を照会するようにしますが、 IUnknown_QueryServiceを呼び出せばまとめて行うことができます。 pUnkSiteのQueryInterfaceからはIWebBrowser2を取得できないことに注意してください。 IWebBrowser2を取得したらIsIEという自作メソッドを呼び出して、ツールバンドをロードしたプロセスがIEであるかを確認します。 エクスプローラーがロードしている場合はここでE_FAILが返ることになり、 ツールバンドは表示されません。 IEにロードされていることが分かったら、pUnkSiteからIOleWindowを取得してIOleWindow::GetWindowを呼び出すことになりますが、 これはIUnknown_GetWindowでラッピングすることができます。 取得できるウインドウはレバーコントロール(ReBarWindow32)であり、 これから作成するバンドオブジェクトの親ウインドウにすることができます。 RegisterAndCreateWindowはバンドオブジェクトのウインドウを作成する自作関数であり、 ウインドウクラスの登録も併せて行います。 IEから渡されたpUnkSiteはメンバ変数として保存する必要がありますが、 単純にm_pSite = pUnkSiteにしていないのには理由があります。 これついては、次節で説明します。 pUnkSiteがNULLの場合はバンドオブジェクトが破棄されることを意味しているため、 ここでクリーンアップ処理を行うことになります。 CToolBand::GetSiteについては、保存しておいたm_pSiteのQueryInterfaceを呼び出すだけでよいようですが、 実際には呼ばれることがないようです。

バンドオブジェクトはC++のオブジェクトであると同時に、IE上に表示される1つのウインドウでもあります。 IEはこのウインドウを必要に応じて操作できなければならないため、 バンドオブジェクトはUI系のメソッドを持ったIDeskBandを実装していなければなりません。

STDMETHODIMP CToolBand::GetWindow(HWND *phwnd)
{
	*phwnd = m_hwndToolbar;

	return S_OK;
}

STDMETHODIMP CToolBand::ContextSensitiveHelp(BOOL fEnterMode)
{
	return E_NOTIMPL;
}
	
STDMETHODIMP CToolBand::ShowDW(BOOL bShow)
{
	if (m_hwnd != NULL)
		ShowWindow(m_hwnd, bShow ? SW_SHOW : SW_HIDE);

	return S_OK;
}

STDMETHODIMP CToolBand::CloseDW(DWORD dwReserved)
{
	if (m_hwnd != NULL) {
		ShowWindow(m_hwnd, SW_HIDE);
		DestroyWindow(m_hwnd);
	}

	return S_OK;
}

STDMETHODIMP CToolBand::ResizeBorderDW(LPCRECT prcBorder, IUnknown *punkToolbarSite, BOOL fReserved)
{
	return E_NOTIMPL;
}

STDMETHODIMP CToolBand::GetBandInfo(DWORD dwBandID, DWORD dwViewMode, DESKBANDINFO *pdbi)
{
	if (pdbi == NULL)
		return E_INVALIDARG;

	if (pdbi->dwMask & DBIM_MINSIZE) {
		pdbi->ptMinSize.x = 200;
		pdbi->ptMinSize.y = 30;
	}

	if (pdbi->dwMask & DBIM_MAXSIZE)
		pdbi->ptMaxSize.y = -1;
	
	if (pdbi->dwMask & DBIM_INTEGRAL)
		pdbi->ptIntegral.y = 1;
	
	if (pdbi->dwMask & DBIM_ACTUAL) {
		pdbi->ptActual.x = 200;
		pdbi->ptActual.y = 30;
	}

	if (pdbi->dwMask & DBIM_TITLE)
		lstrcpyW(pdbi->wszTitle, L"");

	if (pdbi->dwMask & DBIM_MODEFLAGS)
		pdbi->dwModeFlags =  DBIMF_VARIABLEHEIGHT;

	if (pdbi->dwMask & DBIM_BKCOLOR)
		pdbi->dwMask &= ~DBIM_BKCOLOR;

	return S_OK;
}

GetWindowは、ツールバーのハンドルを返すようにします。 このメソッドは、SetSite(pUnkSite != NULL)が制御を返した後に呼ばれます。 ツールバーの親ウインドウ(m_hwnd)を返すのではないことに注意してください。 ContextSensitiveHelpは呼ばれないのでE_NOTIMPLを返すだけで構いません。 ShowDWは、バンドオブジェクトの表示状態を変更する際に呼ばれるため、 ShowWindowを呼び出して実際に表示状態を変更します。 CloseDWは、バンドオブジェクトが破棄すべき際に呼ばれるため、 ウインドウを非表示にしてからDestroyWindowで破棄します。 この後には、SetSite(pUnkSite == NULL)が呼ばれるようになっています。 ResizeBorderDWは呼ばれないのでE_NOTIMPLを返すだけで構いません。 GetBandInfoでは、DESKBANDINFO構造体にバンドオブジェクトの情報を格納します。 dwMaskにDBIM_MINSIZEが含まれる場合は、ptMinSizeにバンドオブジェクトの最小サイズを指定できます。 DBIM_MAXSIZEが含まれる場合は、ptMaxSizeにバンドオブジェクトの最大サイズを指定できます。 xメンバは初期化しないでよく、yメンバは通常-1を指定します。 DBIM_INTEGRALが含まれる場合は、ptIntegralにサイズ変更時の単位を指定できます。 xメンバは初期化しないでよく、yメンバは単位を指定します。 DBIM_ACTUALが含まれる場合は、ptActualにバンドオブジェクトの理想のサイズを指定できます。 DBIM_TITLEが含まれる場合は、wszTitleにバンドオブジェクトのタイトルを指定できます。 これを行わない場合はDBIM_TITLEを取り除くことができますが、 それではIEによって×ボタンが表示されなくなります。 よって、空の文字列でよいのでwszTitleを初期化しています。 DBIM_MODEFLAGSが含まれる場合は、dwModeFlagsにモードフラグを指定できます。 ptIntegralを初期化する場合は、DBIMF_VARIABLEHEIGHTを指定します。 DBIM_BKCOLORが含まれる場合は、crBkgndにバンドオブジェクトの背景色を指定できます。 しかし、今回はこれを行わないということでDBIM_BKCOLORを取り除いています。

バンドオブジェクトは、IPersistStreamも実装するべきとされています。 実際にはQueryInterfaceでIPersistStreamのIIDを返さなくても問題ないようですが、 念のため実装しておきます。

STDMETHODIMP CToolBand::GetClassID(CLSID *pClassID)
{
	*pClassID = CLSID_DeskBandSample;
	
	return S_OK;
}

STDMETHODIMP CToolBand::IsDirty()
{
	return E_NOTIMPL;
}

STDMETHODIMP CToolBand::Load(IStream *pStm)
{
	return S_OK;
}

STDMETHODIMP CToolBand::Save(IStream *pStm, BOOL fClearDirty)
{
	return S_OK;
}

STDMETHODIMP CToolBand::GetSizeMax(ULARGE_INTEGER *pcbSize)
{
	return E_NOTIMPL;
}

GetClassIDは、オブジェクトのCLSIDを返すようにします。 IsDirtyは呼ばれないのでE_NOTIMPLを返すだけで構いません。 Loadはオブジェクトを初めて表示した際に一度だけ呼ばれます。 これはバンドオブジェクトの中で最初に呼ばれるメソッドであり、 これがS_FALSE以外を返した後にSetSite(pUnkSite != NULL)が呼ばれます。 Loadでは保存しておいたデータをロードすることになりますが、 今回はそうしたデータが存在しないため単純にS_OKを返します。 もし、処理した場合はS_FALSEを返すようにしてください。 Saveは、オブジェクトの表示状態が変化した際に呼ばれます。 このメソッドはデータを保存するタイミングを通知するものであると思われますが、 オブジェクトが破棄される際には呼ばれないことに注意してください。 GetSizeMaxは呼ばれないのでE_NOTIMPLを返すだけで構いません。

SetSiteで呼び出していたIsIEの処理は、次のようになっています。

BOOL CToolBand::IsIE()
{
	BSTR bstr;
	BOOL bResult;

	m_pWebBrowser2->get_Name(&bstr);
	if (lstrcmpW(bstr, L"Windows Internet Explorer") == 0)
		bResult = TRUE;
	else
		bResult = FALSE;
	
	return bResult;
}

IWebBrowser2::get_Nameをエクスプローラの中で呼び出すと、"エクスプローラ"という文字列が返りますが、 IEの場合は"Windows Internet Explorer"が返ります。 よって、これを基に判定を行うことができます。 ちなみに、IEではなくWebBrowser コントロールを使用しているアプリケーションにロードされた場合は、 "Microsoft Web ブラウザー コントロール"が返ります。

SetSiteで呼び出していたRegisterAndCreateWindowの処理は、次のようになっています。

BOOL CToolBand::RegisterAndCreateWindow(HWND hwndParent)
{
	TCHAR      szClassName[] = TEXT("sample_toolband");
	WNDCLASSEX wc;
	DWORD      dwStyle;
	TBBUTTON   tbButton[] = {
		{STD_FILESAVE, ID_SAVE, TBSTATE_ENABLED, BTNS_BUTTON, {0}, 0, (INT_PTR)TEXT("保存")}
	};
	int nCount = sizeof(tbButton) / sizeof(tbButton[0]);
	
	if (!GetClassInfoEx(g_hinstDll, szClassName, &wc)) {
		wc.cbSize        = sizeof(WNDCLASSEX);
		wc.style         = 0;
		wc.lpfnWndProc   = WindowProcStatic;
		wc.cbClsExtra    = 0;
		wc.cbWndExtra    = 0;
		wc.hInstance     = g_hinstDll;
		wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
		wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
		wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
		wc.lpszMenuName  = NULL;
		wc.lpszClassName = szClassName;
		wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
		
		RegisterClassEx(&wc);
	}

	m_hwnd = CreateWindowEx(0, szClassName, TEXT(""), WS_CHILD | WS_TABSTOP | WS_CLIPCHILDREN, 0, 0, 0, 0, hwndParent, (HMENU)ID_BANDWINDOW, g_hinstDll, this);
	SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this);

	m_hwndToolbar = CreateToolbarEx(m_hwnd, WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | TBSTYLE_LIST | TBSTYLE_TRANSPARENT | CCS_NODIVIDER | CCS_NOPARENTALIGN, ID_TOOLBAR, 0, HINST_COMMCTRL, IDB_STD_SMALL_COLOR, tbButton, nCount, 0, 0, 0, 0, sizeof(TBBUTTON));
	dwStyle = (DWORD)SendMessage(m_hwndToolbar, TB_GETSTYLE, 0, 0) | TBSTYLE_FLAT;
	SendMessage(m_hwndToolbar, TB_SETSTYLE, 0, (LPARAM)dwStyle);

	m_hwndEdit = CreateWindowEx(0, TEXT("EDIT"), TEXT(""), WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, 60, 0, 250, 25, m_hwndToolbar, (HMENU)ID_EDIT, g_hinstDll, NULL);

	return TRUE;
}

コントロールではなく独自のウインドウを作成する場合は、事前にウインドウクラスの登録を行う必要があります。 GetClassInfoExがFALSEを返した場合は、第2引数の名前のウインドウクラスが登録されていないことを意味するため、 この場合にRegisterClassExを呼び出してウインドウクラスを登録します。 CreateWindowExでは、先に登録したウインドウクラスを指定してバンドオブジェクトのウインドウを作成します。 このときの親ウインドウハンドルは、レバーコントロールを識別するhwndParentを指定します。 ウインドウスタイルは子ウインドウを示すWS_CHILDを指定し、 Tabキーでフォーカスが回ってくるようにWS_TABSTOPを指定します。 また、親ウインドウの再描画の際に子ウインドウがちらつかないようにWS_CLIPCHILDRENを指定します。 SetWindowLongPtrでオブジェクトのアドレスをウインドウに関連付けているのは、 ウインドウプロシージャでオブジェクトを参照できるようにするためです。 wc.lpfnWndProcに指定しているのはstaticなウインドウプロシージャであるため、 無条件にオブジェクトを参照することはできないわけです。 CreateToolbarExでは、先に作成したウインドウの子としてツールバーを作成します。 TBSTYLE_LISTを指定した場合はボタンの文字列が横に表示され、 TBSTYLE_TRANSPARENTを指定した場合はツールバーが透明になります。 これにより、ツールバーの背景色がバンドの背景色と一致します。 CCS_NODIVIDERを指定した場合はツールバーの上線が消去され、 CCS_NOPARENTALIGNを指定した場合はツールバーが正しい位置に表示されます。 ツールバーをフラットにする場合は、現在のスタイルにTBSTYLE_FLATを加えてTB_SETSTYLEを送信するようにします。 最後のCreateWindowExでは、ツールバーの子としてエディットコントロールを作成しています。 ES_AUTOHSCROLLを指定することで、文字の入力が水平方向にスクロールされます。

WindowProcStaticはstaticなウインドウプロシージャであり、 メッセージの具体的な処理はこのメソッドで行いません。 あくまで、正規のWindowProcへの中継として機能します。

LRESULT CALLBACK CToolBand::WindowProcStatic(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	CToolBand *p = (CToolBand *)GetWindowLongPtr(hwnd, GWLP_USERDATA);
	
	if (p != NULL)
		return p->WindowProc(hwnd, uMsg, wParam, lParam);
	else
		return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

SetWindowLongPtrで関連付けておいたオブジェクトのアドレスをGetWindowLongPtrで取得します。 そして、これのWindowProcを呼び出すことで、 オブジェクト内でメッセージの処理を行えるようにします。 ウインドウの作成過程で送られるWM_NCCREATEの時点では、オブジェクトのアドレスを関連付けていないため、 そのような場合はDefWindowProcが呼ばれることになります。 WindowProcの実装は次のようになっています。

LRESULT CToolBand::WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg) {

	case WM_SETFOCUS:
		SetFocus(m_hwndEdit);
		return 0;
	
	case WM_SIZE:
		MoveWindow(m_hwndToolbar, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
		return 0;
	
	default:
		break;

	}

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

WM_SETFOCUSは、Tabキーでウインドウにフォーカスが割り当てられた際に送られます。 このときには、エディットコントールにフォーカスを割り当てるようにし、 キーを入力できる状態にしておきます。 WM_SIZEは、ウインドウが表示された際に送られます。 ここでは、ツールバーをバンドオブジェクト全体に表示されるようにMoveWindowを呼び出します。


戻る