EternalWindows
シェル拡張 / デスクバンド
サンプルはこちら

デスクバンドは、タスクバー上に独自のウインドウを表示したい場合に使用します。 次に、デスクバンドの例を示します。

背景色がタスクバーと同じになっているため少し分かりにくいですが、 右側に表示されたウインドウがデスクバンドです。 上記ではクライアント領域に文字列を描画しているだけで面白みがありませんが、 たとえばクイック起動のようにツールバーを備えてもよいでしょう。 クイック起動とは、上図でスタートボタンの右横に表示されているウインドウであり、 これもデスクバンドの一種です。 デスクバンドの表示/非表示は、タスクバー上における右クリックメニューの「ツール バー」から行えます。

デスクバンドのウインドウの背景色は、常に同じであるべきとは限りません。 Windows Vista以降のWindowsでは、ウインドウの背景色を現在のテーマと一致させなければならないからです。

上図のようにテーマが有効になっている場合は、タスクバーの外観も変化しています。 そして、そのような時にはデスクバンドのウインドウの背景色も変化させるようにします。

デスクバンドのオブジェクトは、IDeskBand2、IObjectWithSite、IPersistStreamを実装する必要があります。 IDeskBand2はWindows Vistaから登場したインターターフェースであり、 それ以前のWindowsではIDeskBandを継承することになります。

class CDeskBand : public IDeskBand2, public IObjectWithSite, public IPersistStream
{
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 CanRenderComposited(BOOL *pfCanRenderComposited);
	STDMETHODIMP SetCompositionState(BOOL fCompositionEnabled);
	STDMETHODIMP GetCompositionState(BOOL *pfCompositionEnabled);
	
	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);

	CDeskBand();
	~CDeskBand();
	BOOL RegisterAndCreateWindow(HWND hwndParent);
	static LRESULT CALLBACK WindowProcStatic(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
	LRESULT WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

private:
	LONG     m_cRef;
	HWND     m_hwnd;
	BOOL     m_bCompositionEnabled;
	IUnknown *m_pSite;
};

GetWindowからGetCompositionStateまでがIDeskBand2のメソッドになります。 ただし、正確に言うとIDeskBand2はIDeskBand、IDockingWindow、IOleWindowを継承しているため、 それらのメソッドも上記に含まれています。 SetSiteとGetSiteがIObjectWithSiteのメソッドであり、 GetClassIDからGetSizeMaxがIPersistStreamのメソッドになります。

デスクバンドでは実装すべきメソッドが多いですが、呼ばれることになるメソッドは限られています。 次に、メソッドが呼ばれる順番を示します。

SetSite
GetWindow
GetBandInfo
ShowDW
GetWindow
GetClassID
CanRenderComposited
SetCompositionState

タスクバーからデスクバンドを表示した場合は、上記のメソッドが順に呼ばれることになります。 デスクバンドを閉じた場合は、GetClassID、CloseDW、SetSiteが呼ばれます。

SetSiteの実装は次のようになります。

STDMETHODIMP CDeskBand::SetSite(IUnknown *pUnkSite)
{
	if (m_pSite != NULL){
		m_pSite->Release();
		m_pSite = NULL;
	}

	if (pUnkSite != NULL) {
		HWND       hwndParent;
		IOleWindow *pOleWindow;

		pUnkSite->QueryInterface(IID_PPV_ARGS(&pOleWindow));
		pOleWindow->GetWindow(&hwndParent);
		pOleWindow->Release();
		
		RegisterAndCreateWindow(hwndParent);
		
		pUnkSite->QueryInterface(IID_PPV_ARGS(&m_pSite));
	}

	return S_OK;
}

SetSiteではデスクバンドとして表示するウインドウを作成します。 このウインドウは、タスクバー上のウインドウ(ReBarWindow32)の子でなければならないため、 まずはIOleWindow::GetWindowで親ウインドウのハンドルを取得します。 その後、RegisterAndCreateWindowという自作メソッドを呼び出して、デスクバンドのウインドウを作成しています。 IOleWindowはSetSiteの引数であるpUnkSiteから取得することでき、 このpUnkSiteはメンバ変数として保存しておく必要があります。 QueryInterfaceを呼び出せば参照カウントを増加したうえでm_pSiteに保存することができます。 pUnkSiteがNULLの場合はクリーンアップ処理を行うことになりますが、 今回は確保したメモリなどがないため何も行っていません。

