Click here to Skip to main content
15,897,291 members
Articles / Multimedia / GDI

Custom Captions (Including Multi-line Captions)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (9 votes)
15 Jul 2000CPOL 241.2K   6.8K   63  
Simple customised Window captions, including multi-line captions
////////////////////////////////////////////////////////////////
//
// class CCaption
//
// Generic caption painter. Handles WM_NCPAINT, WM_NCACTIVATE, etc. to
// handle drawing custom captions. To use it:
//
// - call Install from your frame's OnCreate function. 
// - Set a custom CaptionBackground if desired
// - Set custom TextAttributes if required
//
//	 Derive from this class for custom caption layouts.
// 
//   If you are drawing custom caption buttons, you must handle WM_NCLBUTTONDOWN & co.
//   yourself. CCaption does not handle the mouse for custom caption buttons. 
//
//	Author: Dave Lorde	(dlorde@cix.compulink.co.uk)
//
//          Copyright 1999
//
//			- based on a 1997 Microsoft Systems Journal
//            C++ Q&A article by Paul DiLascia. 
//
////////////////////////////////////////////////////////////////
//

#include "StdAfx.h"
#include "Caption.h"
#include "CaptionBackground.h"
#include "CaptionTextAttributes.h"
#include "SuppressStyle.h"
#include "AutoSelector.h"
#include <algorithm>

using std::_cpp_max;
using std::_cpp_min;

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

const int		MinLuminosity	= 90;				   // good from trial & error
const COLORREF	ColorWhite		= RGB(255,255,255);

IMPLEMENT_DYNAMIC(CCaption, CSubclassWnd);

////////
// Default Constructor 
//
CCaption::CCaption()
	:m_pBackground(0), m_pTextAttributes(0)
{
	Invalidate();
}

CCaption::~CCaption()
{
	delete m_pBackground, m_pBackground = 0;
	delete m_pTextAttributes, m_pTextAttributes = 0;
}

//////////////////
// Install caption handler. 
//
BOOL CCaption::Install(CFrameWnd* pFrameWnd)
{
	ASSERT_KINDOF(CFrameWnd, pFrameWnd);

	return HookWindow(pFrameWnd);
}

//////////////////
// Replace default background painter. 
//
void CCaption::SetBackground(CCaptionBackground* pBackground)
{
	if (m_pBackground)
		delete m_pBackground;

	m_pBackground = pBackground;

	Refresh();
}

//////////////////
// Replace default text attributes
//
void CCaption::SetTextAttributes(CCaptionTextAttributes* pTextAttributes)
{		 
	if (m_pTextAttributes)
		delete m_pTextAttributes;

	m_pTextAttributes = pTextAttributes;

	Refresh();
}

/////////////////
// Regenerate and display caption
//
void CCaption::Refresh()
{
	Invalidate();
	PaintCaption();
}

//////////////////
// Ensure caption bitmaps are repainted	next time through OnNcPaint()
//
void CCaption::Invalidate() 
{ 
	m_szCaption = CSize(0,0); 
}

//////////////////
// Message handler handles caption-related messages
//
LRESULT CCaption::WindowProc(UINT msg, WPARAM wp, LPARAM lp)
{
	switch (msg) 
	{
	case WM_NCPAINT:
		OnNcPaint(HRGN(wp));
		return 0;

	case WM_NCACTIVATE:
		return OnNcActivate(wp);

	case WM_SETTEXT:
		OnSetText((LPCTSTR)lp);
		return 0;

	case WM_SYSCOLORCHANGE:
	case WM_SETTINGCHANGE:
		OnColorChange();
		return 0;
	}
	// We don't handle it: pass along
	return CSubclassWnd::WindowProc(msg, wp, lp);
}

/////////////////
// Handle WM_NCPAINT for main window
//
void CCaption::OnNcPaint(HRGN hRgn)
{
	ASSERT_VALID(m_pWndHooked);
	CWnd& wnd = *m_pWndHooked;

	CRect rc = GetCaptionRect();	// caption rectangle in window coords
	CRect rcWin;					
	wnd.GetWindowRect(&rcWin);		// .. get window rect
	rc += rcWin.TopLeft();			// convert caption rect to screen coords

	// Don't bother painting if the caption doesn't lie within the region.
	//
	if ((WORD)hRgn > 1 && !::RectInRegion(hRgn, &rc)) 
	{
		Default();					// just do default thing
		return;						// and quit
	}

	// Exclude caption from update region
	//
	HRGN hRgnCaption = ::CreateRectRgnIndirect(&rc);
	HRGN hRgnNew     = ::CreateRectRgnIndirect(&rc);

	if ((WORD)hRgn > 1) 
	{
		// wParam is a valid region: subtract caption from it
		//
		::CombineRgn(hRgnNew, hRgn, hRgnCaption, RGN_DIFF);
	} 
	else 
	{
		// wParam is not a valid region: create one that's the whole
		// window minus the caption bar
		//
		HRGN hRgnAll = ::CreateRectRgnIndirect(&rcWin);
		CombineRgn(hRgnNew, hRgnAll, hRgnCaption, RGN_DIFF);
		DeleteObject(hRgnAll);
	}

	// Call Windows to do WM_NCPAINT with altered update region
	//
	MSG& msg = AfxGetThreadState()->m_lastSentMsg;
	WPARAM savewp = msg.wParam;		// save original wParam
	msg.wParam = (WPARAM)hRgnNew;	// set new region for DefWindowProc

	Default();						// Normal message handling

	DeleteObject(hRgnCaption);		// clean up
	DeleteObject(hRgnNew);			// ...
	msg.wParam = savewp;			// restore original wParam

	PaintCaption();					// Now paint our special caption
}

