Introduction
I first saw the new Microsoft Visual Studio .NET after beta 2 was released. One of the things that stood out to me was how nice the tabs looked. It also finally included a tabbed MDI.
I soon after found Bjarke Viksøe's Cool Tab Controls. I was especially interested in the "DotNetTabCtrl
". Bjarke soon linked to an updated version by Pascal Binggeli, that had support for the tabs to have images. There were several minor issues with both versions, but when I examined the code, it seemed an excellent foundation to build from.
Starting with the updates from Pascal, I began making my own updates. Initial updates included adding divider lines between the tabs, adjusting the layout to closer match VS.NET, adding implementations to use the tab control for a tabbed MDI, and adding other improvements. At first, I tried to make minimal changes to the base CCustomTabCtrl
. I eventually began evolving even the base CCustomTabCtrl
to accommodate my vision for what this could be.
Custom Tab Controls
CCustomTabCtrl
is the base, templatized, ATL::CWindowImpl
derived class to help implement a customized tab control window. The painting is double buffered for flicker free drawing. Clients never use this class directly, but instead use a class derived from it. Included out of the box are a handful of tab controls derived from CCustomTabCtrl
:
CDotNetTabCtrl
in "DotNetTabCtrl.h".Written by Daniel Bowen. Tab control with the look and feel of the tabs in VS.NET. Used for both MDI tabs, pane window tabs, and others.
CDotNetButtonTabCtrl
in "DotNetTabCtrl.h".Written by Daniel Bowen. Tab control with the VS.NET style of button tabs (to look like VS.NET view of HTML with the Design/HTML buttons).
CButtonTabCtrl
in "SimpleTabCtrls.h",CButtonTabCtrl
in "SimpleDlgTabCtrls.h".Written by Bjarke Vikøe, updated for new
CCustomTabCtrl
by Daniel Bowen. Push button style tabs. The "DlgTabCtrl
" version is meant to subclass an existing static control.CFolderTabCtrl
in "SimpleTabCtrls.h",CFolderTabCtrl
in "SimpleDlgTabCtrls.h".Written by Bjarke Vikøe, updated for new
CCustomTabCtrl
by Daniel Bowen. Trapezoidal folder tabs similar to the tabs used in the output pane of Visual Studio 6. The "DlgTabCtrl
" version is meant to subclass an existing static control.CSimpleDotNetTabCtrl
in "SimpleTabCtrls.h",CSimpleDotNetTabCtrl
in "SimpleDlgTabCtrls.h".Written by Bjarke Vikøe, updated for new
CCustomTabCtrl
by Daniel Bowen. This is essentially Bjarke's original "CDotNetTabCtrl
" with a flat tab look. The "DlgTabCtrl
" version is meant to subclass an existing static control.
CCustomTabCtrl
derived tab controls are meant to work similar to other common controls such as the list view (SysListView32
) and the tree view (SysTreeView32
). There is already an existing tab control (SysTabControl32
) that is a common control. So why do we need CCustomTabCtrl
? Because there are several customizations that are hard to do with it. SysTabControl32
was originally created to implement the task bar in Windows 95 and later (just like SysListView32
and SysTreeView32
were originally created for Windows Explorer). There are several features that other common controls have that SysTabControl32
is missing, such as custom drawing, insert and delete notifications, position displacement and more.
Custom Tab Items
One of the differences between CCustomTabCtrl
derived tab controls and common controls in Windows are how items are managed. Common controls are meant to work with any client that can handle structures and window programming - from x86 assembly to Visual Basic. The "item" in most common controls is a structure. To get and set items, you fill out a structure with a mask to identify the fields you are interested in. There are several side effects of this design decision. One is that there is often more "memory copying" than really needs to be happening, especially with getting and setting text. Another is that there is no good way to know if a particular field is actually in use. Another effect is that any user data is always put into an "LPARAM
" into one of these structures, cast to and from the real type of data.
CCustomTabCtrl
takes a different approach, and lets you use any C structure or C++ class for the item that provides the needed interface. There are two such classes available out of the box - CCustomTabItem
and CTabViewTabItem
. The type of structure or class is a template parameter on the tab control class. If you want to use the tab control, and have instances of your own class for the tab items, the easiest thing to do is inherit from CCustomTabItem
or CTabViewTabItem
, and extend it to provide the extra functionality you need, then specify this new class as a parameter. To see the interface needed, simply look at CCustomTabItem
.
Using Custom Tab Controls
Tab controls based on CCustomTabCtrl
are meant to be used either as a stand-alone window, or to subclass an existing control on a dialog such as a static control. The parent of the tab control is responsible for creating, destroying, sizing and positioning the tab control window appropriately along side other child windows.
Depending on which custom tab control you use, it will probably depend on some system metrics to figure out which colors and fonts to use. If the user changes these system metrics (for example, by changing items on the "appearance" tab of the display control panel), the tab control can pick up these changes - but only if you propagate WM_SETTINGCHANGE
from the main frame to the tab control. This can be done by handling WM_SETTINGCHANGE
in the main frame, then calling CWindow::SendMessageToDescendants
or the equivalent code. See the included sample applications for an example.
The samples provided with this article demonstrate how to use these tab controls as stand-alone windows used to switch between child "view" windows, where only one child view is visible at a time.
For a simple example of how to use a custom tab control for a tabbed MDI, see the "SimpleTabbedMDIDemo" sample. In this sample, the WTL wizard was run to create a default MDI Application. Instead of having CMainFrame
inherit from CMDIFrameWindowImpl
, change it to inherit from CTabbedMDIFrameWindowImpl
. Instead of CMDICommandBarCtrl
, use CTabbedMDICommandBarCtrl
. Then in each MDI child frame, inherit from CTabbedMDIChildWindowImpl
instead of CMDIChildWindowImpl
.
The "TabDemo" sample uses a tabbed MDI as well, but in addition has a "popup tool window frame" that uses CDotNetTabCtrl
to switch between child views. It also uses CDotNetButtonTabCtrl
for the child frame to switch between an HTML view and an edit view (of the source of the HTML).
The "DockingDemo" sample shows how you might integrate these custom tab controls with Sergey Klimov's WTL Docking Windows. I've included the source for his docking windows with permission. However - be sure to get the latest updates from him! In the file "TabbedDockingWindow.h", there is the class CTabbedDockingWindow
that inherits from CTabbedFrameImpl
and Sergey's CTitleDockingWindowImpl
.
The "TabbedSDISplitter" sample shows the use of a splitter in an SDI application, with the right side window "tabbed" to show multiple views. For another sample showing a slightly different use of these tabs in an SDI application, see the "SDITabbedSample" from Sergey Klimov's WTL Docking Windows and the work by Igor Katrayev.
It should be possible to integrate the custom tab controls in with other docking frameworks, splitters, etc. as well. As time allows, I'll try to get some more sample applications up.
Tabbed Frame
TabbedFrame.h contains classes to make it simple to add the ability to turn a frame window with one "view" into a tabbed frame window with a custom tab control to switch between one or more views. Included are the classes:
CCustomTabOwnerImpl
- MI class that helps implement the parent of the actual custom tab control window. The class doesn't have a message map itself, and is meant to be inherited from along-side aCWindowImpl
derived class. This class handles creation of the tab window as well as adding, removing, switching and renaming tabs based on anHWND
.CTabbedFrameImpl
- Base template to derive your specialized frame window class from to get a frame window with multiple "view" child windows that you switch between using a custom tab control (such asCDotNetTabCtrl
).CTabbedPopupFrame
- Simple class deriving fromCTabbedFrameImpl
that is suitable for implementing a tabbed "popup frame" tool window, with one or more views. See the "TabDemo" sample for an example of using this class.CTabbedChildWindow
- Simple class deriving fromCTabbedFrameImpl
that is suitable for implementing a tabbed child window, with one or more views. See the "TabbedSDISplitter" sample for an example of using this class.
Tabbed MDI
TabbedMDI.h contains classes to help implement a tabbed MDI using a custom tab control. There are multiple approaches to implementing a tabbed MDI. The approach that is used here is to subclass the out-of-the-box "MDIClient" from the OS, and require each MDI child frame to inherit from a special class CTabbedMDIChildWindowImpl
(instead of the normal CMDIChildWindowImpl
). Included are the classes:
CTabbedMDIFrameWindowImpl
- Instead of havingCMainFrame
inherit fromCMDIFrameWindowImpl
, you can have it inherit fromCTabbedMDIFrameWindowImpl
. For an out-of-the box WTL MDI application, there are three instances ofCMDIFrameWindowImpl
to replace withCTabbedMDIFrameWindowImpl
.CTabbedMDIChildWindowImpl
- If you want your MDI child window to have a corresponding tab in the MDI tab window, inherit from this class instead of fromCMDIChildWindowImpl
. This class also provides a couple of nice additional features:- When the child frame is created, if the previously active MDI child is maximized, the new window is also maximized.
- Your child frame class can specify
WS_MAXIMIZE
so that it is forced to start out life maximized. - The method
SetTitle
is provided to set the frame title (and possibly the corresponding MDI tab's text). - The method
SetTabText
lets you set the text for the corresponding MDI tab regardless of what the frame caption (window text) is. - The method
SetTabToolTip
lets you set the tooltip's text for the corresponding MDI tab. - Your derived class can handle the message
UWM_MDICHILDSHOWTABCONTEXTMENU
to show a context menu for the corresponding MDI tab. The default context menu is the window's system menu.
CTabbedMDIClient
- TheCTabbedMDIFrameWindowImpl
containsCTabbedMDIClient
, which subclasses the "MDI Client" window (from the OS, that manages the MDI child windows). It handles sizing/positioning the tab window, calling the appropriate Display, Remove, UpdateText for the tabs with theHWND
of the active child, etc. You can useCTabbedMDIClient
without usingCTabbedMDIFrameWindowImpl
. To do so, simply callSetTabOwnerParent(m_hWnd)
, thenSubclassWindow(m_hWndMDIClient)
on aCTabbedMDIClient
member variable after callingCreateMDIClient
in your main frame class.CMDITabOwner
- TheMDITabOwner
is the parent of the actual tab window (such asCDotNetTabCtrl
), and sibling to the "MDI Client" window. The tab owner tells the MDI child when to display a context menu for the tab (the default menu is the window's system menu). The tab owner changes the active MDI child when the active tab changes. It also does the real work of hiding and showing the tabs. It also handles adding, removing, and renaming tabs based on anHWND
.CTabbedMDICommandBarCtrl
- In your MDI application, instead of usingCMDICommandBarCtrl
, useCTabbedMDICommandBarCtrl
. It addresses a couple of bugs in WTL 7.0'sCMDICommandBarCtrl
, and allows you to enable or disable whether you want to see the document icon and min/max/close button in the command bar when the child is maximized.
Note to previous users
I originally posted my version of the "DotNetTabCtrl
" code to the WTL mailing list's site on groups.yahoo.com, and to Bjarke Viksøe. If you have used a previous version downloaded from either of these places, there are only a couple of updates to your client code to accommodate updates that I've made:
- Instead of:
#include "CoolTabCtrls.h"
now you need:
#include "CustomTabCtrl.h" #include "DotNetTabCtrl.h" // (include other versions of tab controls)
- Instead of calling
SetBoldSelectedTab
, use theCTCS_BOLDSELECTEDTAB
style. - Instead of notifications starting with "
TCN_
", they now start with "CTCN_
" (they are numerically identical to tab control "TCN_
" notifications where there is overlap). - Instead of using the tab control styles starting with "
TCS_
", use the custom tab control styles starting with "CTCS_
" (they are numerically identical to tab control "TCS_
" styles where there is overlap). - Use
CTCHITTESTINFO
instead ofTCHITTESTINFO
. - Tab related classes now take a template argument of the data item type.
- If you inherited off of
CCustomTabCtrl
orCDotNetTabCtrlImpl
or others, there have been a few other interface changes that hopefully will be obvious to address (you'd get compile errors). - For the history of the files prior to the release of this article, please reference the prior history.
Custom Tab Control Reference
Requirements
- ATL 3.0, 7.0, or 7.1
- WTL 7.1
Styles
CTCS_SCROLL
- This enables "scroll buttons". When the tab items don't get all the real estate they want, they overflow and the scroll button for that side is enabled. You can call the methodsGet
/SetScrollDelta
andGet
/SetScrollRepeat
to adjust how much is scrolled and how fast scrolling is repeated when holding down the button.CTCS_BOTTOM
- If this style is set, the tab window is meant to be displayed on the bottom of the client area. Otherwise, it is meant to be displayed on the top of the client area. It is up to the parent of the tab window to honor this style.CTCS_CLOSEBUTTON
- This enables a "close" button. When this button is clicked, the parent gets a "CTCN_CLOSE
" notification.CTCS_HOTTRACK
- Enables hot tracking tab items. If you are targeting Windows 2000/98 or later, be sure to#define WINVER
and/or_WIN32_WINNT
to 0x0500 or later, before including this file (usually in your precompiled header) so that the new "COLOR_HOTLIGHT
" is used.Note: If you specify
CTCS_SCROLL
orCTCS_CLOSEBUTTON
, those buttons are always hot tracked regardless of this style.CTCS_FLATEDGE
- Tab controls derived fromCCustomTabCtrl
can use this style to determine whether to draw the outline of the control with a flat look.CTCS_DRAGREARRANGE
- If you set this style, a tab item can be dragged to another position within the same tab control.CTCS_BOLDSELECTEDTAB
- The selected tab's text is rendered in the bold version of the tab font.CTCS_TOOLTIPS
- Enable tooltips to be displayed. Each item's tooltip defaults to the text of the tab, but can be adjusted by callingSetToolTip
on the tab item.
Notifications
These messages are sent to the parent of the tab control window in the form of a WM_NOTIFY
message.
NM_CLICK
- Notifies a tab control's parent window when the left mouse button has been pressed. ThelParam
of the message is a pointer to aNMCTCITEM
structure, withiItem
being the index of the item which the cursor is over, or -1 if no item is under the cursor. In the handler, returnTRUE
to prevent the default processing to occur, orFALSE
to allow it. The default processing selects the item under the cursor. This notification is not sent when clicking on a scroll or close button.NM_DBLCLK
- Notifies a tab control's parent window when the left mouse button has been double clicked. ThelParam
of the message is a pointer to aNMCTCITEM
structure, withiItem
being the index of the item which the cursor is over, or -1 if no item is under the cursor. In the handler, returnTRUE
to prevent the default processing to occur, orFALSE
to allow it.NM_RCLICK
- Notifies a tab control's parent window when the right mouse button has been pressed. ThelParam
of the message is a pointer to aNMCTCITEM
structure, withiItem
being the index of the item which the cursor is over, or -1 if no item is under the cursor. In the handler, returnTRUE
to prevent the default processing to occur, orFALSE
to allow it. The default processing selects the item under the cursor.NM_RDBLCLK
- Notifies a tab control's parent window when the right mouse button has been double clicked ThelParam
of the message is a pointer to aNMCTCITEM
structure, withiItem
being the index of the item which the cursor is over, or -1 if no item is under the cursor. In the handler, returnTRUE
to prevent the default processing to occur, orFALSE
to allow it.NM_CUSTOMDRAW
- Notifies a tab control's parent window about drawing operations. ThelParam
of the message is a pointer to aNMCTCCUSTOMDRAW
structure. See MSDN for an explanation of how custom drawing works with common controls in general, in the article "Customizing a Control's Appearance". For the most part, the custom tab control is very similar (especially to custom drawing a toolbar control). The one difference is that theNMCTCCUSTOMDRAW
structure lets you set theHFONT
for the inactive and selected item, instead of selecting the item's font into the device context and returningCDRF_NEWFONT
on eachCDDS_ITEMPREPAINT
(you can still change theHFONT
on eachCDDS_ITEMPREPAINT
, or you can just set it in theCDDS_PREPAINT
notification).CTCN_FIRST
- Value of first custom tab control notification code.CTCN_LAST
- Value of last custom tab control notification code. If a derived class wants to define its own message, it can useCTCN_LAST - 1
,CTCN_LAST - 2
, etc.CTCN_SELCHANGE
- Notifies a tab control's parent window that the currently selected tab has changed. ThelParam
of the message is a pointer to aNMCTC2ITEMS
structure, withiItem1
being the old selected item, andiItem2
the new item to select.CTCN_SELCHANGING
- Notifies a tab control's parent window that the currently selected tab is about to change. ThelParam
of the message is a pointer to aNMCTC2ITEMS
structure, withiItem1
being the old selected item, andiItem2
the new item to select. The receiver of the message should returnTRUE
to prevent the selection from changing, orFALSE
to allow the selection to change.CTCN_INSERTITEM
- Notifies a tab control's parent window that an item has been inserted. ThelParam
of the message is a pointer to aNMCTCITEM
structure, withiItem
being the index of the inserted item.CTCN_DELETEITEM
- Notifies a tab control's parent window that an item is about to be deleted. ThelParam
of the message is a pointer to aNMCTCITEM
structure, withiItem
being the index of the item to be deleted. The receiver of the message should returnTRUE
to prevent the deletion, orFALSE
to allow it.CTCN_MOVEITEM
- Notifies a tab control's parent window that an item has been moved to another index by a "MoveItem
" call. ThelParam
of the message is a pointer to aNMCTC2ITEMS
structure, withiItem1
being the old index, andiItem2
the new index.CTCN_SWAPITEMPOSITIONS
- Notifies a tab control's parent window that two items have been switched in position by a "SwapItemPositions
" call. ThelParam
of the message is a pointer to aNMCTC2ITEMS
structure, withiItem1
being the index of the first item, andiItem2
the index of the second.CTCN_CLOSE
- Notifies a tab control's parent window that the "close" button has been clicked. The close button is only displayed for tab controls withCTCS_CLOSEBUTTON
set.CTCN_BEGINITEMDRAG
- Notifies a tab control's parent window that a tab item drag has started. ThelParam
of the message is a pointer to aNMCTCITEM
structure, withiItem
being the index of the item being dragged.CTCN_ACCEPTITEMDRAG
- Notifies a tab control's parent window that a tab item drag has ended and is accepted by the user. ThelParam
of the message is a pointer to aNMCTC2ITEMS
structure, withiItem1
being the index of the item in its original place, andiItem2
the new index of the item.CTCN_CANCELITEMDRAG
- Notifies a tab control's parent window that a tab item drag has ended and was cancelled by the user. ThelParam
of the message is a pointer to aNMCTCITEM
structure, withiItem
being the index of the item that was being dragged.
Structures
NMCTCITEM
-NMHDR hdr;
- Generic Notification Information.int iItem;
- Index of item involved in action, or -1 if not used.POINT pt;
- Screen coordinate of point of action.
NMCTC2ITEMS
-NMHDR hdr;
- Generic Notification Information.int iItem1;
- Index of first item involved in action.int iItem2;
- Index of second item involved in action.POINT pt;
- Screen coordinate of point of action.
CTCHITTESTINFO
-POINT pt;
- Position to hit test, in client coordinates.UINT flags;
- Variable that receives the results of a hit test. The tab control sets this member to one of the following values:CTCHT_NOWHERE
- The position is not over a tab.CTCHT_ONITEM
- The position is over a tab item.CTCHT_ONCLOSEBTN
- The position is over the close button.CTCHT_ONSCROLLRIGHTBTN
- The position is over the right scroll button.CTCHT_ONSCROLLLEFTBTN
- The position is over the left scroll button.
NMCTCCUSTOMDRAW
-NMCUSTOMDRAW nmcd;
- General custom draw structure.HFONT hFontInactive;
- Font for text of inactive tabs.HFONT hFontSelected;
- Font for text of selected tab.HBRUSH hBrushBackground;
-HBRUSH
toPatBlt
into background.COLORREF clrTextInactive;
- Color of text of inactive tab.COLORREF clrTextSelected;
- Color of test of selected tab.COLORREF clrSelectedTab;
- Color of the selected tab's background.COLORREF clrBtnFace;
- Used in drawing 3D shapes. Defaults toCOLOR_BTNFACE
.COLORREF clrBtnShadow;
- Used in drawing 3D shapes. Defaults toCOLOR_BTNSHADOW
.COLORREF clrBtnHighlight;
- Used in drawing 3D shapes. Defaults toCOLOR_BTNHIGHLIGHT
.COLORREF clrBtnText;
- Used in drawing 3D shapes. Defaults toCOLOR_BTNTEXT
.COLORREF clrHighlight;
- Used when a derived tab wants a "highlight" color.COLORREF clrHighlightHotTrack;
- Used for hot tracking.COLORREF clrHighlightText;
- Used when a derived tab wants a "highlight text" color.
CTCSETTINGS
-signed char iPadding;
- Tab item padding.signed char iMargin;
- Tab item margin.signed char iSelMargin;
- Selected tab item margin.signed char iIndent;
- Indent from left of client area to beginning of first tab (sometimes used similarly on the right of client area).CDotNetTabCtrlImpl
(base class ofCDotNetTabCtrl
andCDotNetButtonTabCtrl
) interprets margin and padding like so:M - Margin P - Padding I - Image Text - Tab Text With image: __________________________ | M | I | P | Text | P | M | -------------------------- Without image: ______________________ | M | P | Text | P | M | ----------------------
Constants
FindItem
flagsCTFI_NONE
- 0x0000CTFI_RECT
- 0x0001CTFI_IMAGE
- 0x0002CTFI_TEXT
- 0x0004CTFI_TOOLTIP
- 0x0008CTFI_TABVIEW
- 0x0010CTFI_HIGHLIGHTED
- 0x0020CTFI_CANCLOSE
- 0x0040CTFI_LAST
-CTFI_CANCLOSE
CTFI_ALL
- 0xFFFF
#define
the following constants before you #include CustomTabCtrl.h
to redefine them.
- Scroll Repeat (milliseconds).
CTCSR_NONE
- 0CTCSR_SLOW
- 100CTCSR_NORMAL
- 25CTCSR_FAST
- 10
- Drag and Drop Constants
CTCD_SCROLLZONEWIDTH
- 20 (pixels)
Methods
Note: This is one place where custom tab controls are dissimilar to common controls. Instead of sending messages to the window, you call public methods - much like working with the common control wrappers of WTL. If you want corresponding messages to these methods, then let me know.
SubclassWindow
- Call when you have an existing window, such as a static control, that you want to subclass into a custom tab control window.GetTooltips
- Get the tooltip control window (the WTL tooltip control window wrapper is returned).Get/SetImageList
- Images for tab items are kept in an image list. The WTL image list wrapper is used.Get/SetScrollDelta
- The distance in pixels that each atomic scroll operation scrolls the view. Only valid when theCTCS_SCROLL
style is set. Valid values are 0-63.Get/SetScrollRepeat
- When a scroll button is held down, the scroll repeat determines how quickly the atomic scroll operation is repeated. Only valid when theCTCS_SCROLL
style is set. A Windows timer is used to repeat the scroll. Valid values are:ectcScrollRepeat_None
,ectcScrollRepeat_Slow
,ectcScrollRepeat_Normal
, andectcScrollRepeat_Fast
.InsertItem
- Insert a new tab item. There are two versions of this method. The first allows you to pass the parameters for the tab item. The second allows you to call theCreateTabItem
method to create a new tab, set the parameters on that item, then insert the item (the tab control takes ownership of the item created withCreateTabItem
).MoveItem
- Move an existing tab item to another index, and shift the affected tab item indexes.SwapItemPositions
- Swap the positions of two tab items.DeleteItem
- Delete a tab item.DeleteAllItems
- Delete all the tab items.GetItem
- Get the pointer to the tab item class. The tab item class will either beCCustomTabItem
, a class derived fromCCustomTabItem
, or a class with the same interface asCCustomTabItem
.Get/SetCurSel
- The currently selected tab item.SetCurSel
will first send a notification that the selected item is about to change (CTCN_SELCHANGING
). If the receiver of the notification doesn't cancel the attempt, the current selection is changed, and another notification is sent to say the selection has changed (CTCN_SELCHANGE
).GetItemCount
- Returns the number of tab items.HitTest
- Determines which tab, if any, is at a specified position (in client coordinates).EnsureVisible
- Ensures that the tab item specified is in view. Only really useful whenCTCS_SCROLL
is set.GetItemRect
- Get theRECT
of an item in client device coordinates.HighlightItem
- Similar toTCM_HIGHLIGHTITEM
forSysTabControl32
and the MFC and WTL wrappersCTabCtrl::HighlightItem
.FindItem
- Find the next tab item matching the search criteria The function is meant to mimic howCListViewCtrl::FindItem
andLVM_FINDITEM
work, since there are no comparable messages or functions for a tab control.
Acknowledgements
I'd like to thank Bjarke Viksøe for his original "Cool Tab controls" and for all of the other cool things on his web site that he so generously shares with the world. I'd also like to thank all the many others who have given feedback, made suggestions, and helped this become what it is today.