IDeskBand2の中で呼ばれるメソッドを次に示します。

STDMETHODIMP CDeskBand::GetWindow(HWND *phwnd)
{
	*phwnd = m_hwnd;

	return S_OK;
}
	
STDMETHODIMP CDeskBand::ShowDW(BOOL bShow)
{
	if (m_hwnd != NULL)
		ShowWindow(m_hwnd, bShow ? SW_SHOW : SW_HIDE);

	return S_OK;
}

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

	return S_OK;
}

STDMETHODIMP CDeskBand::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)
		pdbi->dwMask &= ~DBIM_TITLE;

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

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

	return S_OK;
}

STDMETHODIMP CDeskBand::CanRenderComposited(BOOL *pfCanRenderComposited)
{
	*pfCanRenderComposited = TRUE;

	return S_OK;
}

STDMETHODIMP CDeskBand::SetCompositionState(BOOL fCompositionEnabled)
{
	m_bCompositionEnabled = fCompositionEnabled;
	
	InvalidateRect(m_hwnd, NULL, FALSE);
	UpdateWindow(m_hwnd);

	return S_OK;
}

GetWindowは、バンドオブジェクトのウインドウハンドルを返すだけで構いません。 ShowDWは引数の値を基にウインドウの表示状態を変更し、 CloseDWはウインドウを破棄するようにします。 GetBandInfoはバンドオブジェクトの情報を返すメソッドであり、 マスクに関連する各メソッドを初期化していきます。 ただし、関連するメンバを初期化したくない場合は、マスクを取り除くこともできます。 CanRenderCompositedは、テーマに応じた描画が行えるかどうかを引数に格納します。 SetCompositionStateは、引数として与えられた現在のテーマの状態をメンバに保存しておきます。 これがTRUEである場合は、テーマが有効になっていることを意味します。 また、InvalidateRectでWM_PAINTを生成し、UpdateWindowで直ちにWM_PAINTが処理されるようにします。

デスクバンドはウインドウとして表示され、 当然ながらウインドウプロシージャを持っています。 WM_PAINTでは、クライアント領域の塗りつぶしとテキストの描画を行っています。

