Click here to Skip to main content
15,886,756 members
Articles / Desktop Programming / MFC

Ownerdraw Tab Controls - Borders and All

Rate me:
Please Sign up or sign in to vote.
4.90/5 (44 votes)
30 Jun 2002CPOL2 min read 431.9K   17.3K   103   55
A framework for overriding all aspects of a tab control's apprearance, including the borders, the background and of course the tabs themselves.

Sample Image - TabControl.jpg

Introduction

Tab controls when used well are a very valuable ui tool.

However, I have more recently felt that the original (and current) look of the standard tab control was too heavy: instead of just looking at the contents of the tab control I was distracted by the 3D borders which overplay the folder metaphor.

As many of you will know, Windows provides for limited customizing of the tab control appearance via owner-draw but this is restricted to the tab labels only and not to the tab border or the background.

The obvious solution was to override WM_PAINT to extend the owner-draw mechanism to handle the main tab control border and the individual label borders, and to override WM_ERASEBKGND to paint the background.

The result is CBaseTabControl and CEnTabControl.

CBaseTabControl is the class which contains the necessary hooks to allow derived classes to paint whichever parts of the tab control they choose, and CEnTabControl is an example derived class which implements some features that I have found useful.

Some Comments on the Source Code

  • The background painting is done in CBaseTabControl because this has to be done in a very specific way to prevent flicker and overcome some assumptions Microsoft made during the underlying tab control development (see source comments)
  • If you request that you want to override aspects of the drawing and then do not provide a virtual override, CBaseTabControl will ASSERT just like the standard MFC code and no drawing will be done.
  • CEnTabControl implements its custom drawing using static flags so that every tab control instantiated will share the same attributes. I did it this way so that all the tab controls in an application will have the same look and feel.
  • If you use property sheets alot and want the same functionality, DON'T worry. All you have to do is subclass the tab control within the property sheet like this:
    BOOL CMyPropertySheet::OnInitDialog() 
    {
    	CPropertySheet::OnInitDialog();
    
    	...
    
    	// subclass tab control
    	m_tabCtrl.SubclassDlgItem(
                CPropertySheet::GetTabControl()->GetDlgCtrlID(), this);
    
    	...
    }

Code Fix 1.1

Gian (saviour@libero.it) correctly pointed out that if you try to attach a CImageList to the tab control then you get an ASSERT in the drawing code.

This occurs because the drawing code temporarily attaches the tab control imagelist to a CImageList for drawing purposes and MFC asserts that its already attached to a CImageList (the original).

The reason MFC asserts is because it keeps a static map for converting between HIMAGELIST and CImageList*, and the implementation of the underlying map prevents an HIMAGELIST being mapped to more than one CImageList*.

The fix is to use ImageList_Draw() for drawing.

Notes

  • The code has not been thoroughly tested for bottom and side tabs, so please don't complain if it doesn't work as expected.

License

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


Written By
Software Developer Maptek
Australia Australia
.dan.g. is a naturalised Australian and has been developing commercial windows software since 1998.

Comments and Discussions

 
AnswerRe: one tab looks like a command button - HOW? Pin
.dan.g.18-Nov-04 15:00
professional.dan.g.18-Nov-04 15:00 
GeneralNot correct repaint Pin
Mirikos19-Sep-04 11:29
Mirikos19-Sep-04 11:29 
QuestionHow to change tab size Pin
IMANTHA3-Aug-04 1:23
IMANTHA3-Aug-04 1:23 
AnswerRe: How to change tab size Pin
.dan.g.3-Aug-04 17:08
professional.dan.g.3-Aug-04 17:08 
GeneralRe: How to change tab size Pin
IMANTHA4-Aug-04 22:10
IMANTHA4-Aug-04 22:10 
GeneralDisplay Icoms with 256 colors and mor Pin
Ralph18-May-04 3:39
Ralph18-May-04 3:39 
GeneralRe: Display Icoms with 256 colors and mor Pin
.dan.g.18-May-04 21:07
professional.dan.g.18-May-04 21:07 
GeneralCool article man. Now Drawing on bottom is OK Pin
vmonster20-Feb-04 3:24
vmonster20-Feb-04 3:24 
After all, sorry by my english.
I make some changes on original code, to make tab on bottom correctly and to show the selcted tab text in bold.

