Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Y(et)A(nother)TabControl: A Custom Tab Control With Owner Drawn Tabs

0.00/5 (No votes)
6 May 2013 1  
A foray into the world of creating composite custom controls with design-time support.

Table of contents  

What are you thinking, Curtis?

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. 

The legal stuff 

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.

Features of the YaTabControl

I designed the YaTabControl to have the following features:

  • Fully-customizable and easy-to-use owner-drawn tabs;
  • Fully exposed coloring;
  • Hideable scroll buttons;
  • Tabs docked to any of the four cardinal directions;
  • A cancelable TabChanging event;
  • Design-time support; and,
  • ImageList support.
  • Three supplied tab drawing classes:
    • VsTabDrawer: Draws tabs like the ones in Visual Studio.
    • XlTabDrawer: Draws tabs like the ones in Microsoft Excel.
    • OvalTabDrawer: Draws the active tab as an oval.

Essentially, I wanted a nicer version of the System.Windows.Forms.TabControl with more functionality.

The challenges

I had three major challenges in writing this control:

  • Providing a simple-to-use capability for customizing the tabs;
  • Getting the controls to stay where they should in the tab pages; and,
  • Creating the designer to provide interactivity in the design-time environment.

Drawing the tabs easily

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:

public abstract void DrawTab( Color foreColor,
                              Color backColor,
                              Color highlightColor,
                              Color shadowColor,
                              Color borderColor,
                              bool active,
                              bool mouseOver,
                              DockStyle dock,
                              Graphics graphics,
                              SizeF tabSize );

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.

Getting controls to stay where they belong

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 true Visible 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.

Design-time support

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.

Writing your own tab drawer

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:

using System;
using System.Drawing;
using System.Windows.Forms;

namespace GrayIris.Utilities.UI.Controls
{
  /// <summary>
  /// Draws a pretty tab and a pretty line.
  /// </summary>
  public class LineAndBoxTabDrawer : YaTabDrawer
  {
    /// <summary>
    /// Creates a new instance of the <see cref="LineAndBoxTabDrawer"/>
    /// class.
    /// </summary>
    public LineAndBoxTabDrawer() {}

    /// <summary>
    /// Overridden. Inherited from <see cref="YaTabDrawer"/>.
    /// </summary>
    public override DockStyle[] SupportedTabDockStyles
    {
      get
      {
        return new DockStyle[] { DockStyle.Bottom, 
              DockStyle.Top, DockStyle.Left, DockStyle.Right };
      }
    }

    /// <summary>
    /// Overridden. Inherited from <see cref="YaTabDrawer"/>.
    /// </summary>
    /// <param name="dock">
    /// See <see cref="YaTabDrawer.SupportsTabDockStyle( DockStyle )"/>.
    /// </param>
    /// <returns>
    /// Returns <b>true</b> for any <see cref="DockStyle"/> in one
    /// of the four cardinal directions.
    /// </returns>
    public override bool SupportsTabDockStyle( DockStyle dock )
    {
      return dock != DockStyle.None && dock != DockStyle.Fill;
    }

    /// <summary>
    /// Overridden. Inherited from <see cref="YaTabDrawer"/>.
    /// </summary>
    public override bool UsesHighlghts
    {
      get
      {
        return false;
      }
    }
  }
}

Of course, it won't compile. I haven't implemented the drawing portion. So, in the previous class, I write the following method:

    /// <summary>
    /// Overridden. Inherited from <see cref="YaTabDrawer"/>.
    /// </summary>
    /// <param name="foreColor">See <see cref="YaTabDrawer"/>.</param>
    /// <param name="backColor">See <see cref="YaTabDrawer"/>.</param>
    /// <param name="highlightColor">See <see cref="YaTabDrawer"/>.</param>
    /// <param name="shadowColor">See <see cref="YaTabDrawer"/>.</param>
    /// <param name="borderColor">See <see cref="YaTabDrawer"/>.</param>
    /// <param name="active">See <see cref="YaTabDrawer"/>.</param>
    /// <param name="mouseOver">See <see cref="YaTabDrawer"/>.</param>
    /// <param name="dock">See <see cref="YaTabDrawer"/>.</param>
    /// <param name="graphics">See <see cref="YaTabDrawer"/>.</param>
    /// <param name="tabSize">See <see cref="YaTabDrawer"/>.</param>
    public override void DrawTab( Color foreColor,
                                  Color backColor,
                                  Color highlightColor,
                                  Color shadowColor,
                                  Color borderColor,
                                  bool active,
                                  bool mouseOver,
                                  DockStyle dock,
                                  Graphics graphics,
                                  SizeF tabSize )
    {
      if( active )
      {
        Pen p = new Pen( borderColor );
        graphics.DrawRectangle( p, 0, 0, tabSize.Width, tabSize.Height );
        p.Dispose();
      }
      else
      {
        Brush b = Brushes.Peru;
        float dif = tabSize.Height / 4.0f;
        RectangleF r = new RectangleF( 0.0f, dif, 
             tabSize.Width, tabSize.Height - dif - dif );
        graphics.FillRectangle( b, r );
      }
    }

So, with 16 net lines of code, I have implemented a custom tab drawing class for the YaTabControl. The above code provides tabs as in the following screenshot:

Obviously, we could do a lot of cool things with this tab drawer. We could expose the color of the band, the percentage of the available height to use, and more. You can knock yourself out, kid!

Dropping it in

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!

History

  • 17 Feb 2005 - Source code updated.
  • 6 Feb 2006 - Fixed a "leak" with undisposed Pen with patch from bschurter.
  • 14 Dec 2010 - Fixed SelectedIndex with patch from Ondra.
  • 25 Apr 2013 - Code moved to GitHub and mouseover code added.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here