case WM_PAINT: {
	HDC         hdc;
	RECT        rc;
	PAINTSTRUCT ps;
	COLORREF    crText = RGB(255, 0, 0);
	TCHAR       szText[] = TEXT("My DeskBand");
	
	hdc = BeginPaint(hwnd, &ps);
	GetClientRect(hwnd, &rc);

	if (m_bCompositionEnabled) {
		HTHEME       htheme;
		HDC          hdcPaint;
		HPAINTBUFFER hBufferedPaint;
		DTTOPTS      dttOpts;

		htheme = OpenThemeData(NULL, L"TASKBAND");
		hBufferedPaint = BeginBufferedPaint(hdc, &rc, BPBF_TOPDOWNDIB, NULL, &hdcPaint);

		DrawThemeParentBackground(m_hwnd, hdcPaint, &rc);

		ZeroMemory(&dttOpts, sizeof(DTTOPTS));
		dttOpts.dwSize  = sizeof(DTTOPTS);
		dttOpts.dwFlags = DTT_COMPOSITED | DTT_TEXTCOLOR;
		dttOpts.crText  = crText;
		DrawThemeTextEx(htheme, hdcPaint, 0, 0, szText, -1, DT_SINGLELINE | DT_CENTER | DT_VCENTER, &rc, &dttOpts);
		
		EndBufferedPaint(hBufferedPaint, TRUE);
		CloseThemeData(htheme);
	}
	else {
		FillRect(hdc, &rc, GetSysColorBrush(COLOR_MENU));

		SetTextColor(hdc, crText);
		SetBkMode(hdc, TRANSPARENT);
		DrawTextW(hdc, szText, -1, &rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
	}

	EndPaint(hwnd, &ps);
	return 0;
}

m_bCompositionEnabledがTRUEの場合は、テーマを利用した描画を行うことになります。 このためにはまず、テーマのハンドルをOpenThemeDataで取得しなければなりませんが、 使用するテーマはどれが最適なのかよく分かりません。 上記では"TASKBAND"としていますが、"BUTTON"でも問題はないようです。 BeginBufferedPaintはBufferedPaintと呼ばれるデバイスコンテキストを返す関数で、 このデバイスコンテキストに行った描画はEndBufferedPaintを呼び出すまでウインドウに反映されません。 また、BufferedPaintを使用しなければ、デスクバンドを綺麗に描画することができないように思えます。 DrawThemeParentBackgroundを呼び出せば、第1引数のウインドウの親ウインドウの背景が第2引数に描画されます。 これにより、デスクバンドの背景を親ウインドウと同じにすることができます。 DrawThemeTextExはテキストを描画する関数であり、DTTOPTS構造体を要求します。 BeginBufferedPaintにBPBF_TOPDOWNDIBを指定した場合はdwFlagsにDTT_COMPOSITEDを指定し、 crTextにテキストの色を指定する場合はDTT_TEXTCOLORを指定します。 テキストの周りにぼかしを表示したい場合は、DTT_GLOWSIZEを指定してiGlowSizeを初期化(たとえば10を指定)します。 m_bCompositionEnabledがFALSEの場合はクラシックに描画するということで、通常のGDI関数を呼び出すことになります。 FillRectは第3引数のブラシで第2引数の範囲を塗りつぶす関数であり、 COLOR_MENUのブラシであればタスクバーと同じ色になります。 テキストの描画はDrawTextで行うことができ、色の変更と透過の指定はSetTextColorとSetBkModeで行います。

デスクバンドを正しく表示するためには、次のメッセージも処理しておくことになります。

case WM_THEMECHANGED:
	m_bCompositionEnabled = !m_bCompositionEnabled;
	break;

case WM_ERASEBKGND:
	return 0;

現在のテーマが変更された場合はデスクバンドの描画を変更する必要がありますが、 このテーマの変更に併せてSetCompositionStateが呼ばれることはありません。 つまり、m_bCompositionEnabledが変化せず、テーマ変更後も同じ描画処理が行われるという問題があります。 よって、WM_THEMECHANGEDで明示的にテーマの変更を検出し、m_bCompositionEnabledを変更することになります。 WM_ERASEBKGNDは、クライアント領域がウインドウの背景色で塗りつぶされる際に送られますが、 0を返すことでこの塗りつぶしを防ぐことができます。 これにより、ちらつきの発生を防ぐことができます。 より完全にちらつきの発生を防ぐために、今回作成するウインドウのスタイルにはWS_CLIPSIBLINGSを指定しています。 これを指定しないと、デスクバンド同士が重なった時にちらつきが発生します。

デスクバンドの登録には、特別なレジストリ書き込みが必要ではありません。 つまり、HKEY_CLASSES_ROOT\CLSID以下に独自のCLSIDを書き込むだけで構いません。 ただし、代わりとしてカテゴリマネージャへの登録が必要になります。

BOOL RegisterCategoryManager(BOOL bRegister)
{
	HRESULT      hr;
	ICatRegister *pCatRegister;
	CATID        catid = CATID_DeskBand;
	
	CoInitialize(NULL);
	
	hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pCatRegister));
	if (SUCCEEDED(hr)) {
		if (bRegister)
			hr = pCatRegister->RegisterClassImplCategories(CLSID_DeskBandSample, 1, &catid);
		else
			hr = pCatRegister->UnRegisterClassImplCategories(CLSID_DeskBandSample, 1, &catid);
		pCatRegister->Release();
	}

	CoUninitialize();

	return hr;
}

カテゴリマネージャでは、オブジェクトを特定のカテゴリに属するものとしてグループ化することができます。 カテゴリはCATIDで識別することができ、デスクバンドのカテゴリはCATID_DeskBandになります。 ICatRegister::RegisterClassImplCategoriesではこのCATIDを第3引数に指定すると共に、 登録するオブジェクトのCLSIDを第1引数に指定します。

デスクバンドの表示/非表示

デスクバンドの表示方法は、それがシェル拡張内で行われるのか、外部アプリケーションで行われるのかで異なります。 シェル拡張内で行う場合は、次のようにします。

CoCreateInstance(CLSID_TrayBandSiteService, NULL, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&pBandSite));
pBandSite->AddBand(static_cast<IDeskBand *>(this));

CLSID_TrayBandSiteServiceで識別されるオブジェクトは、一連のデスクバンドを管理しています。 このオブジェクトからはIBandSiteを取得することができ、AddBandを呼び出すことでシェル拡張(自作のデスクバンド)を追加することができます。 外部アプリケーションが任意のデスクバンドを追加する場合は、次のような処理を行います。

#include <windows.h>
#include <shlobj.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	CLSID         clsid = {0x112143a6, 0x62c1, 0x4478, {0x9e, 0x8f, 0x87, 0x26, 0x99, 0x25, 0x5e, 0x2e}};
	HRESULT       hr;
	ITrayDeskBand *pTrayDeskBand;

	CoInitialize(NULL);
	
	hr = CoCreateInstance(CLSID_TrayDeskBand, NULL, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&pTrayDeskBand));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}

	pTrayDeskBand->ShowDeskBand(clsid);
	
	pTrayDeskBand->Release();
	CoUninitialize();

	return 0;
}

CLSID_TrayDeskBandで識別されるオブジェクトは、ITrayDeskBandを実装しています。 このインターフェースのShowDeskBandを呼び出すとダイアログが表示され、 これに応答することにより第1引数のCLISDで識別されるデスクバンドを表示することができます。 ITrayDeskBandはWindows Vistaから使用可能です。

デスクバンドを非表示にする方法も2通りあります。 ITrayDeskBandを使用する場合は次のようになります。

pTrayDeskBand->HideDeskBand(clsid);

HideDeskBandを呼び出すことにより、CLISDで識別されるデスクバンドを非表示にすることができます。 このときにはダイアログが表示されることはありません。 IBandSiteを使用する場合は次のようになります。

#include <windows.h>
#include <shlobj.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT   hr;
	DWORD     i, dwCount;
	DWORD     dwBandId;
	IPersist  *pPersist;
	IBandSite *pBandSite;
	LPOLESTR  lpszClsid;
	CLSID     clsid;
	CLSID     clsidTarget = {0x112143a6, 0x62c1, 0x4478, {0x9e, 0x8f, 0x87, 0x26, 0x99, 0x25, 0x5e, 0x2e}};

	CoInitialize(NULL);
	
	hr = CoCreateInstance(CLSID_TrayBandSiteService, NULL, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&pBandSite));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}

	dwCount = pBandSite->EnumBands(-1, &dwBandId);
	for (i = 0; i < dwCount; i++) {
		pBandSite->EnumBands(i, &dwBandId);
		pBandSite->GetBandObject(dwBandId, IID_PPV_ARGS(&pPersist));
		pPersist->GetClassID(&clsid);
		pPersist->Release();
		
		if (IsEqualCLSID(clsid, clsidTarget)){
			StringFromCLSID(clsid, &lpszClsid);
			MessageBoxW(NULL, lpszClsid, L"OK", MB_OK);
			pBandSite->RemoveBand(dwBandId);
			CoTaskMemFree(lpszClsid);
		}
	}
	
	pBandSite->Release();
	CoUninitialize();

	return 0;
}

EnumBandsの第1引数に-1を指定すれば、現在表示されているバンドオブジェクトの総数を取得することができます。 後はこの数だけEnumBandsを呼び出してバンドIDを取得し、 GetBandObjectを呼び出してデスクバンドをIPersistで識別します。 IPersist::GetClassIDを呼び出せばデスクバンドのCLISDを取得することができるため、 これが目的のデスクバンドのCLSIDと一致するかを調べ、 一致する場合はそのデスクバンドのバンドIDをRemoveBandに指定します。



戻る