DWORD CTabCtrlEx::m_sdwCustomLook = 0;

enum { PADDING = 3, EDGE = 20};

//////////////////////////////////////////////////////////////////////////////
// helpers

COLORREF Darker(COLORREF crBase, float fFactor)
{
ASSERT ( fFactor < 1.0f && fFactor > 0.0f );

fFactor = min ( fFactor, 1.0f );
fFactor = max ( fFactor, 0.0f );

BYTE bRed, bBlue, bGreen;
BYTE bRedShadow, bBlueShadow, bGreenShadow;

bRed = GetRValue ( crBase );
bBlue = GetBValue ( crBase );
bGreen = GetGValue ( crBase );

bRedShadow = ( BYTE )( bRed * fFactor );
bBlueShadow = ( BYTE )( bBlue * fFactor );
bGreenShadow = ( BYTE )( bGreen * fFactor );

return RGB ( bRedShadow, bGreenShadow, bBlueShadow );
}

COLORREF Lighter(COLORREF crBase, float fFactor)
{
ASSERT ( fFactor > 1.0f );

fFactor = max ( fFactor, 1.0f );

BYTE bRed, bBlue, bGreen;
BYTE bRedHilite, bBlueHilite, bGreenHilite;

bRed = GetRValue ( crBase );
bBlue = GetBValue ( crBase );
bGreen = GetGValue ( crBase );

bRedHilite = ( BYTE )min ( ( int )( bRed * fFactor ), 255 );
bBlueHilite = ( BYTE )min ( ( int )( bBlue * fFactor ), 255 );
bGreenHilite = ( BYTE )min ( ( int )( bGreen * fFactor ), 255 );

return RGB ( bRedHilite, bGreenHilite, bBlueHilite );
}

CSize FormatText(CString& sText, CDC* pDC, int nWidth)
{
CRect rect ( 0, 0, nWidth, 20 );
UINT uFlags = DT_CALCRECT | DT_SINGLELINE | DT_MODIFYSTRING | DT_END_ELLIPSIS;

::DrawText ( pDC->GetSafeHdc (), sText.GetBuffer ( sText.GetLength () + 4 ), -1, rect, uFlags );
sText.ReleaseBuffer ();

return pDC->GetTextExtent ( sText );
}

// helpers
////////////////////////////////////////////////////////////////////////////////////////

/////////////////////////////////////////////////////////////////////////////
// CTabCtrlEx

CTabCtrlEx::CTabCtrlEx(int nType)
{
m_crBack = ( COLORREF )-1; // use default color
m_nDrawType = TCF_NONE;
m_nOldSel = -1;

EnableDraw ( nType );
}

CTabCtrlEx::~CTabCtrlEx()
{
}


BEGIN_MESSAGE_MAP(CTabCtrlEx, CTabCtrl)
//{{AFX_MSG_MAP(CTabCtrlEx)
ON_WM_ERASEBKGND()
ON_WM_PAINT()
ON_WM_LBUTTONDOWN()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CTabCtrlEx message handlers

BOOL CTabCtrlEx::OnEraseBkgnd(CDC* pDC)
{
// TODO: Add your message handler code here and/or call default

CTabCtrl::OnEraseBkgnd(pDC);

Invalidate ( FALSE );

return TRUE;
}

