Table of contents
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.
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.
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.
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 Color
s, 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 TabPage
s 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 YaTabPage
s can get added to the YaTabControl
and the YaTabPage
s always have a DockStyle
of Fill
which means that, when the runtime draws the YaTabPage
s, 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 YaTabPage
s 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 Timer
s and OpenFileDialog
s. 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:
using System;
using System.Drawing;
using System.Windows.Forms;
namespace GrayIris.Utilities.UI.Controls
{
public class LineAndBoxTabDrawer : YaTabDrawer
{
public LineAndBoxTabDrawer() {}
public override DockStyle[] SupportedTabDockStyles
{
get
{
return new DockStyle[] { DockStyle.Bottom,
DockStyle.Top, DockStyle.Left, DockStyle.Right };
}
}
public override bool SupportsTabDockStyle( DockStyle dock )
{
return dock != DockStyle.None && dock != DockStyle.Fill;
}
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:
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!
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.