////////////////////////////////////////////////////////////////
//
// 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);
}