Second Edition of this Control Now Available
I have now released a much improved version of this control as Painting Your Own Tabs - the second edition is available here which includes new styling, a re-worked painting metaphor, and a totally different approach to transparency. I did not want to replace this article and code as I know many people are using it, however I do recommend you update to the new version if you can. It is much better.
Introduction
A project I have been working on required a tabbed user interface, and for some time, we had been using the excellent tab control from Infragistics. However, I wanted to use the native .NET TabControl
, for such a core part of the system where the speed was vital. It doesn't seem like much, but the Infragistics controls are custom drawn from the ground up, and that does make a difference. The more you can use the native control set, the faster your application will run.
When VS.NET 2005 was released, I was hoping that the TabControl
would support the style used in the VS IDE, as that was the style we wanted to use. Unfortunately, this was not the case. Furthermore, I had noted that the native TabControl
and the Infragistics tab control both had a grey border around the tab page section, as you can see from the image above. I really wanted to be able to control this colour. Having searched the web, I found that everyone was using the Infragistics technique, including some good articles here on CodeProject. However, replicating a control created by Microsoft, available out of the box, when I just wanted to tweak it, did not sound like fun to me!
So, having decided to subclass the native TabControl
, I had just two problems to solve. Firstly, how to paint the tabs themselves to make them look like the VS.NET IDE, without losing any of the auto sizing functionality. Secondly, how to get rid of that annoying grey background!
There was a third part to this, I had to add the tab closer and the tab navigation buttons to the top right corner of the control. I did that myself, but the solution is a bit flaky so I have left that out of this article.
Using the Code
The solution I came up with is shown in my demo application, and consists of the sub classed TabControl
itself, and a themed colour provider. I used an updated version of the themed colour provider I wrote for an earlier article on theming the CheckBox
control.
To use the control in your own applications, just copy the two classes into your code.
I used SharpDevelop 2.0 to create the solution, and it works fine from VS.NET 2005 as well. I have included both a C# version and a VB.NET version in the solution, though the article code snippets are from the VB.NET version. It should be possible to do the same thing in .NET 1.0 or 1.1, but I am working exclusively in .NET 2.0 now.
So, how did I solve the problems?
The 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.
So, 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. I also set the height of the tabs to 15 pixels, to match the size of the tabs in the VS IDE. Setting the width to zero makes the tabs auto-size to fit long captions. I also added a property to switch between using the native painting and my custom painting.
Me.SetStyle(ControlStyles.UserPaint, True)
Me.ItemSize = New Size(0, 15)
The simplest solution to getting rid of the grey border turned out to be to set the control to have a transparent background and set the colour of the containing control. Unfortunately, invoking the Paint
event of the parent control tends to crash in the designer, so I set it to a nice gradient at design time. First, you enable transparency support:
Me.SetStyle(ControlStyles.SupportsTransparentBackColor, True)
Then, you override the OnPaintBackground
method to invoke the parent control's Paint
method:
Protected Overrides Sub OnPaintBackground( _
ByVal pevent As System.Windows.Forms.PaintEventArgs)
If Me.DesignMode Then
Dim backBrush As New LinearGradientBrush( _
Me.Bounds, _
SystemColors.ControlLightLight, _
SystemColors.ControlLight, _
Drawing2D.LinearGradientMode.Vertical)
pevent.Graphics.FillRectangle(backBrush, Me.Bounds)
backBrush.Dispose()
Else
Me.InvokePaintBackground(Me.Parent, pevent)
Me.InvokePaint(Me.Parent, pevent)
End If
End Sub
Painting the tabs is a little trickier. Most of the time, only the currently selected tab and the previously selected tab repaint. The clip rectangle of the PaintEventArgs
restricts the area that is being asked to paint itself. However, you have to supply code to paint all the tabs, and paint the currently selected tab differently within that. Any painting code that paints outside of the requested clip rectangle just doesn't do anything.
I have broken down the tab painting into a number of stages. See my code for details.
- For each tab, paint the background, border, and text. (The selected tab gets a slightly different look.)
- Paint a themed border over the top of the provided tab page border. (I know this doesn't sound important, but the default border is not themed.)
- Repaint the bit of the selected tab that the border just drew over.
Points of Interest
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, along with the speed gain to the application due to using the native control. And, I made it paint just the way I wanted.
The only part I failed to fully replicate from the VS 2005 IDE was that their selected tab seems to slightly overlap the tab to the left. Try as I might, I could not get that to work as the Paint
event does not fire on that tab. I could paint the overlap fine, but it would not go away when I selected a different tab! However, my tabs were good enough for our purposes.
Also, I mentioned that I had a way to add the tab close and navigation buttons. You can paint stuff up there, but it won't respond to mouse events, at least not without writing your own mouse handling code! So, my solution was to make the custom tab control dynamically add these buttons to the containing control and manually position them over the top of the tab control at run time. Unfortunately, the docking and anchoring in .NET 2.0 doesn't work properly, or at least not in the way it did in 1.1, and so my nicely not docked, or anchored buttons would keep stretching and moving in response to events from the .NET docking manager! In the very controlled environment I created for our project, this was not a problem; however it's not the sort of flaky solution you want to publish.
History
- 22/6/2006 - An update to this control. I have updated the VB code (C# will follow when I have time) to support the following new features:
- The tabs now overlap properly on the left of the selected tab (thanks Bloggins).
- The selected tab now paints its text in bold, just like the VS.NET tabs.
- Images on the tabs are now supported.
- If you set a tabpage to be
Enabled = False
, then the tab is not displayed with grey text and cannot be selected. Watch out, because, if you set the disabled tab to be the initial tab in the Forms Designer, then it will fail at run time. Also note that, TabPage.Enabled
is a hidden property and can only be set in code (intellisense will never show it). - The background now paints better in design mode.
- 19/2/2009 - An update to this control. I have updated the C# code to match the VB code. I have also implemented the font sizing fix suggested by martin.riepl.
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.