Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Painting Your Own Tabs - Second Edition

4.93/5 (193 votes)
21 Sep 2010CPOL20 min read 970.5K   80.2K  
A better way to control the painting of the .NET TabControl
With the built in System.Windows.Forms.TabControl, the rendering of the tab control is okay when aligned to the top, but even then, it is not perfect. In this article, a solution to this problem has been presented. You will see how to use the code followed by a list of properties and events exposed by the control. Finally, you will also see some tips.
(Now with support for Visual Studio 2010 Style, Drag'n'Drop, Mnemonics, RightToLeftLayout, and compilable against the Mono framework.)

Introduction

Sample Image

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 native tab control

The issues are as follows:

  1. When set to TabAlignment.Top or TabAlignment.Bottom, the tab page area has an unsightly white shadow to the bottom right.
  2. 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.
  3. When set to TabAlignment.Top or TabAlignment.Bottom, the tab page area border is not XP themed.
  4. When set to TabAlignment.Left or TabAlignment.Right the tab page area border becomes 3D.
  5. When set to TabAlignment.Left or TabAlignment.Right the background becomes a solid gray, rather than transparent.
  6. When set to TabAlignment.Left or TabAlignment.Right the tabs lose any pretence of styling.
  7. When viewed on Windows XP set to TabAlignment.Left or TabAlignment.Right, the text of the tabs disappears altogether!
  8. It is impossible to turn off hot tracking.

In addition:

  1. No support for modern styles
  2. Not possible to hide the tabs
  3. No support for disabled tabs
  4. 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 has 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.

C#
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 too much flicker.

To fix the alignment rendering, I had to resort to custom painting, but as this is 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.

A better tab control

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 thank you 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 too 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 overrideable 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 useful 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 important 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.

class diagram

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

    Visual Studio 2005 Styled tab controls

  • Chrome - Imitating the Google Chrome Tabs

    Google Chrome Styled tab controls

  • IE8 - Imitating the Internet Explorer 8 Tabs

    IE 8 Styled tab controls

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

    Rounded Styled tab controls

  • Angled - Not quite like Google Chrome

    Angled Styled tab controls

  • VS2010 - Imitating the Visual Studio 2010 Tabs

    Visual Studio 2010 Styled tab controls

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 totally 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 its 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,

C#
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:

C#
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 its parent, not the current CustomTabControl. This is vital for dragging from one CustomTabControl to another. Secondly, we must insert the tab in front 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.

C#
protected override void OnDragDrop(DragEventArgs drgevent){
 base.OnDragDrop(drgevent);
 
 //	Test for a TabPage in the drag data
 if (drgevent.Data.GetDataPresent(typeof(TabPage))){
  drgevent.Effect = DragDropEffects.Move;
 
  //	Extract the TabPage from the drag data
  TabPage dragTab = (TabPage)drgevent.Data.GetData(typeof(TabPage));
 
  //	Do not drop on the place you started
  if (this.ActiveTab == dragTab){
   return;
  }
 
  //	Capture insert point and adjust for removal of tab
  //	We cannot assess this after removal as differing tab sizes will cause
  //	inaccuracies in the activeTab at insert point.
  //	We will insert just before the active tab.
  int insertPoint = this.ActiveIndex;
  if (dragTab.Parent.Equals(this) && this.TabPages.IndexOf(dragTab) < insertPoint){
   insertPoint --;
  }
  if (insertPoint < 0){
   insertPoint = 0;
  }
 
  //	Remove from current position (could be another tabcontrol)
  ((TabControl)dragTab.Parent).TabPages.Remove(dragTab);
 
  //	Add to current position
  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.

  1. 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.
  2. 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.

C#
public static IntPtr SendMessage (IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam){
 //	This Method replaces the User32 method SendMessage, but will only work for sending
 //	messages to Managed controls.
 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:

C#
public static TabStyleProvider CreateProvider(CustomTabControl tabControl){
TabStyleProvider provider;
// Depending on the display style of the tabControl generate an appropriate provider.
switch (tabControl.DisplayStyle) {
// New case for Visual Studio style
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 vertical padding right down.

C#
public TabStyleVisualStudioProvider(CustomTabControl tabControl) : base(tabControl){
this._ImageAlign = ContentAlignment.MiddleRight; this._Overlap = 7;
// Must set after the _Radius as this is used in the calculations of the actual padding
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.

C#
public override void AddTabBorder(GraphicsPath path, Rectangle tabBounds){
switch (this._TabControl.Alignment) {
case TabAlignment.Top:
// bottom left of tab up the leading slope
path.AddLine(tabBounds.X, tabBounds.Bottom, tabBounds.X + 
             tabBounds.Height - 4, tabBounds.Y + 2);
// along the top, leaving a gap that is auto completed for us
path.AddLine(tabBounds.X + tabBounds.Height, 
             tabBounds.Y, tabBounds.Right - 3, tabBounds.Y);
// round the top right corner
path.AddArc(tabBounds.Right - 6, tabBounds.Y, 6, 6, 270, 90);
// back down the right end
path.AddLine(tabBounds.Right, tabBounds.Y + 3, tabBounds.Right, tabBounds.Bottom);
// no need to complete the figure as this path joins into the border for the entire tab
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 Visual Studio 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

License

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