(Now with support for Visual Studio 2010 Style, Drag'n'Drop, Mnemonics, RightToLeftLayout, and compilable against the Mono framework)
Introduction
Some years ago I wrote a custom tab control which has gained
quite a following here[^] on Code Project. As with all
such controls it did what I needed it to do, and I haven't touched the
code in years. Recently I needed a tab control for another project so I
dug out the old code, screamed a few times at how bad my coding was
back then, re-wrote it from scratch and fixed all the bugs that had
been reported. I also added missing functionality like alignment of the
tabs, and other custom styles, because I needed them for my new
project. So, time for a new article to explain what is new. By the way,
the eye damaging background to the forms shown here are only used to
highlight the transparency issues.
I have tested this code against the following .Net frameworks:
3.0 Sp1, 3.5 Sp1 & 4.0
While the c# version will compile against the 2.0 .Net
framework the vb.Net version includes the use of inline functions to
define a find predicate. As this is not supported in VB.Net 2.0 that
line of code will have to be replaced by a foreach loop if you wish to
compile it for VB.Net 2.0.
The compiled assembly has been compiled against
.Net Framework 3.5 .
The problem
First, let me explain the problems with the built in System.Windows.Forms.TabControl.
As you can see from the picture below the rendering of the tab control
is okay when aligned to the top, but even then it is not perfect.

The issues are as follows:
- When set to
TabAlignment.Top or TabAlignment.Bottom
the tab page area has an unsightly white shadow to the bottom right.
- When set to
TabAlignment.Bottom
the tabs are not attached to the page area. The tab strip is the same
as from the top, just displayed at the bottom, whereas the tabs should
hang off the bottom of the page area.
- When set to
TabAlignment.Top or TabAlignment.Bottom
the tab page area border is not XP themed.
- When set to
TabAlignment.Left or TabAlignment.Right
the tab page area border becomes 3D.
- When set to
TabAlignment.Left or TabAlignment.Right
the background becomes a solid gray, rather than transparent.
- When set to
TabAlignment.Left or TabAlignment.Right
the tabs loose any pretense of styling.
- When viewed on Windows XP set to
TabAlignment.Left
or TabAlignment.Right the text of the tabs
disappears altogether!
- It is impossible to turn off hot tracking.
In addition:
- No support for modern styles.
- Not possible to hide the tabs.
- No support for disabled tabs.
- No drag'n'drop support.
The solution
I did make a few design changes up front this time round. The
first of these was to move the control code into the System
namespaces. I know these are usually reserved for stuff supplied by
Microsoft, however José
Manuel Menéndez Poo[^] wrote a brilliant ribbon
control and in it made the observation that having control based stuff
in the System namespaces is a big advantage
when you are coding. No special imports, swap the classes with the
underlying .Net classes with ease, and so on. The second was to place
all native code interaction in a separate class, just as Microsoft have
done inside the .Net Framework. I also decided to try passing the FXCop[^] test. Most projects I have
collaborated on would take years to make them pass FXCop[^] tests but I did this one from
the start, so the code is theoretically better for it.
Transparency
First priority was to sort out the transparency issue.
In the original version of this control I did some clever
coding to paint the control underneath the tab control to replicate
transparency. There are several better ways to achieve transparency,
and all of them use far less code! Unfortunately, I soon discovered
that they all introduce no end of flicker, so I have ended up with a
hybrid solution, using some of the transparency code from the old
project.
protected void PaintTransparentBackground(Graphics graphics, Rectangle clipRect)
{
graphics.Clear(Color.Transparent);
if ((this.Parent != null)) {
clipRect.Offset(this.Location);
PaintEventArgs e = new PaintEventArgs(graphics, clipRect);
GraphicsState state = graphics.Save();
graphics.SmoothingMode = SmoothingMode.HighSpeed;
try {
graphics.TranslateTransform((float)-this.Location.X, (float)-this.Location.Y);
this.InvokePaintBackground(this.Parent, e);
this.InvokePaint(this.Parent, e);
}
finally {
graphics.Restore(state);
clipRect.Offset(-this.Location.X, -this.Location.Y);
}
}
}
This fills the area with a transparent background, then if the
parent object is available we offset the paint origin and call the
parent to paint the area under the tabs.
The normal tricks of ignoring the WM_ERASEBKGRND message,
changing the createParams, or setting the Region all fail because they
introduce far to much flicker.
To fix the alignment rendering I had to resort to custom
painting, but as this the whole point of the article I shall deal with
it in detail later. For now, here is how the tabs look in the default
style with a transparent background, and the painting corrected for all
alignments.