//////////////////
// Handle WM_NCACTIVATE for main window
//
BOOL CCaption::OnNcActivate(BOOL bActive)
{
	ASSERT_VALID(m_pWndHooked);
	CFrameWnd& frame = *((CFrameWnd*)m_pWndHooked);
	ASSERT_KINDOF(CFrameWnd, &frame);

	// Mimic MFC kludge to stay active if WF_STAYACTIVE bit is on
	//
	if (frame.m_nFlags & WF_STAYACTIVE)
		bActive = TRUE;

	if (!frame.IsWindowEnabled())			// but not if disabled
		bActive = FALSE;

	if (bActive == m_bActive)
		return TRUE;						// nothing to do

	// In case this is a MDI app, manually activate/paint active MDI child
	// window, because Windows won't do it if parent frame is invisible.
	// Must do this BEFORE calling Default, or it will not work.
	//
	CFrameWnd* pActiveFrame = frame.GetActiveFrame();

	if (pActiveFrame != &frame) 
	{		
		pActiveFrame->SendMessage(WM_NCACTIVATE, bActive);
		pActiveFrame->SendMessage(WM_NCPAINT);
	}

	// Turn WS_VISIBLE off before calling DefWindowProc,
	// so DefWindowProc won't paint and thereby cause flicker.
	//
	{
		SuppressStyle ss(frame.GetSafeHwnd(), WS_VISIBLE);

		MSG& msg = AfxGetThreadState()->m_lastSentMsg;
		msg.wParam = bActive;

		Default();						// Normal message handling
	}
	// At this point, nothing has happened (since WS_VISIBLE was off).
	// Now it's time to paint.
	//
	m_bActive = bActive;				// update state
	frame.SendMessage(WM_NCPAINT);		// paint non-client area (frame too)

	return TRUE;						// done OK
}

//////////////////
// Handle WM_SETTEXT for main window
//
void CCaption::OnSetText(LPCTSTR)
{
	ASSERT_VALID(m_pWndHooked);
	CWnd& wnd = *m_pWndHooked;

	// Turn WS_VISIBLE style off before calling Windows to
	// set the text. Reset to visible afterwards
	{
		SuppressStyle ss(wnd.GetSafeHwnd(), WS_VISIBLE);

		Default();						// Normal message handling
	}
	Refresh();
}

//////////
// Ensure caption is repainted when system colors change
//
void CCaption::OnColorChange()
{	 
	Default();							 // Normal message handling
	Refresh();
}

//////////////////
// Paint custom caption. m_bActive flag tells whether frame is active or not. 
// Just blast the bitmap to the title bar.
//
void CCaption::PaintCaption()
{
	ASSERT(m_pWndHooked);
	CWnd& wnd = *m_pWndHooked;

	// Get caption DC and rectangle
	//
	CWindowDC dcWin(&wnd);					// window DC
	CDC dc;									// memory DC
	dc.CreateCompatibleDC(&dcWin);			// ...create it

	CRect rc = GetCaptionRect();			// get caption rectangle
	if (rc.Size() != m_szCaption) 			// if size changed:
	{		
		m_bmCaption[0].DeleteObject();		// invalidate bitmaps
		m_bmCaption[1].DeleteObject();		// ...
		m_szCaption = rc.Size();			// update new size
	}

	// Get active/inactive bitmap & determine if needs to be regenerated
	//
	CBitmap& bm = m_bmCaption[m_bActive != 0];	// get bitmap

	BOOL bPaintIt = FALSE;						// paint new bitmap?

	if (!HBITMAP(bm)) 
	{											// no bitmap, so create one	  :
		bm.CreateCompatibleBitmap(&dcWin, rc.Width(), rc.Height()); 
		bPaintIt = TRUE;						// and paint it
	}
	CBitmap* pOldBitmap = dc.SelectObject(&bm);	// select bitmap into memory DC

	// If bitmap needs painting, do it
	//
	if (bPaintIt) 
		PaintBitmap(&dc);

	// blast bits to screen
	//
	dcWin.BitBlt(rc.left, rc.top, rc.Width(), rc.Height(), &dc, 0, 0, SRCCOPY);

	dc.SelectObject(pOldBitmap);				// restore DC
}

