Yep, I've written yet another TabControl-like control. I had a transparent motive for doing so: user-drawn tabs. The System.Windows.Forms.TabControl in the 1.1 Framework has a DrawMode property that allows the user to draw the controls; however, the tabs get drawn with a fixed size and I didn't like that. Moreover, I wanted to plug in different drawing capabilities based on the style that I needed.

Oh, and I thought it would be cool to roll my own control. I hope that you can learn from this code and have fun with it.

I have released the GrayIris.Utilities.dll under the Modified BSD License which I reproduce here for your viewing pleasure:

Copyright (c) 2005, Gray Iris Software LC
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
Neither the name of Gray Iris Software LC nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Because I wanted the YaTabControl to have the ability to display the tabs docked to the four cardinal directions, I needed a way to provide an easy-to-use mechanism for drawing the tabs. After giving it some careful thought, I decided that, regardless of the orientation of the tabs, the mechanism that actually draws the tabs should start drawing at (0,0) and extend the length of the tab.

Accomplishing this required a two-step procedure. First, I created an abstract base class with, among other things, the following method:

This method would get called to actually draw the tab given the Colors, its active status, the Graphics on which to draw, and the tabSize. The implementation would start drawing at (0,0) and extend within the given SizeF (or not, if overlap is desired).

To provide that ease-of-use, the YaTabControl overrides its OnPaint method. The Graphics supplied to the method gets passed to the specified tab drawing object; however, some magic happens first to provide the tab-ignorant drawing. The class first applies a RotateTransform so that the tabs get drawn in the correct orientation. Then, when the tab drawer gets called to draw the nth tab, a TranslateTransform gets applied to the Graphics object so that the coordinate (0,0) exists at the upper-left corner of where the tab should get drawn.

Because of this design, the tab drawing class need not know the direction the tab needs to face, does not need to translate coordinates on its own, and does not need to handle any of the other messy details needed to draw on the Graphics' surface.

You may note, though, that a DockStyle gets passed into the method. This exists so that if the tab drawing method would like to draw highlights and shadows, it knows which direction the tab faces. This information gets used in the VsTabDrawer class included with the source. The tabs in Visual Studio have highlights and shadows. Therefore, the drawing class needs to know on which side the tabs have docked to draw the appropriate shadows/highlights.

My next problem revolved around the placement of controls. I investigated how the TabControl acted when in use at runtime. The TabControl's Controls collection held only the TabPages and only one TabPage had a trueVisible property at any given time. I wanted to emulate this functionality, as well as provide a margin around the area in which the tab pages would get drawn.

After performing many trials (and getting many errors), I hit upon a solution that worked very well for me. I will detail it for you in the following list.

Created the YaTabControl.ControlCollection inner class inherited from Control.ControlCollection. Overrode the following methods from the base class:

Constructor: Throws an ArgumentException if the owning control does not have YaTabControl in its inheritance chain.

Add( Control ): Throws an ArgumentException if the Control getting added to the collection does not have YaTabPage in its inheritance chain.

Overrode the YaTabControl.CreateControlsInstance() method to return a new YaTabControl.ControlCollection.

Overrode the YaTabControl.DisplayRectangle property to return a Rectangle that described the area in which the YaTabPage should get drawn.

Created the YaTabPage.ControlCollection inner class inherited from Control.ControlCollection. Overrode the following methods from the base class:

Constructor: Throws an ArgumentException if the owning control does not have YaTabPage in its inheritance chain.

Add( Control ): Throws an ArgumentException if the Control getting added to the collection does have YaTabPage in its inheritance chain.

Overrode the YaTabPage.CreateControlsInstance() method to return a new YaTabPage.ControlCollection.

Overrode the YaTabPage.DockStyle property to ignore all attempts to set the DockStyle. In doing so, the YaTabPage always has a DockStyle of Fill.

With those implementations in place, only YaTabPages can get added to the YaTabControl and the YaTabPages always have a DockStyle of Fill which means that, when the runtime draws the YaTabPages, they fill the Rectangle returned by YaTabControl.DisplayRectangle.

Once I got the YaTabControl working the way I wanted it to work in the run-time environment, I fired up the Forms Designer View, dropped a YaTabControl onto a form, and found that nothing worked. Nothing. I couldn't add YaTabPages to it. I could not change tabs once I did add them. I could not scroll the tabs. I could do nothing! I growled, cursed, drank some coffee, and went to Google to find some help.