void CTabCtrlEx::OnPaint()
{
CPaintDC dc(this); // device context for painting

// TODO: Add your message handler code here

// Do not call CTabCtrl::OnPaint() for painting messages
if ( m_nDrawType == TCF_NONE )
Default();
else if ( m_nDrawType == TCF_TABS )
{
ASSERT ( GetStyle () & TCS_OWNERDRAWFIXED );
Default ();
}
else // all
{
CPaintDC dc ( this ); // device context for painting

DRAWITEMSTRUCT dis;
dis.CtlType = ODT_TAB;
dis.CtlID = GetDlgCtrlID ();
dis.hwndItem = GetSafeHwnd ();
dis.hDC = dc.GetSafeHdc ();
dis.itemAction = ODA_DRAWENTIRE;

// draw the rest of the border
CRect rClient, rPage;
GetClientRect ( &dis.rcItem );
rPage = dis.rcItem;
AdjustRect ( FALSE, rPage );

BOOL bBottom = ( GetStyle () & TCS_BOTTOM ) == TCS_BOTTOM;

if ( bBottom )
dis.rcItem.bottom = rPage.bottom;
else
dis.rcItem.top = rPage.top - 2;

DrawMainBorder ( &dis );

// paint the tabs first and then the borders
int nTab = GetItemCount () - 1;
int nSel = GetCurSel ();

if ( !nTab ) // no pages added
return;

// prepare dc
CFont* currFont = GetFont ();
CFont* pOldFont = dc.SelectObject ( currFont );

VERIFY ( GetItemRect ( 0, &dis.rcItem ) );

for ( int iTab = 0; iTab < nTab; iTab++ )
{
VERIFY ( GetItemRect ( iTab, &dis.rcItem ) );

if ( iTab != nSel )
{
dis.itemID = iTab;
dis.itemState = 0;

dis.rcItem.bottom -= bBottom ? 3 : 2;

DrawItem ( &dis );
DrawItemBorder ( &dis );
}
}

// now selected tab
dis.itemID = nSel;
dis.itemState = ODS_SELECTED;

VERIFY ( GetItemRect ( nSel, &dis.rcItem ) );

dis.rcItem.bottom += bBottom ? 3 : 2;
dis.rcItem.top -= bBottom ? 3 : 2;

DrawItem ( &dis );
DrawItemBorder ( &dis );

dc.SelectObject ( pOldFont );
}
}

void CTabCtrlEx::DrawItem(LPDRAWITEMSTRUCT lpdis)
{
CDC* pDC = CDC::FromHandle ( lpdis->hDC );
HIMAGELIST hilTabs = ( HIMAGELIST )TabCtrl_GetImageList ( GetSafeHwnd () );

BOOL bSelected = ( lpdis->itemID == ( UINT )GetCurSel () );
BOOL bColor = ( m_sdwCustomLook & TCF_COLOR );
BOOL bBottom = ( GetStyle () & TCS_BOTTOM ) == TCS_BOTTOM;

CRect rItem ( lpdis->rcItem );

CString sTemp;
TC_ITEM tci;
tci.mask = TCIF_TEXT | TCIF_IMAGE;
tci.pszText = sTemp.GetBuffer ( 100 );
tci.cchTextMax = 99;

GetItem ( lpdis->itemID, &tci );
sTemp.ReleaseBuffer();

if ( bSelected )
{
CFont font;
LOGFONT lgFont;
GetFont ()->GetLogFont ( &lgFont );
lgFont.lfWeight = FW_BOLD;
font.CreateFontIndirect ( &lgFont );
pDC->SelectObject ( &font );

rItem.bottom -= bBottom ? 2 : 1;
}
else
{
if ( bBottom )
{
rItem.top -= 1;
rItem.bottom += 1;
}
else
rItem.bottom += 2;
}

// tab
// blend from back color to COLOR_3DFACE if 16 bit mode or better
COLORREF crFrom = GetTabColor ( bSelected );

if ( m_sdwCustomLook & TCF_GRADIENT && pDC->GetDeviceCaps ( BITSPIXEL ) >= 16 )
{
COLORREF crTo = bSelected ? ::GetSysColor ( COLOR_3DFACE ) :
Darker ( !bColor || m_crBack == -1 ? ::GetSysColor ( COLOR_3DFACE ) : m_crBack, 0.6f );
//crFrom = bSelected ? GetTabColor ( bSelected ) : Lighter ( ::GetSysColor ( COLOR_3DFACE ), 1.7f );

int nROrg = GetRValue ( crFrom );
int nGOrg = GetGValue ( crFrom );
int nBOrg = GetBValue ( crFrom );
int nRDiff = GetRValue ( crTo ) - nROrg;
int nGDiff = GetGValue ( crTo ) - nGOrg;
int nBDiff = GetBValue ( crTo ) - nBOrg;

int nHeight = rItem.Height ();

for ( int nLine = 0; nLine < nHeight; nLine += 2 )
{
int nRed = nROrg + ( nLine * nRDiff ) / nHeight;
int nGreen = nGOrg + ( nLine * nGDiff ) / nHeight;
int nBlue = nBOrg + ( nLine * nBDiff ) / nHeight;

if ( bBottom )
pDC->FillSolidRect ( CRect ( rItem.left, rItem.bottom - nLine - 2, rItem.right, rItem.bottom - nLine ), RGB ( nRed, nGreen, nBlue ) );
else
pDC->FillSolidRect ( CRect ( rItem.left, rItem.top + nLine, rItem.right, rItem.top + nLine + 2 ), RGB ( nRed, nGreen, nBlue ) );
}
}
else // simple solid fill
pDC->FillSolidRect ( rItem, crFrom );

// text & icon
rItem.left += PADDING;

if ( bBottom )
rItem.top += PADDING + ( bSelected ? 1 : -2 );
else
rItem.top += PADDING + ( bSelected ? 1 : 0 );

pDC->SetBkMode ( TRANSPARENT );

// icon
if ( hilTabs )
{
ImageList_Draw ( hilTabs, tci.iImage, *pDC, rItem.left, rItem.top, ILD_TRANSPARENT );
rItem.left += 16 + PADDING;
}

// text
rItem.left -= (PADDING + 1);

pDC->SetTextColor ( GetTabTextColor ( bSelected ) );
pDC->DrawText ( sTemp, rItem, DT_NOPREFIX | DT_CENTER );
}