/////////
// Paint the caption bitmap. Override for custom caption painters.
// Note: SDK DrawCaption() method not used, so as to provide useful 
// virtual functions for derived caption painters
//
void CCaption::PaintBitmap(CDC* pDC)
{
	PaintBackground(pDC);

	PaintIcon(pDC);

	PaintButtons(pDC);

	PaintLowerBorder(pDC);

	PaintText(pDC);
}

///////////
// Delegate background painting to CCaptionBackground class
//
void CCaption::PaintBackground(CDC* pDC)
{
	GetBackground()->Paint(pDC, m_szCaption, m_bActive);
}

////////
// Paint the border between the bottom of the caption rectangle and the 
// shadow on the top of the client rectangle 
//
void CCaption::PaintLowerBorder(CDC* pDC)
{
	int x = 0;
	int y =	m_szCaption.cy - GetSystemMetrics(SM_CYBORDER);
	int h = m_szCaption.cy;
	int w = m_szCaption.cx;

	CCaptionBackground::PaintRect(pDC, x, y, w, h, GetSysColor(COLOR_3DFACE));
}

////////
// Calculate the caption text clipping rect
//
CRect CCaption::GetTextRect()
{
	CRect textRect = GetCaptionRect();
	textRect.left += GetIconWidth();
	textRect.right -= GetButtonsWidth() + 4;
	textRect.top -= 2;
	return textRect;
}

///////////
// Draw the caption text onto the bitmap
//
void CCaption::PaintText(CDC* pDC)
{
	pDC->SetBkMode(TRANSPARENT);			// draw on top of our background

	CString text;
	ASSERT(m_pWndHooked);
	CWnd& wnd = *m_pWndHooked;

	wnd.GetWindowText(text);

	// Set text color
	COLORREF textColor = GetTextColor(m_bActive);

	pDC->SetTextColor(textColor);

	// Get caption font	and select into DC
	AutoSelector a(pDC, GetFont(m_bActive));

	CRect textRect = GetTextRect();

	pDC->DrawText(text, textRect, DT_LEFT|DT_END_ELLIPSIS);
}

////////////////
// Draw caption icon if valid DC is provided. Returns effective width of icon.
//
int CCaption::PaintIcon(CDC* pDC)
{
	ASSERT(m_pWndHooked);
	CWnd& wnd = *m_pWndHooked;

	////////////
	// If there's no icon or system menu, don't draw one
	//
	if (!(wnd.GetStyle() & WS_SYSMENU))
		return 0;

	//////////////
	// Within the basic button rectangle, Windows 95 uses a 1 or 2 pixel border
	// Icon has 2 pixel border on left, 1 pixel on top/bottom, 0 right
	//
	int cxIcon = GetSystemMetrics(SM_CXSIZE);
	CRect rc(0, 0, cxIcon, GetSystemMetrics(SM_CYSIZE));
	rc.DeflateRect(0,1);
	rc.left += 2;
	if (pDC != 0)
	{
		DrawIconEx(pDC->m_hDC, rc.left, rc.top, 
			(HICON)GetClassLong(wnd.m_hWnd, GCL_HICONSM),
			rc.Width(), rc.Height(), 0, NULL, DI_NORMAL);
	}
	return cxIcon;
}

//////////////
// Helper
//
int CCaption::GetIconWidth()
{
	return PaintIcon();
}

////////////////
// Draw min, max/restore, close buttons.
// Returns total width of buttons drawn.
//
int CCaption::PaintButtons(CDC* pDC)
{
	ASSERT(m_pWndHooked);
	CWnd& wnd = *m_pWndHooked;
	DWORD dwStyle = wnd.GetStyle();
	if (!(dwStyle & WS_CAPTION))
		return 0;

	int cxIcon = GetSystemMetrics(SM_CXSIZE);
	int cyIcon = GetSystemMetrics(SM_CYSIZE);

	// Draw caption buttons. These are all drawn inside a rectangle
	// of dimensions SM_CXSIZE by SM_CYSIZE
	CRect captRect = GetCaptionRect();
	CRect rc(0, 0, cxIcon, cyIcon);
	rc += CPoint(captRect.Width() - cxIcon, 0);	// move right

	//////////////
	// Close box has a 2 pixel border on all sides but left, which is zero
	//
	rc.DeflateRect(0,2);
	rc.right -= 2;

	if (pDC)
		pDC->DrawFrameControl(&rc, DFC_CAPTION, DFCS_CAPTIONCLOSE);

	//////////////
	// Max/restore button is like close box; just shift rectangle left
	// Also does help button, if any.
	//
	BOOL bMaxBox = dwStyle & WS_MAXIMIZEBOX;
	if (bMaxBox || (wnd.GetExStyle() & WS_EX_CONTEXTHELP)) 
	{
		rc -= CPoint(cxIcon, 0);
		if (pDC)
			pDC->DrawFrameControl(&rc, DFC_CAPTION,
				bMaxBox ? (wnd.IsZoomed() ? DFCS_CAPTIONRESTORE : DFCS_CAPTIONMAX) :
						DFCS_CAPTIONHELP);
	}

	///////////////
	// Minimize button has 2 pixel border on all sides but right.
	//
	if (dwStyle & WS_MINIMIZEBOX) 
	{
		rc -= CPoint(cxIcon - 2,0);
		if (pDC)
			pDC->DrawFrameControl(&rc, DFC_CAPTION, 
				(wnd.IsIconic() ? DFCS_CAPTIONRESTORE : DFCS_CAPTIONMIN));
	}
	return captRect.Width() - (rc.left - 2);
}