Getting the YaTabControl to work

I should have just stayed on CodeProject. In the section "Design-time integration" of A designable PropertyTree for VS.NET by Russell Morris, I found all the information I needed. If you have developed a control and want design-time support, I recommend reading that section of his article, as well as the articles to which he links.

Rather than rehash good code, I point you to the details in the implementation of the GrayIris.Utilities.Controls.Design.YaTabControlDesigner class. I will write about the most difficult portion: getting the mouse clicks in the design-time environment to change tabs. I overrode the ControlDesigner.WndProc( Message ) method to intercept mouse clicks and, if they exist in the correct places, to perform the appropriate methods on the underlying YaTabControl.

Getting the YaTabDrawer to work

I initially designed the YaTabControl to use a class that implemented the IYaTabDrawer interface to draw the tabs. However, that just doesn't work in the design-time environment the way I wanted it to work. So, I changed the IYaTabDrawer interface to an abstract base class YaTabDrawer that inherits from Component. Now, classes that support drawing tabs can get dropped into the design-time environment just like Timers and OpenFileDialogs. You can see all this in the screenshot included in the Dropping It In section of this article.

I don't think that an article on CodeProject would feel complete without just a little bit of code. Since I expect the developers that use this control will want to implement their own tab drawing classes, I thought I would give an example of that process.

The following list describes the questions that you should answer before you continue with the implementation:

Will your tabs use highlights and shadows when drawing the tab?

Should your tabs get docked to all sides, or just a specific subset of Left, Right, Top, and Bottom?

What shape are your tabs?

After you answer those questions, you are about half-way there.

As an example, I will develop the LineAndBoxTabDrawer class. It will make a pretty band of color across the region, and the active tab will have a box around it:

Using my checklist above, this tab drawer will not use highlights and can get docked on any four sides. So, I create a class inherited from YaTabDrawer and override the appropriate method and properties:

As I wrote about in Design-Time Support, I spent a lot of time developing the functionality to use the YaTabControl in the Form Designer environment. To use it, all you need to do is add the control into the toolbox, drag and drop it to your control, and customize away!

Thanks for this awesome control. It's helping me construct an accordion control. I have an issue where I can drag the "AccordionPage" out on to the form.

This is okay I guess but the problem is I cannot drag it back into the Accordion. I believe that this trouble is escaped in your tab control because the DockStyle of each tabpage is fill so it's not movable.

The AccordionPages are control containers like your tab page. Any suggestions?

To prevent a user from selecting a tab, subscribe to the TabChanging event on the YATabControl. Then, in the event handler, if you want to prevent the user from selecting a tab, set the Cancel property on the incoming TabChangingEventArgs to true.

It looks great, but I can't get it to work.
I'm using VS2008 and did import the dll in the toolbox bar.

I can drag the YaTabControl on the form. No problem.
I can drag the XlTabDrawer on the form. No problem.
But if I drag the YaTabPage on the form, anywhere, the page will maximize and cover everything.
Dock properties are on Fill and can not be changed.
In the tabcontrol itself I can now select the new tabpage as selectedTab, but when doing it, it will give a warning "Property value is not valid".

Hi,Curtis:
I am a hardware driver programer, but with almost a few experiences with gui stuffs, let alone the c#.net.
So it took me a lot of time to learn basic c# and its basic gui components in order to understand your amazing work (because this is the best tab stuff that i ever found). The yaTabControl was to much for me too know. But I finally got some hints on how to manage it now. This is what i have wanted for a long time. Many thanks for your kindly releasing the coding source.

Hi, i just discovered this Control, works just great. Can anyone tell me how can i modify the tab strip height? I played with it a lot but still coouldn't find a way to make the tab headers' height value higher, for example i need to use tab headers of this dimension: Size(200,60)
Thanks

There's no direct way to set the height of the tabs. However, if you download the source code from the article, on line 1262 of YaTabControl.cs, you'll find the function CalculateTabSpans() which determines the height of the tabs. You can modify that function to use a new property that you create, like

I have changed the code somewhat to support centered tabs and added a new drawer that simulates Apple Tab Views. I would like to post this code as a new article. Something along the lines of
"Y(et)A(nother)TabControl Revisited: Adding Apple Tab Views Drawer". Any objections?