void CTabCtrlEx::DrawItemBorder(LPDRAWITEMSTRUCT lpdis)
{
ASSERT ( m_sdwCustomLook & TCF_FLAT );

BOOL bSelected = ( lpdis->itemID == ( UINT )GetCurSel () );
BOOL bBackTabs = ( m_sdwCustomLook & TCF_BACKTABS );
BOOL bBottom = ( GetStyle () & TCS_BOTTOM ) == TCS_BOTTOM;

CRect rItem ( lpdis->rcItem );
CDC* pDC = CDC::FromHandle ( lpdis->hDC );

COLORREF crTab = GetTabColor ( bSelected );
COLORREF crHighlight = Lighter ( crTab, 1.5f );
COLORREF crShadow = Darker ( crTab, 0.75f );

if ( bSelected || bBackTabs )
{
if ( bBottom )
{
if ( m_nOldSel != -1 && lpdis->itemID == ( UINT )m_nOldSel )
{
CRect rect;
GetItemRect ( m_nOldSel, &rect );
rect.InflateRect ( 0, 5 );
InvalidateRect ( &rect, TRUE );
m_nOldSel = -1;
}

// edges
if ( bSelected )
{
pDC->FillSolidRect ( CRect ( rItem.left, rItem.top, rItem.left + 1, rItem.bottom - 2 ), crHighlight );
pDC->FillSolidRect ( CRect ( rItem.left, rItem.bottom - 3, rItem.right, rItem.bottom - 2 ), crShadow );
pDC->FillSolidRect ( CRect ( rItem.right - 1, rItem.top, rItem.right, rItem.bottom - 2 ), crShadow );
}
else
{
pDC->FillSolidRect ( CRect ( rItem.left, rItem.top - 1, rItem.left + 1, rItem.bottom + 1 ), crHighlight );
pDC->FillSolidRect ( CRect ( rItem.left + 1, rItem.bottom, rItem.right, rItem.bottom + 1 ), crShadow );
pDC->FillSolidRect ( CRect ( rItem.right - 1, rItem.top - 1, rItem.right, rItem.bottom ), crShadow );
}
}
else
{
rItem.bottom += bSelected ? -1 : 1;

// edges
pDC->FillSolidRect ( CRect ( rItem.left, rItem.top, rItem.left + 1, rItem.bottom ), crHighlight );
pDC->FillSolidRect ( CRect ( rItem.left, rItem.top, rItem.right, rItem.top + 1 ), crHighlight );
pDC->FillSolidRect ( CRect ( rItem.right - 1, rItem.top, rItem.right, rItem.bottom ), crShadow );
}
}
else // draw simple dividers
{
if ( bBottom )
{
pDC->FillSolidRect ( CRect ( rItem.left, rItem.top - 1, rItem.left + 1, rItem.bottom ), crShadow );
pDC->FillSolidRect ( CRect ( rItem.right - 1, rItem.top - 1, rItem.right, rItem.bottom ), crShadow );
}
else
{
pDC->FillSolidRect ( CRect ( rItem.left - 1, rItem.top, rItem.left, rItem.bottom ), crShadow );
pDC->FillSolidRect ( CRect ( rItem.right - 1, rItem.top, rItem.right, rItem.bottom ), crShadow );
}
}
}

void CTabCtrlEx::DrawMainBorder(LPDRAWITEMSTRUCT lpdis)
{
CRect rBorder ( lpdis->rcItem );
CDC* pDC = CDC::FromHandle ( lpdis->hDC );

COLORREF crTab = GetTabColor();
COLORREF crHighlight = Lighter ( crTab, 1.5f );
COLORREF crShadow = Darker ( crTab, 0.75f );

pDC->Draw3dRect ( rBorder, crHighlight, crShadow );
}

void CTabCtrlEx::PreSubclassWindow()
{
// TODO: Add your specialized code here and/or call the base class

CTabCtrl::PreSubclassWindow();

if ( m_nDrawType != TCF_NONE )
ModifyStyle ( 0, TCS_OWNERDRAWFIXED );
}

void CTabCtrlEx::SetBkgndColor(COLORREF color)
{
// set new color
m_crBack = color;

// redraw
if ( GetSafeHwnd () )
Invalidate ();
}

BOOL CTabCtrlEx::EnableDraw(int nType)
{
ASSERT ( nType >= TCF_NONE && nType <= TCF_ALL );

if ( nType < TCF_NONE || nType > TCF_ALL )
return FALSE;

m_nDrawType = nType;

if ( GetSafeHwnd () )
{
if ( m_nDrawType != TCF_NONE )
ModifyStyle ( 0, TCS_OWNERDRAWFIXED );
else
ModifyStyle ( TCS_OWNERDRAWFIXED, 0 );

Invalidate ();
}

return TRUE;
}

void CTabCtrlEx::SetText(int index, CString strText)
{
TCITEM tci;
tci.mask = TCIF_TEXT;
tci.pszText = ( LPTSTR )( LPCTSTR )strText;

SetItem ( index, &tci );
}

CString CTabCtrlEx::GetText(int index)
{
TCITEM tci;

tci.mask = TCIF_TEXT;

GetItem ( index, &tci );

return tci.pszText;
}

COLORREF CTabCtrlEx::GetTabColor(BOOL bSelected)
{
BOOL bColor = ( m_sdwCustomLook & TCF_COLOR );
BOOL bHiliteSel = ( m_sdwCustomLook & TCF_SELECTION );
BOOL bBackTabs = ( m_sdwCustomLook & TCF_BACKTABS );
BOOL bFlat = ( m_sdwCustomLook & TCF_FLAT );

if ( bSelected && bHiliteSel )
{
if ( bColor )
return Lighter ( ( m_crBack == -1 ) ? ::GetSysColor ( COLOR_3DFACE ) : m_crBack, 1.4f );
else
return Lighter ( ::GetSysColor ( COLOR_3DFACE ), 1.4f );
}
else if ( !bSelected )
{
if ( bBackTabs || !bFlat )
{
if ( bColor )
return Darker ( ( m_crBack == -1 ) ? ::GetSysColor ( COLOR_3DFACE ) : m_crBack, 0.9f );
else
return Darker ( ::GetSysColor ( COLOR_3DFACE ), 0.9f );
}
else
return ( m_crBack == -1 ) ? ::GetSysColor ( COLOR_3DFACE ) : m_crBack;
}

// else
return ::GetSysColor ( COLOR_3DFACE );
}

COLORREF CTabCtrlEx::GetTabTextColor(BOOL bSelected)
{
BOOL bColor = ( m_sdwCustomLook & TCF_COLOR );
BOOL bFlat = ( m_sdwCustomLook & TCF_FLAT );

if ( bSelected )
{
return ::GetSysColor ( COLOR_WINDOWTEXT );
}
else
{
if (bColor || bFlat)
return Darker ( ( m_crBack == -1 ) ? ::GetSysColor ( COLOR_3DFACE ) : m_crBack, 0.5f );
else
return Darker ( ::GetSysColor ( COLOR_3DFACE ), 0.5f );
}

// else
return Darker ( ::GetSysColor ( COLOR_3DFACE ), 0.5f );
}