int CCaption::GetButtonsWidth()
{
	return PaintButtons();
}

///////////////
// Calculate the caption rectangle relative to the window frame
//
CRect CCaption::GetCaptionRect()
{
	ASSERT(m_pWndHooked);
	CWnd& wnd = *m_pWndHooked;

	// Get window rect as window-relative
	CRect captionRect;
	wnd.GetWindowRect(captionRect);
	captionRect -= captionRect.TopLeft();	// shift origin to (0,0)

	// Shrink to caption size
	CSize szFrame = GetFrameSize();
	captionRect.InflateRect(-szFrame.cx, -szFrame.cy);
	captionRect.bottom = captionRect.top + GetSystemMetrics(SM_CYCAPTION);	// height of caption

	// Iconic captions move 1 pixel up. They just do, OK?
	//
	if (wnd.IsIconic())
		captionRect.OffsetRect(0, -1);
	
	return captionRect;
}

///////
// Return width and height of window frame elements
//
CSize CCaption::GetFrameSize() const
{
	ASSERT(m_pWndHooked);
	CWnd& wnd = *m_pWndHooked;

	////////////
	// Get size of frame around window
	//
	DWORD dwStyle = wnd.GetStyle();
	CSize szFrame = (dwStyle & WS_THICKFRAME) ?
		CSize(GetSystemMetrics(SM_CXSIZEFRAME),
			   GetSystemMetrics(SM_CYSIZEFRAME)) :
		CSize(GetSystemMetrics(SM_CXFIXEDFRAME),
				GetSystemMetrics(SM_CYFIXEDFRAME));
	return szFrame;
}

COLORREF CCaption::GetTextColor(BOOL bActive)
{
	COLORREF textColor = bActive ? GetTextAttributes()->GetActiveColor() :
								   GetTextAttributes()->GetInactiveColor();
//////////////////
// Uncomment these lines to automatically set the text colour to white
// when the background is too dark
//								   
//	if (GetLuminosity(textColor) < MinLuminosity)		
//		textColor = ColorWhite;

	return textColor;
}

///////////////
// Delegate to CCaptionTextAttributes for appropriate font
//
CFont* CCaption::GetFont(BOOL m_bActive)
{
	ASSERT(m_pWndHooked);
	CWnd& wnd = *m_pWndHooked;

	if (!wnd.IsIconic())
		return m_bActive ? GetTextAttributes()->GetActiveFont() : 
						   GetTextAttributes()->GetInactiveFont();
	else
		return CCaptionTextAttributes::GetSystemFont();
}

////////////
// Lazy construction of text attributes	object
//
CCaptionTextAttributes* CCaption::GetTextAttributes()
{
	if (!m_pTextAttributes)
		m_pTextAttributes = new CCaptionTextAttributes();
	
	return m_pTextAttributes;
}

////////////
// Lazy construction of background object
//
CCaptionBackground* CCaption::GetBackground()
{
	if (!m_pBackground)
		m_pBackground = new CCaptionBackground();

	return m_pBackground;
}

//////////////////
// Helper function to compute the luminosity for an RGB color.
// Measures how bright the color is. I use this so I can draw the caption
// text using the user's chosen color, unless it's too dark. See MSDN for
// definition of luminosity and how to compute it.
//
int CCaption::GetLuminosity(COLORREF color) const
{
	const int HlsMax = 240;	// This is what Display Properties uses
	const int RgbMax = 255;	// max r/g/b value is 255
	int r = GetRValue(color);
	int g = GetGValue(color);
	int b = GetBValue(color);
	int rgbMax = _MAX( _MAX(r, g), b);
	int rgbMin = _MIN( _MIN(r, g), b);
	return (((rgbMax + rgbMin) * HlsMax) + RgbMax ) / (2 * RgbMax);
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions