Click here to Skip to main content
15,881,715 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 240.1K   6.8K   63  
Simple customised Window captions, including multi-line captions
////////////////////////////////////////////////////////////////
//
// class CMultiLineCaption
//
// Multi-line auto-wrap caption painter. 
//
// To use:
// - call Install from your frame's OnCreate function. 
// - Set a custom CaptionBackground if desired
// - Set custom TextAttributes if required
//
//   If you are drawing custom caption buttons, you must handle WM_NCLBUTTONDOWN & co.
//   yourself. CMultiLineCaption does not handle the mouse for custom caption buttons. 
//
//	Author: Dave Lorde	(dlorde@cix.compulink.co.uk)
//
//          Copyright 1999
//
////////////////////////////////////////////////////////////////
//

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

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

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

IMPLEMENT_DYNAMIC(CMultiLineCaption, CCaption); 	  

namespace
{
const int TextFormat			= DT_CENTER|DT_WORDBREAK;
const int SingleLineTextFormat	= DT_LEFT|DT_END_ELLIPSIS;
const int DrawTextFudgeFactor	= 2;
const int TextLowerBorder		= 2;
const UINT RestoreFromMinimized	= 0x8124;		  // WindowPosChanged flags
const CString ellipsis("...");
}

///////////////
// Constructor
//
CMultiLineCaption::CMultiLineCaption(int maxLines)
	:m_MaxLines(maxLines > 0 ? maxLines : 1), 
	m_InitialShow(true), 
	m_ActiveSysColor(GetSysColor(COLOR_ACTIVECAPTION)),
	m_InactiveSysColor(GetSysColor(COLOR_INACTIVECAPTION))
{
}

//////////
// Adjust maximum lines for title wrapping, and refresh
//
void CMultiLineCaption::SetMaxLines(int maxLines)
{
	m_MaxLines = maxLines > 0 ? maxLines : 1;

	AdjustHeight();
	Repaint();								// Redraw caption
}

////////////////
// Return maximum permitted lines for title
//
int CMultiLineCaption::GetMaxLines() const
{
	return m_MaxLines;
}

//////////////////
// Message handler handles caption size-related messages
//
LRESULT CMultiLineCaption::WindowProc(UINT msg, WPARAM wp, LPARAM lp)
{
	switch (msg) 
	{
	case WM_NCACTIVATE:
		return OnNcActivate(wp);

		///////////
		// 	Adjust height of client area according to text wrapping
		//
	case WM_SETTEXT:
		OnSetText((LPCTSTR)lp);
		return 0;

		///////////
		// Change colours if using system colours and SysColorChange
	case WM_NCPAINT:
		OnNcPaint(HRGN(wp));
		return 0;
		
		///////////
		// Adjust height of client area according to required caption height
		//
	case WM_NCCALCSIZE:
		return OnNcCalcSize((BOOL)wp, (LPNCCALCSIZE_PARAMS)lp);

		////////////
		// Ensure caption repaints smoothly when sized
		//
	case WM_SIZE:
		OnSize( (UINT)wp, LOWORD(lp), HIWORD(lp) );
		return 0;

		///////////
		// Prevent multi-line caption being overdrawn by client area
		//
	case WM_GETMINMAXINFO:
		OnGetMinMaxInfo((LPMINMAXINFO)lp);
		return 0;

		///////////
		// Refresh when activated or inactivated
		//
	case WM_MDIACTIVATE:
		OnMDIActivate();
		return 0;

		///////////
		// Refresh when sizing stopped
		// 
	case WM_EXITSIZEMOVE:
		OnExitSizeMove();
		return 0;

		///////////
		// Refresh caption when window is tiled 
		//
	case WM_WINDOWPOSCHANGED:
		OnWindowPosChanged( (WINDOWPOS*) lp);
		return 0;
	}
	////////////
	// We don't handle it: pass to base class
	//
	return CCaption::WindowProc(msg, wp, lp);
}

//////////////////
// Handle WM_NCACTIVATE for main window
//
BOOL CMultiLineCaption::OnNcActivate(BOOL bActive)
{
	BOOL result = CCaption::OnNcActivate(bActive);

	Repaint();								// Redraw caption

	return result;
}

void CMultiLineCaption::OnSetText(LPCTSTR lpStr)
{
	CCaption::OnSetText(lpStr);
	
	AdjustHeight();
	Repaint();								// Redraw caption
}

void CMultiLineCaption::OnNcPaint(HRGN hRgn)
{
	if (GetBackground()->IsUsingSystemColors())
	{
		if (m_ActiveSysColor != GetSysColor(COLOR_ACTIVECAPTION) ||
			m_InactiveSysColor != GetSysColor(COLOR_INACTIVECAPTION))
		{
			m_ActiveSysColor = GetSysColor(COLOR_ACTIVECAPTION);
			m_InactiveSysColor = GetSysColor(COLOR_INACTIVECAPTION);
			Repaint();
		}
	}
	CCaption::OnNcPaint(hRgn);	
}

void CMultiLineCaption::OnSize( UINT nType, int cx, int cy )
{
	Default();
	Refresh();
}

void CMultiLineCaption::OnExitSizeMove()
{
	Default();
	Refresh();
}

/////////////
// Paint wrapped header text in rectangle
//
void CMultiLineCaption::PaintText(CDC* pDC)
{
	ASSERT(m_pWndHooked);
	ASSERT(m_pWndHooked);
	CWnd& wnd = *m_pWndHooked;

	// For minimised windows, forget all the multi-line stuff
	if (wnd.IsIconic())
	{
		CCaption::PaintText(pDC);
		return;
	}

	pDC->SetBkMode(TRANSPARENT);						// draw on top of our background

	// Get window text to paint
	//
	CString text;
	wnd.GetWindowText(text);

	// Set text color into device context
	//
	COLORREF textColor = GetTextColor(m_bActive);
	pDC->SetTextColor(textColor);

	// Get the rect to paint the text in
	//
	CRect paintRect(CalcTextRect(m_MaxLines > 1 ? TextFormat : SingleLineTextFormat));

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

	// Paint text into rectangle
	//
	if (m_MaxLines > 1)
	{
		int height = pDC->DrawText(text, paintRect, TextFormat|DT_TOP);

		// Unfortunately, DT_END_ELLIPSIS doesn't work with DT_WORDBREAK, so we must handle
		// ellipsis manually...	 (unless you know better)
		//
		if (height > paintRect.Height())		// Text won't fit, so output truncation ellipsis
		{
			int lenEllipsis = pDC->GetTextExtent(ellipsis).cx;
			paintRect.left = paintRect.right;
			paintRect.right += lenEllipsis;
			paintRect.top = paintRect.bottom - (GetLineHeight(pDC) + 2);

			pDC->DrawText(ellipsis, paintRect, DT_SINGLELINE|DT_LEFT); 
		}
	}
	else // m_MaxLines == 1
	{
		pDC->DrawText(text, paintRect, SingleLineTextFormat);
	}
}

//////////////
// Refresh caption when window is tiled
//
void CMultiLineCaption::OnWindowPosChanged( WINDOWPOS* lpwndpos )
{
	Default();						// Let Windows handle it

	// These flags together seem unique to windows being tiled
	//
	UINT tileFlags = SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOCOPYBITS;
	
	if ((lpwndpos->flags & tileFlags) == tileFlags)
	{
		Refresh();
	}
}

void CMultiLineCaption::OnGetMinMaxInfo( MINMAXINFO FAR* lpMMI )
{
	Default();							// Let Windows handle it

	// Ensure min height is correct - we mustn't allow a height less 
	// than the caption height
	//
	lpMMI->ptMinTrackSize.y = 
		max(lpMMI->ptMinTrackSize.y, GetCaptionHeight() + GetFrameSize().cy);
}

////////////////
// Refresh on activate/deactivate for windows with different font
// sizes for active and inactive caption
//
void CMultiLineCaption::OnMDIActivate()
{
	AdjustHeight();
	Repaint();							// Redraw caption

	Default();							// Let Windows handle it
}

// Call Windows to do WM_NCCALCSIZE then set the top of the new client area to allow for 
// multi-line caption
//
LRESULT CMultiLineCaption::OnNcCalcSize( BOOL bCalcValidRects, LPNCCALCSIZE_PARAMS lpncsp )
{
	LRESULT res = Default();							// Let Windows handle it

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

	// When restoring from minimised (old client area left and right are both zero), CalcTextRect()
	// won't calculate the new client area correctly because the current client area is not valid.
	// In this case, skip it and let OnSize() trigger this function when the client area is valid.
	//
	BOOL wasIconic = lpncsp->rgrc[2].left == lpncsp->rgrc[2].right;
	BOOL isZoomed = wnd.IsZoomed();

	if (text.GetLength() > 0 && !isZoomed && !wasIconic)
	{
		// LPNCCALCSIZE_PARAMS are relative to parent client area (or screen origin if no parent),
		// so add vertical offset of child frame into parent client area 
		//
		lpncsp->rgrc[0].top =  GetChildOffset() + GetCaptionHeight();
	}
	return res;
} 

///////////////
// Return the rectangle occupied by caption text with the specified DrawText() format
//
CRect CMultiLineCaption::CalcTextRect(int textFormat)
{
	CString text;
	ASSERT(m_pWndHooked);
	m_pWndHooked->GetWindowText(text);

	// Get caption font	and select into window DC
	//
	CWindowDC dc(m_pWndHooked);
	AutoSelector a(&dc, GetFont(m_bActive));

	// Size caption to exclude buttons & icon
	//
	CRect textRect = GetCaptionRect();

	textRect.left  += GetIconWidth();				// start text after icon
	textRect.right -= GetButtonsWidth() + 4;		// end text before buttons

	// Use DrawText to calculate the height necessary to contain the text
	//
	int width = textRect.Width();
	int height = dc.DrawText(text, textRect, textFormat | DT_CALCRECT | DT_EXTERNALLEADING);

	// Restrict to specified maximun height
	//
	int maxHeight = GetLineHeight(&dc) * m_MaxLines;
	if (height > maxHeight)
		textRect.bottom = textRect.top + maxHeight;

	// Restrict to minimum height
	//
	int minTextHeight = GetSystemMetrics(SM_CYCAPTION) - 6; 
	if (textRect.Height() < minTextHeight)
		textRect.bottom = textRect.top + minTextHeight;

	// I'll work out why I need this when I get more time... :-)
 	textRect.top -=	DrawTextFudgeFactor;

	// DrawText word-wrap fails to clip for very narrow rects, so ignore if necessary
	//
	if (textRect.Width() > width)
		textRect.right = textRect.left + width;

	return textRect;
}

///////////
// Returns the height of a line of text in the specified device context
//
LONG CMultiLineCaption::GetLineHeight(CDC* pDC)
{
    TEXTMETRIC textMetric;
    pDC->GetOutputTextMetrics(&textMetric);
    return textMetric.tmHeight;
}

///////////
// Returns the total caption height needed to contain the text
//
int CMultiLineCaption::GetCaptionHeight()
{
	return GetFrameSize().cy + 
		CalcTextRect(m_MaxLines > 1 ? TextFormat : SingleLineTextFormat).bottom + 
		TextLowerBorder;
}

///////////
// Return vertical offset of child frame into parent client area (for OnNcCalcSize).
//
int CMultiLineCaption::GetChildOffset()
{
	CRect parentRect(0,0,0,0);		
	ASSERT(m_pWndHooked);
	CWnd& wnd = *m_pWndHooked;

	CWnd* pParent = wnd.GetParent();
	if (pParent)
	{
		pParent->GetClientRect(parentRect );
		pParent->ClientToScreen(parentRect);
	}
	CRect hookedRect;
	wnd.GetWindowRect( &hookedRect );
	return hookedRect.top - parentRect.top;
}

/////////////
// Override of base class function:
// Return caption drawing rectangle, the lower edge abutting the
// client area (as adjusted in OnNcCalcSize())
//
CRect CMultiLineCaption::GetCaptionRect()
{
	ASSERT(m_pWndHooked);
	CWnd& wnd = *m_pWndHooked;

	// When minimised, draw normal size caption
	if (wnd.IsIconic())
		return CCaption::GetCaptionRect();

	// Get client rect in screen co-ordinates
	//
	CRect captionRect;
	wnd.GetClientRect(captionRect);
	wnd.ClientToScreen(captionRect);

	// Convert client rect to window co-ordinates
	//
	CRect windowRect;
	wnd.GetWindowRect(windowRect);
	captionRect -= windowRect.TopLeft();

	// Set top and botton for caption
	//
	captionRect.bottom = captionRect.top;	// bottom of caption is top of client window
	captionRect.top = GetFrameSize().cy;	// top of caption is window rect top + frame thickness

	return captionRect;
}

///////////
// Ensure caption size is recalculated by triggering an OnNcCalcSize()
//
void CMultiLineCaption::Repaint()
{
	ASSERT(m_pWndHooked);
	CWnd& wnd = *m_pWndHooked;

	// Trigger an OnNcCalcSize() to resize caption
	//
	Invalidate();
	wnd.SetWindowPos(&CWnd::wndTopMost, 0,0,0,0, 
		SWP_FRAMECHANGED|SWP_NOACTIVATE|SWP_NOZORDER|SWP_NOSIZE|SWP_NOMOVE);
}

///////////
// Redraw caption and client area, sizing window as required
void CMultiLineCaption::Refresh()
{
	AdjustHeight();							 
	Repaint();								// Redraw caption

	ASSERT(m_pWndHooked);
	CWnd& wnd = *m_pWndHooked;
	wnd.Invalidate();						// paint client area
	UpdateWindow(wnd.GetSafeHwnd());		
}


void CMultiLineCaption::AdjustHeight()
{
	// Size window to caption if caption is taller
	//
	ASSERT(m_pWndHooked);
	CWnd& wnd = *m_pWndHooked;
	CRect wndRect;
	wnd.GetWindowRect(wndRect);
	if (wndRect.Height() < GetCaptionHeight() + GetFrameSize().cy)
	{
		wnd.SetWindowPos(&CWnd::wndTopMost, 0,0,wndRect.Width(),
		                 GetCaptionHeight() + GetFrameSize().cy, 
						 SWP_FRAMECHANGED|SWP_NOACTIVATE|SWP_NOZORDER|SWP_NOMOVE);
	}
}

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