Custom painting
Clearly the custom painting of the tabs is the main point of
this article. As I explained in my previous article on this subject The
.Net framework SDK explains how you can set the TabControl
Drawmode to OwnerDrawFixed to
paint the tabs yourself; however, the tabs do not resize for longer
captions, and there is a really annoying border painted on each tab
that you just can't get rid of. I found that most people must have
tried the .NET SDK way of painting the tabs and found it didn't work.
They then proceeded to write their own tab controls, with all the
problems of getting the design time experience just right, and so on.
Although it took me a while to stumble across this solution, I think it
is much cleaner (but then, I am biased in that respect). I retained the
design time experience and the functionality of the underling .NET
control, and I made it paint just the way I wanted. I found that you
can set the control style to UserPaint and do
it all yourself. This keeps the auto sizing tabs, and all the tab page
functionality remains intact.
Before I get stuck in to the implementation details, I should
mention that many improvements found their way into the code thanks to
posters on the original article.
- Thanks to Bloggins[^] for pointing out that you need
to paint the entire tab strip every time to make overlapping tabs work.
- Thanks to martin.riepl[^] who suggested a font sizing
fix.
- Thanks to Tasosval[^] for the suggested ImageClick
event.
- And a big thankyou to Mick
Doherty[^] whose work on the
TabControl
provided many tips[^]
that I have integrated into this solution.
To enhance the rendering I played around with double buffering
using the control styles, the .Net 2.0 BufferedGraphics
class and all sorts, but the only buffering that worked was painting
into an in memory bitmap then pushing that to the screen in one hit.
Anything else resulted in a loss of the transparent background, or
excessive flicker.
The original version of this control painted the background,
each tab, the tab page border, and finally repainted the selected tab
to place it on top. It also had special code to paint the first tab
differently and to only paint the overlap for the selected tab. As this
new version supports any level of overlap, or none at all I decided
that all tabs must be treated equally. Therefore this version paints
each tab except the selected one, going right to left, and finished
with the selected tab. This means that all tabs cap paint their
overlapping parts and be happy in the knowledge that the next tab will
cover up the bits that should be hidden away. Painting a tab therefore
includes the tab page, complete border, tab background, text and image
(if required). The tab page with its border, along with the text,
image, and tab background colouring are standard throughout the styles,
so the only part to customise for a new style is the path describing
the border of the tab for each orientation.
Note that if the tab page is set to Enabled =
false then the tab is painted greyed out and is not
selectable. However this is a runtime check so if the initial tab is
disabled it will still appear selected on starting your application.
The standard method on the TabControl to
get the tab rectangle is to small for our purposes. We take this
rectangle and expand it to include the border edge of the tab page. The
first tab must then be moved a couple of pixels as it defaults to start
at the edge of the control not at the border of the tab page. We then
reduce the size of non-selected tabs, and stretch all except for the
first tab to allow for any overlap. This complete tab rectangle is then
passed to an overideable method to generate the appropriate shape for
the tab.
A further problem I uncovered is how to paint the tabs
correctly when in multiline mode. The problems are twofold. Firstly,
you need to paint the tabs on the outer rows before you paint the inner
rows or they will vanish when over-painted. Secondly, it would be nice
to get all rows to line up correctly on the left hand side.
Help is at hand with a new method GetTabRow.
This enables us to paint the tabs row by row. An additional check for
being the left most tab in the row is easily implemented, though I have
included a method to get the row/column of the tab in the multirow tab
array.
Another usefull addition is the ability to make hide
individual tabs using HideTab and ShowTab.
While not strictly speaking part of the painting process they
none-the-less effect the display. Most importantly is that unless used
they incur no overhead as the backup copy of the tab references is only
initialised on hiding a tab for the first time. Care must also be taken
to restore tabs in their original positions relative to the currently
visible tabs.
A more versatile and extendable styling solution
As I developed this control further there was a growing need
for more flexibility, meaning more properties, and styles. Clearly
putting all this into the one class was going to become unmanageable so
I extracted a large part of the Tab painting code into another class.
This TabStyleProvider class acts as the base class for any new style,
and has a factory method for getting instances of style providers. The
TabControl now has only the DisplayStyle property, which changes the
provider. Properties on the provider can then be customised further in
the designer as required.

