FAQ9-1

Q. タイトルバーにビットマップを描画するには

 はじめまして,山野と申します.
 MFCを使って,アプリケーションを開発していますが,どうしても解決できないことあります.
 それは,タイトルバーへの描画です.秀Termなどで,行っているようにタイトルバーへ自分で描画を行いたいのです.
 具体的には,MDIの親と子のそれぞれのWindowsのタイトルバーに自分のテキストやビットマップを描画したいのです.
 何かよい方法があれば教えてください.
(編注:この質問をしてくれたのは,山野容揮さんです)


A. WM_NCPAINTをハンドルして直接描く

 タイトルバーに自分のテキストやビットマップを描画する時に,よく使われる方法は二つあります(よく使うといってもタイトルバーに描画なんてめったにしないけど).

オーナー付きポップアップウィンドウを使う

 まず一つは,オーナー付きポップアップウィンドウ(もしくはモードレス・ダイアログボックス)を作る方法です.
 ポップアップウィンドウの位置を,ちょうどタイトルバーと重なるように配置してやることで,あたかもタイトルバーを自分で描画しているかのように見せることができます.
 ただし,この方法はあまり一般的ではないので,簡単に説明します.
 オーナー付きポップアップウィンドウを作るには,CreateWindow()するときに親ウィンドウにフレームウィンドウを指定し,ウィンドウスタイルにWS_CHILDを指定する代わりにWS_POPUPを指定するようにして作ります.
 オーナー付きにしておくと,そのウィンドウは必ずオーナー(=フレームウィンドウ)の手前に表示されるようになるので,タイトルバーの代わりにするのにちょうどよいことになります(図9-1).

[図9-1] :

 しかし,「子ウィンドウ」ではなくて,ただの「オーナー付き」なので,フレームウィンドウが移動したりサイズを変更すると,ウィンドウがずれてしまいます.
 しかたがないので,WM_SIZEやWM_MOVEが来るのを見計らって同時に両方をウィンドウを動かしてやるようにしましょう.
 あと,ポップアップウィンドウのほうがアクティブにならないような配慮もしなくてはいけません.
 と,いろいろな配慮が必要なのでやめたほうがよいでしょう(じゃあ書くなって).
 でもこの方法のほうがウィンドウが分離するため非常に安全なので,このような方法もあるということを知っておきましょう.

WM_NCPAINTをハンドルして直接描く

 もう一つの方法は,ウィンドウに直接描画する方法です.
 直接描画するほうがなんとなくスマートであり,不自然に感じないので,こちらのほうが一般的なようです.
 直接描画するには,WM_PAINTメッセージではなく,WM_NCPAINTをハンドルします.MFCではOnNcPaint()をオーバーライドします.
 NCPAINTのNCとはNC旋盤のことではなく(^^;),Nonclient Area=非クライアント領域のことで,ウィンドウの外枠やタイトルバーやメニューバーのある部分をいいます.
 この非クライアント領域を描画してくれ,というとき,WindowsはWM_NCPAINTメッセージを送ってきます.
 では,さっそくWM_NCPAINTを使ってみたところ,図9-2のようになりました.

[図9-2] :

 ちなみにこれは失敗です.WM_NCPAINTを完全に自分で処理してしまうと,あとの外枠などを描画する処理をWindowsが行ってくれなくなり,変なことになってしまいます.
 こういう場合は,DefWindowProc()を先に呼んでから,自分の処理をするようにします.DefWindowProc()は,メッセージをハンドルしないときに,標準の処理として呼んでおくもので,WM_NCPAINTをハンドルしていないときは,実はいつもDefWindowProc()が非クライアント領域を描画してくれていたのです.
 というわけで,先にDefWindowProc()を呼んでおくと,Windowsが非クライアント領域をちゃんと描いてくれるようになり,その後でタイトルバーの部分だけ上書きすれば,あたかもタイトルバーだけ描画されているかのようになります.
 ということは,この方法を使うと一度元のタイトルバーが描かれてから自分のタイトルバーを描くので,一瞬ちらつくのでは,と思うかもしれませんが,実はちらついてます(笑).でも気になる速度ではありません.(図9-3).

[図9-3] :

アクティブの切り替えのとき

 さきほど,非クライアント領域を描画する必要があるときにWM_NCPAINTが送られると書きましたが,実は送られてこない場合があります.
 WM_NCPAINTはウィンドウの重なり方やサイズなどが変わって,再描画するときに送られてくるメッセージで,自発的に描画する必要のあるときは送られてきません.
 とりあえずわかっているのは,アクティブが切り替わったときにタイトルバーや枠の色を変える必要のあるときです.
 このときはWM_NCACTIVATEメッセージが送られてくるので,このときもまたタイトルバーの描画をすればよいです.
 と,ここで忘れていましたが,タイトルバーの背景色はアクティブ時と非アクティブ時で違う色なので,状態を判断して色を使い分けなくてはいけません(リスト9-1).

[リスト9-1] :