void CTabCtrlEx::EnableCustomLook(DWORD dwStyle)
{
m_sdwCustomLook = dwStyle;
}

BOOL CTabCtrlEx::GetItemRect(int nItem, LPRECT lpRect) const
{
BOOL bRet = CTabCtrl::GetItemRect ( nItem, lpRect );

if ( bRet && nItem )
{
int nItens = GetItemCount () - 1;
int nWidth = (lpRect->right - lpRect->left);
lpRect->left += ( 10 * (nItem) );
lpRect->right = lpRect->left + nWidth + 10;
}
else
lpRect->right += 10;

return bRet;
}

int CTabCtrlEx::HitTestEx(CPoint point) const
{
int nTabs = GetItemCount () - 1;

for ( int iTab = 0; iTab < nTabs; iTab++ )
{
CRect rect;

VERIFY ( GetItemRect ( iTab, &rect ) );

if ( rect.PtInRect ( point ) )
return iTab;
}

return -1;
}

void CTabCtrlEx::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default

//CTabCtrl::OnLButtonDown(nFlags, point);

m_nOldSel = GetCurSel ();

int nSel = HitTestEx ( point );
SetCurSel ( nSel );

NMHDR hdr;
hdr.hwndFrom = this->GetSafeHwnd ();
hdr.code = TCN_SELCHANGE;
hdr.idFrom = IDC_TAB_VIEW;

GetParent ()->SendMessage ( WM_NOTIFY, 0, ( LPARAM )&hdr );

Invalidate ( FALSE );
}
Big Grin | :-D
GeneralCool enough!! thanks. Pin
t2di4u23-Dec-03 21:30
t2di4u23-Dec-03 21:30 
GeneralGet the initial tab for the TabCtrl Pin
Ruben93815-Sep-03 15:43
Ruben93815-Sep-03 15:43 
Generalsupport for bottom style Pin
rtessler16-Mar-03 14:11
rtessler16-Mar-03 14:11 
AnswerRe: support for bottom style Pin
Bartosz Bien18-Apr-06 10:43
Bartosz Bien18-Apr-06 10:43 
GeneralOnwer Draw spin control only Pin
Wolfram Steinke22-Feb-03 12:48
Wolfram Steinke22-Feb-03 12:48 
GeneralRe: Onwer Draw spin control only Pin
.dan.g.25-Feb-03 18:44
professional.dan.g.25-Feb-03 18:44 
GeneralRe: Onwer Draw spin control only Pin
Wolfram Steinke27-Feb-03 11:19
Wolfram Steinke27-Feb-03 11:19 
QuestionWhat incase of bitmapped Dialogs Pin
confuzed121-Nov-02 11:34
confuzed121-Nov-02 11:34 
AnswerRe: What incase of bitmapped Dialogs Pin
.dan.g.21-Nov-02 14:16
professional.dan.g.21-Nov-02 14:16 
GeneralPossible to remove spin control Pin
3-Jul-02 0:34
suss3-Jul-02 0:34 
GeneralRe: Possible to remove spin control Pin
.dan.g.3-Jul-02 17:13
professional.dan.g.3-Jul-02 17:13 
GeneralRe: Possible to remove spin control Pin
5-Jul-02 13:02
suss5-Jul-02 13:02 
General'Icons Pin
27-Jun-02 8:16
suss27-Jun-02 8:16 
GeneralRe: 'Icons Pin
.dan.g.27-Jun-02 17:35
professional.dan.g.27-Jun-02 17:35 
GeneralRe: Icons Pin
Gian27-Jun-02 21:45
Gian27-Jun-02 21:45 
GeneralRe: Icons Pin
.dan.g.30-Jun-02 14:39
professional.dan.g.30-Jun-02 14:39 
GeneralVertical style not supported! Pin
31-Jan-02 16:20
suss31-Jan-02 16:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.