Here are a few examples of the kinds of tab styles you can
achieve.
The currently supported styles via the DisplayStyle
property are:
- None - No tabs visible
- Default - Identical to the .Net default
rendering on Vista
- VisualStudio - Imitating the Visual Studio 2005
tabs

- Chrome - Imitating the Google Chrome Tabs

- IE8 - Imitating the Internet Explorer 8 Tabs

- Rounded - My personal favourite, which looks
good aligned left

- Angled - Not quite like Google Chrome

- VS2010 - Imitating the Visual Studio 2010 Tabs

Using the Code
To use this code just copy the contents of the TabControl
folder, and sub folders into your project and use it. For your
convenience I have included code in C# and in VB.Net, although the
VB.Net version does not have the complete demo code, just the complete
control code. The files for the VB.Net version are the same names as
for C# just with .vb at the end.
If you prefer to use Managed C++ I used a tool to convert an
early version of this control into a Managed C++ project for Visual
Studio 2005. I have tested it works be referencing , and using the
resulting dll, but as my knowledge of C++ is limited I won't pretend I
can support it in any way, and I won't be updating that one with
changes. But it should be enough to get things started for you.
Alternatively, download the compiled
assembly[^] and just add a reference to it,
or add it to your toolbox.
Special note for VB.Net developers. As T_uRRiCA_N[^] noted, VB.Net and C# are
different in how they use the project properties, specifically the Root
Namespace value in the Project Properties. In C#
projects this property indicates the namespace that will be
automatically added to the source code of any class you create in the
project after setting the root namespace value. In VB.Net the same
property is not used at design time, but at compile time. The VB.Net
compiler prepends this namespace to every class and resource in the
assembly. So then class, System.Windows.Forms.CustomTabControl
in C# could compile as MyApplication.System.Windows.Forms.CustomTabControl
in VB.Net. There are two ways round this issue. The first is to not set
the root namespace for VB.Net projects. This is my preferred option as
I want to control my namespaces, not be at the whim of the compiler.
The second option is to place your code in a custom namespace and
ensure you add the appropriate Imports to
each file.
Properties and Events exposed by the control
Some properties, such as HotTrack and Padding I have moved to
the TabStyle provider classes. Others, such as the Appearance
property I have totaly hidden as it is no use to use here. The
properties exposed by the TabStyleProvider
include:
BorderColor - controls the border
colour of non-selected tabs.
BorderColorHot -
controls the border colour of tabs under the mouse when HotTracking is
on.
BorderColorSelected - controls the
border colour of selected tabs. Defaults to an XP Themed border colour
matching the standard textbox border.
CloserColor - controls the colour
of the closer cross if displayed on tabs.
CloserColorActive - controls the
colour of closer cross when the mouse if over the closer area on tabs.
FocusColor - controls the colour
of the focus indicator if required.
FocusTrack - controls whether tabs
display a focus indicator when the tab control has focus.
TextColor -
controls the colour of the text displayed on tabs.
TextColorDisabled -
controls the colour of the text displayed on disabled tabs.
TextColorSelected -
controls the colour of the text displayed on selected
tabs.;
HotTrack - controls whether tabs
change colour when the mouse is over them.
ImageAlign - controls the position
of any image displayed on the tabs.
Opacity - controls the opacity of
the entire tab control.
Overlap - controls how far the
tabs extend to the left (or top) covering the previous tab.
Padding - controls the spacing
around the text, giving extra height or width to the tabs.
Radius - controls the curvature of
rounded tabs, or the spread of angled tabs.
ShowTabCloser - controls whether
tabs display a cross to close the tab.
I have also added a couple of new events and properties to the
tabcontrol itself.
ActiveIndex - returns the index of
the TabPage related to the tab currently
under the mouse, or -1 if the tab is disabled or
the mouse is not near a tab.
ActiveTab - returns the TabPage
related to the tab currently under the mouse, or null
if the tab is disabled or the mouse is not near a tab.
HScroll - fired when the tab
scroller is clicked.
TabImageClick - fired when an
image on a tab is clicked.
TabClosing - fired when the closer
on a tab is clicked. This event can be cancelled.
And few new methods are also exposed.
GetTabPosition - returns the row
and column of the tab within the multi row tab array as a Point.
GetTabRow - returns the row of the
tab within the multi row tab array.
isFirstTabInRow - returns true if
the specified tab index is the first tab in it's row.
HideTab - Removes the tab
specified by reference, key or index from the visible tabs
ShowTab - Restores the tab
specified by reference, key or index to the visible tabs, or adds the
tab if it is not present.
Tips
Padding
In order to gain space for the tab closer, and allow for the curvature
of the tabs I have had to adjust the actual
Padding
of the tabs. The apparent
Padding is from
before this adjustment. In some cases you may find that the text does
not wrap in the correct place, in which case adjust the
Padding
appropriately.
Context Menus
I have not customised the tab scroller, in this version.
However you could use the workaround from Mick
Doherty[^] for adding navigation buttons
at Add a Custom Scroller to TabControl[^].
For tab closing I personally like to add a context menu to the
tab control, though I have now implemented tab closer functionality. In
the context menu Opening event you add the following code to stop it
opening for disabled tabs, and select the active tab on right mouse
actions. Your should always set the selected tab to be the active tab
in this scenario as by the time you click the close option your mouse
will have moved away from the tab,
void ContextMenuStripOpening(object sender, CancelEventArgs e){
if (this.customTabControl1.ActiveIndex > -1){
this.customTabControl1.SelectedIndex = this.customTabControl1.ActiveIndex;
} else {
e.Cancel = true;
}
}
I would then have a 'Close' option on the context menu and in
the Click event place the following code:
void CloseToolStripMenuItemClick(object sender, EventArgs e){
TabPage pageToRemove = this.customTabControl1.SelectedTab;
if (pageToRemove != null){
this.customTabControl1.TabPages.Remove(pageToRemove);
pageToRemove.Dispose();
}
}
Implementing Drag 'n' Drop
The default TabControl does not
support drag 'n' drop. I had implemented it in a separate application,
so I have included it here also. To turn it on just set AllowDrop
to true on any instance of the CustomTabControl
that you want to drag 'n' drop. You can then
drag tabs around on the control, or drag them from one CustomTabControl to another.
The basics of drag 'n' drop are the same for all controls.
Start the drag on mouse down, enable drag effects when the mouse is
over areas that can accept the drop, and handle the drop to move stuff
around. You can find a good example of it on the Microsoft
support
site[^].
Of interest however is how to move the tabs around.
As you can see from the example code below we face two
problems. Firstly we must ensure we remove the tab from it's parent,
not the current CustomTabControl. This is vital for dragging from
one CustomTabControl to another. Secondly we must insert the
tab infront of the tab currently under the mouse, remembering that if
the tab came from a point to the left then removing it will change the
positions of all the tabs.
protected override void OnDragDrop(DragEventArgs drgevent){
base.OnDragDrop(drgevent);
if (drgevent.Data.GetDataPresent(typeof(TabPage))){
drgevent.Effect = DragDropEffects.Move;
TabPage dragTab = (TabPage)drgevent.Data.GetData(typeof(TabPage));
if (this.ActiveTab == dragTab){
return;
}
int insertPoint = this.ActiveIndex;
if (dragTab.Parent.Equals(this) && this.TabPages.IndexOf(dragTab) < insertPoint){
insertPoint --;
}
if (insertPoint < 0){
insertPoint = 0;
}
((TabControl)dragTab.Parent).TabPages.Remove(dragTab);
this.TabPages.Insert(insertPoint, dragTab);
this.SelectedTab = dragTab;
}
}
Mono Support
The downloads posted here are all compiled against .Net Framework 3.5, however I have tested a separate build against Mono 2.7.
The Mono implementation of the System.Windows.Forms namespace is not a perfect replica of the Microsoft implementation. When compiling against Mono you will discover the following differences in operation of this control.
- The Mono implementation of the
TabControl does not support HotTracking so it is best to set HotTrack to false or you will get some odd effects.
- Because of the lack of Hottracking you will find that Drag'n'Drop does not work either.
If anyone is working with Mono and finds fixes for these issues that are compatible with the standard implementation please let me know and I will integrate them into the main source here.
Even so, the look of the standard Mono implementation is terrible, making this control a massive improvement, even in the default style!
Having stated the limitations, there was only one change I had to make in order to make this control Mono portable. I had to remove all P/Invoke calls. to do this I turned the UserPaint style back on, with the associated need to handle font changes correctly. This is because my previous implementation used BeginPaint and EndPaint to perform my own painting. I also wrote my own implementation of SendMessage, which is used to get the active tab, and for font updates.
As you can see below, all we do is create the message object and invoke WndProc on the control. This ensures we are on the correct thread, and means no unmanaged code. However, this only works in our specific implementation here as the control is our control. This method cannot be used for sending messages to unmanaged controls.
public static IntPtr SendMessage (IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam){
Control control = Control.FromHandle(hWnd);
if (control == null){
return IntPtr.Zero;
}
Message message = new Message();
message.HWnd = hWnd;
message.LParam = lParam;
message.WParam = wParam;
message.Msg = msg;
MethodInfo wproc = control.GetType().GetMethod("WndProc"
, BindingFlags.NonPublic
| BindingFlags.InvokeMethod
| BindingFlags.FlattenHierarchy
| BindingFlags.IgnoreCase
| BindingFlags.Instance);
object[] args = new object[] {message};
wproc.Invoke(control, args);
return ((Message)args[0]).Result;
}
Mnemonic Support
Mnemonic support is built in to this implementation of the TabControl,
however the Mnemonic characters are only displayed, and only respond to key
presses if the KeyPreview property is set to true on the parent form.
Creating a new Style provider
As an example of how to implement a style provider, here is
how I implemented the Visual Studio Provider.
The first thing to do is subclass the TabStyleProvider,
creating the TabStyleVisualStudioProvider
class. Add a new item to the TabStyle enum,
and add an appropriate case to the CreateProvider
factory method in the TabStyleProvider class
as shown below.
public static TabStyleProvider CreateProvider(CustomTabControl tabControl){
TabStyleProvider provider;
switch (tabControl.DisplayStyle) {
case TabStyle.VisualStudio:
provider = new TabStyleVisualStudioProvider(tabControl);
break;
case TabStyle.None:
provider = new TabStyleNoneProvider(tabControl);
We need to set a few defaults in the constructor of our new
provider. Images should align right, careful measurement shows that the
tabs overlap by seven pixels, and we need to insert some padding to
allow for the slope of the leading edge. Of course these tabs are also
not as tall as the standard tabs, so we drop the virtical padding right
down.
public TabStyleVisualStudioProvider(CustomTabControl tabControl) : base(tabControl){
this._ImageAlign = ContentAlignment.MiddleRight; this._Overlap = 7;
this.Padding = new Point(11, 1);
}
Finally we override the AddTabBorder
method in our new provider class. This supplies the shape for the tabs.
Visual Studio 2005 tabs have a leading edge that slopes at 45 degrees
before curving into the top line of the tab. There is also a roundness
to all the corners.
public override void AddTabBorder(GraphicsPath path, Rectangle tabBounds){
switch (this._TabControl.Alignment) {
case TabAlignment.Top:
path.AddLine(tabBounds.X, tabBounds.Bottom, tabBounds.X + tabBounds.Height - 4, tabBounds.Y + 2);
path.AddLine(tabBounds.X + tabBounds.Height, tabBounds.Y, tabBounds.Right - 3, tabBounds.Y);
path.AddArc(tabBounds.Right - 6, tabBounds.Y, 6, 6, 270, 90);
path.AddLine(tabBounds.Right, tabBounds.Y + 3, tabBounds.Right, tabBounds.Bottom);
break;
I have only included the code for top aligned tabs, but as you
can see from the other providers included you need to supply code for
each alignment. It is simplest if you imagine drawing round clockwise.
Check out one of the existing providers for an example.
As you can see adding a new style is simple, the main
dificulty is getting the outline correct for all orientations. As an
additional complication some styles, such as the IE8 style include
overriding methods to paint the tabs in different colours, and custom
painting of the closer button.
History
- 21/9/2010 - Fixed bugs in resizing code
- - Corrected direction of Images and default alignment of Images and Closers for RightToLeftLayout tabs.
- 14/9/2010 -
RightToLeftLayout, RightToLeft and Mnemonic support added.
- - Improved painting for better Mono compatability.
- 11/9/2010 - Removed calls to P/Invoke functions in order to be portable to the Mono Framework.
- 10/9/2010 - Drag'n'Drop support added.
- 8/9/2010 - Release of new style, VS2010.
- - Updated VisualStudio style to correct the colouring of
the closer.
- - New properties added to control the text colour, and
border colour.
- - Improved glass effect.
- 7/9/2010 - Fixed bug affecting the TabControl size when
anchored inside an MDI child form that has been maximized prior to
calling Show.
- 2/9/2010 - Fixed bug where resizing the control left an
artefact at the bottom.
- - Fixed bug where anchored controls on a
TabPage
would move on re-opening the designer.
- - Refactored the
CustomTabControl
and associated code into a separate assembly.
- - New methods added,
HideTab and ShowTab.
- 23/8/2010 - Large scale refactoring to a style provider
model, and addition of tab closing capability.
- 23/7/2010 - New properties,
FocusColor
and FocusTrack to better indicate focus.
- - Improved positioning of the images.
- 22/7/2010 - Clipped the sided so that tabs do not paint
beyond the edges of the tabPage.
- - In Default style the selected tab is now bigger than the
rest.
- - as in the underlying .Net control.
- 21/7/2010 - Two new properties,
BorderColor
and SelectedBorderColor
- - Update of rendering code to reduce flicker.
- 20/7/2010 - Update of article regarding the root namespace
project property.
- 8/7/2010 - Release of new CHROME style.
- - Update to the handling of the painting to reduce flicker
when changing tabs.
- 7/7/2010 - Order of painting corrected for
MultiLine
TabControls.
- - The overlap was running the wrong way.
- 6/7/2010 -
MultiLine TabControls
now paint correctly in all alignments.
- - Method
GetTabPosition added.
- - Method
GetTabRow added for more
efficient painting.
- 5/7/2010 - Hot tracking implemented (when
HotTrack is set to true).
- -
TabImageClick event
added.
- 5/7/2010 - Source for C++ version (dll containing the
CustomTabControl only) added.
- 2/7/2010 - First release on CodeProject.
Unfortunately my real name was already in use as a code project login. For those of you who are wondering I am really Napoleon Solo. Sorry, I mean, Mark Jackson. Well, I do look a bit like him I think.