void MyDrawCaption( HWND hwnd, BOOL fActive ) {
	COLORREF crCaption, crText;
	int cxFrame = GetSystemMetrics( SM_CXFRAME );
	int cyFrame = GetSystemMetrics( SM_CYFRAME );
	int cxButton = GetSystemMetrics( SM_CXSIZE );
	int cyButton = GetSystemMetrics( SM_CYSIZE );
	if( fActive ) {
		crCaption = GetSysColor( COLOR_ACTIVECAPTION );
		crText = GetSysColor( COLOR_CAPTIONTEXT );
	} else {
		crCaption = GetSysColor( COLOR_INACTIVECAPTION );
		crText = GetSysColor( COLOR_INACTIVECAPTIONTEXT );
	}

	RECT rcWnd;
	char sz[128];
	GetWindowRect( hwnd, &rcWnd);
	GetWindowText( hwnd, sz, sizeof(sz) - 1 );

	HDC	hdc = GetWindowDC( hwnd );

	//テキスト描画の例
	RECT rcFill;
	rcFill.left = cxFrame + cxButton + 1;
	rcFill.right = (rcWnd.right - rcWnd.left) - (cxFrame + 3*(cxButton+1));
	rcFill.top = cyFrame;
	rcFill.bottom = cyFrame + cyButton;
	SetTextColor( hdc, crText );
	SetBkColor( hdc, crCaption);
	HBRUSH hbr = CreateSolidBrush( crCaption );
	FillRect( hdc, &rcFill, hbr );
	DeleteObject( hbr );
	DrawText( hdc, sz, lstrlen(sz), &rcFill,
				DT_RIGHT | DT_VCENTER | DT_SINGLELINE );

	//ビットマップ描画の例
	HDC hdcMem = CreateCompatibleDC( hdc );
	HGDIOBJ	hold = SelectObject( hdcMem, hbmp );
	#ifdef WIN32
	SIZE	size;
	GetTextExtentPoint32( hdc, sz, lstrlen(sz), &size );
	int cxText = size.cx;
	#else
	int cxText = LOWORD(GetTextExtent( hdc, sz, lstrlen(sz) ));
	#endif
	int cxDraw = rcFill.bottom - rcFill.top - 4;
	int	xBmp = rcFill.right - size.cx - cxDraw;
	StretchBlt( hdc, xBmp, rcFill.top + 2, cxDraw, cxDraw,
				hdcMem, 0, 0, cxBmp, cyBmp, SRCCOPY );
	SelectObject( hdcMem, hold );
	DeleteDC( hdc );

	ReleaseDC( hwnd, hdc );
}

MDIのとき

 質問ではMDIだったので,MDIの親と子で両方タイトルバー描画処理を入れてみたところ,不都合がいくつかありました.
 例えば,MDI子ウィンドウを最大化したとき,タイトルバー描画が効かなくなり元のタイトルバーが表示されしてしいました.
 どうしてなのか調べてみると,どうやらMDI子ウィンドウを最大化しているときは,勝手にWindowsがWM_SETTEXTなどを使ってタイトル文字を変えているようです.
 さらに調べてみたところ,SetWindowText()やWM_SETTEXTメッセージなどでウィンドウのタイトルを変えた場合,自発的にタイトル描画されるのでWM_NCPAINTが来ないということがわかりました.
 というわけで,WM_SETTEXTメッセージもハンドルして,このときも(タイミングをずらして)タイトル描画するようにすることでこの問題も解決することができました(リスト9-2,9-3).

[リスト9-2] :

	case WM_NCPAINT:{
		LRESULT ret = DefWindowProc( hwnd, message, wParam, lParam );
		if( !IsIconic( hwnd ) ) {
			#ifdef WIN32
			MyDrawCaption( hwnd, GetForegroundWindow() == hwnd );
			#else
			MyDrawCaption( hwnd, GetActiveWindow() == hwnd );
			#endif
		}
		return ret;
		}

	case WM_NCACTIVATE:{
		LRESULT ret = DefWindowProc(hwnd , message ,wParam ,lParam);
		if( !IsIconic( hwnd ) ) {
			MyDrawCaption( hwnd, wParam );
		}
		return ret;
		}

	case WM_SETTEXT:{
		LRESULT ret = DefWindowProc( hwnd, message, wParam, lParam );
		if( !IsIconic( hwnd ) ) {
			MyDrawCaption( hwnd, GetForegroundWindow() == hwnd );
		}
		return ret;
		}

[リスト9-3] :

BOOL CMainFrame::OnNcActivate(BOOL bActive) {
	BOOL ret;
	ret = CMDIFrameWnd::OnNcActivate(bActive);
	if( !IsIconic() ) {
		MyDrawCaption( m_hWnd, bActive );
	}
	return ret;
}

void CMainFrame::OnNcPaint() {
	CMDIFrameWnd::OnNcPaint();
	if( !IsIconic() ) {
		#ifdef WIN32
		DrawCaption( m_hWnd, (BOOL)(m_hWnd == GetForegroundWindow()->m_hWnd) );
		#else
		DrawCaption( m_hWnd, (BOOL)(m_hWnd == GetActiveWindow()->m_hWnd) );
		#endif
	}
}

LONG CMainFrame::OnSetText( UINT wParam, LONG lParam ) {
	LRESULT ret = DefWindowProc( WM_SETTEXT, wParam, lParam );
	MyDrawCaption( m_hWnd, TRUE );
	return ret;
	//メッセージマップで ON_MESSAGE( WM_SETTEXT, OnSetText ) としている
}
 このように,タイミングやメッセージが特定できない場合は,どういうメッセージが来ているかを調べて臨機応変に対応することでたいていの問題は解決することができるはずなので,何だかわからない場合は自分でメッセージを調べてみましょう.
 ちなみにメッセージを調べるにはSDKのツールで「スパイ」というものがあるのでこれを使うとよいでしょう.

Back to FAQ main page