Creating Custom Controls using ToolStrip

advertisement

Creating Custom Controls using ToolStrip:

Building the TabStrip control

Jessica Fosler

Microsoft Corporation

September 2005

Summary: This sample shows how to extend the ToolStrip control to build specialized controls that match the look and feel of the rest of your application. In particular it shows how to work with the renderer classes to custom paint a ToolStrip to look like a tab control, best practices for building a custom “tab” items, how create a custom layout, and z-order concepts for ToolStrip.

Finally, the article briefly walks through some ways to add designer features to the TabStrip control.

Download sample .

Introduction

This article walks through some of the key aspects of building a TabStrip control. It assumes some familiarity with the ToolStrip control, custom painting in windows forms, and layout.

Building the TabStrip Runtime

We’ll start off by building the TabStrip itself. In order to accomplish this, we’ll create the

TabStrip class which inherits from the ToolStrip class.

Changing a ToolStripButton into a Tab

Obviously we’re going to need a tab-like thing to go into our TabStrip. We’ll pick the

ToolStripButton as the base item to work with here for several reasons: we don’t need a dropdown, we want some interactivity on mouse hover, we want to re-use the Checked property and CheckOnClick in order to indicate the “selected tab”.

There are two problems we need to solve: getting the buttons to look like tabs and getting the tabs to overlap. We’ll get to the second problem in a bit.

Inheriting the ToolStripProfessionalRenderer to paint our tabs

The ToolStrip exposes its many layers of paint through the ToolStrip.Renderer. Windows Forms

2.0 provides several stock ToolStripRenderer classes – ToolStripSystemRenderer and

ToolStripProfessionalRenderer. In this case, we’re going to inherit from the

ToolStripProfessionalRenderer to achieve our painting effects. class TabStripProfessionalRenderer : ToolStripProfessionalRenderer {

}

Since we want to change how the background of the button paints to look like a tab instead of a button, we override the following method: protected override void

OnRenderButtonBackground( ToolStripItemRenderEventArgs e) {

}

Filling the background with a gradient

The first thing we’ve got to think about is filling in the background area with a special gradient to distinguish it from the background. We’ll use colors from the ColorTable property, which is an instance of the ProfessionalColorTable class. There are two advantages to this – the colors match the current color scheme of the theme and someone can replace the colors later by passing a custom instance of ProfessionalColorTable into the constructor of

TabStripProfessionalRenderer .

Color startColor = ColorTable.MenuStripGradientEnd;

Color endColor = ColorTable.MenuStripGradientBegin;

One of the difficulties here is that we don’t just want to fill a rectangular shape – a Tab is more like a trapezoid than a rectangle. In order to accomplish this, we’ll figure out the points that make the core shape of the tab. We’ll pick a set of points that make the shape, then use

FillPolygon with a LinearGradientBrush to fill in. In order to achieve a rounded effect, we’ll chop off the corner of the trapezoid.

Figure 1. Showing result of FillPolygon – black outline shows the points used to fill the area. Colors in brush swapped to Color.HotPink.

Notice that the Image in this example continues to draw. The Image painting is handled in

OnRenderItemImage, which is a foreground layer of the item painting. By overriding

OnRenderButtonBackground, we are currently only changing how the background of the tab paints.

One thing to note is that the fill points are calculated based off the height of the item. We want a perfect 45 degree angle for the tab edge so the lines we’re about to add to the edge will draw smoothly. For this reason every time we ascend a pixel in the x, we have to ascend a pixel in y.

Drawing the edges of the tab

The tab is surrounded by a series of dark and light colors to achieve definition from the background of the TabStrip. This trick is very similar to the 3D border effects used in the

“Classic” windows theme.

Figure 2. Showing the “edging” around the Tab. Just like classic buttons, the Tab uses a series of dark and light lines to achieve definition.

When we zoom in on our Tab either using Magnifier from (Start > Accessories > Accessibility >

Magnifier) or Paint, it is easy to see that the edging can be created using a series of calls to

DrawLine on the Graphics object.

When debugging painting you may need to screen capture. In order to place a copy of the desktop to the clipboard, you can press the print screen button. To copy just the active window, you can use ALT+print screen. It is also occasionally handy to swap out the color of the line you’re currently painting to something noticeable (perhaps to Color.HotPink?) so you can see which call is giving you trouble.

Making the Tabs Look Real

Once we nailed down making one Tab look like a Tab, we have to move to making a set of Tabs look like Tabs. We need to indicate which Tab is the “Selected Tab”. Additionally, Tabs usually overlap each other; we have not figured out how to achieve this yet.

Figure 3. Three tabs in a row, no overlapping.

There are two pieces to the overlap puzzle – convincing the TabStrip to place Tabs on top of one another and making the Tab that is “topmost” paint on top of the other Tabs. Before we can tackle either of these, we need to find a way to keep track of the Selected Tab.

Keeping track of the “Selected Tab”

Since Tab inherits from ToolStripButton, we can re-use the Checked property to simulate selection. By turning on Tab.CheckOnClick, we can easily manage a SelectedTab property by overriding the ItemClicked method on TabStrip. protected override void OnItemClicked( ToolStripItemClickedEventArgs e){ for ( int i = 0; i < Items.Count; i++) {

Tab currentTab = Items[i] as Tab ; if (currentTab!= null ) { if (currentTab!= e.ClickedItem) {

currentTab.Checked = false ;

}

}

}

SelectedTab = e.ClickedItem as Tab ; base .OnItemClicked(e);

}

Creating Tabs instead of ToolStripButtons on TabStrip.Items.Add(“Tab1”)

The ToolStrip API allows us to override creation of the “default” item for the ToolStrip. The

“default” item is created when someone uses one of the overloads to Items.Add() that takes a string or image instead of specific ToolStripItem. We can override CreateDefaultItem and return a Tab with the passed in arguments. protected override ToolStripItem CreateDefaultItem( string text,

Image image, EventHandler onClick) { return new Tab (text, image, onClick);

}

Implementing a Custom LayoutEngine to Overlap the Tabs

Typically you do not need to create your own LayoutEngine for a ToolStrip. FlowLayout and

TableLayout are exposed through the ToolStrip.LayoutStyle property. Individual settings for the

FlowLayout and TableLayout can be achieved by casting the ToolStrip.LayoutSettings to a

FlowLayoutSettings or a TableLayoutSettings respectively. However, in this case we need to customize our layout in order to allow for successive tabs to be located on top of one another.

Figure 4: Placing tabs on top of one another. The pink rectangle indicates the bounds of the center Tab.

In order to provide a completely custom layout, TabStrip overrides the LayoutEngine property and returns its own class: private TabStripLayoutEngine tabStripLayout; public override LayoutEngine LayoutEngine { get { if (tabStripLayout == null ) {

tabStripLayout = new TabStripLayoutEngine ( this );

} return tabStripLayout;

}

}

Where TabStripLayoutEngine is private class TabStripLayoutEngine : LayoutEngine {

}

The TabStripLayoutEngine overrides one method “Layout” to control the Size and Locations of all the items within the TabStrip. The items themselves have a property for setting the Size, but the

TabStrip itself must be used to control the Location of the item via the TabStrip .SetItemLocation method.

Since it is the responsibility of the LayoutEngine to set the Size/Location of the items, it should figure out what size the items want to be. In general, if the item’s AutoSize property is true, it should set the item’s size to the result from the item.GetPreferredSize.

For our Tabs, we want to make sure the tab stretches from edge to edge, so that if there is one tab with an image and one with text, they are all the same size and line up. In this case we will respect item.GetPreferredSize(..).Width, and stretch the Tabs to the height to the TabStrip’s interior space as represented by the TabStrip.DisplayRectangle.Height.

If you are not familiar with DisplayRectangle, it represents the area in which child controls/items should layout. The ToolStrip specifies the DisplayRectangle to be it’s ClientRectangle minus it’s

Padding minus room to display the grip.

Figure 5. Common rectangles in ToolStrip.

Figure 5 shows the common rectangles used in ToolStrip and the effects of the Padding and

Margin properties.

Rectangles

Rectangle 5: ClientRectangle of the ToolStrip. (Area to paint ToolStrip)

Rectangle 4: DisplayRectangle of the ToolStrip. (Area to layout items in ToolStrip)

Rectangle 2: Bounds of save button. (Actual area of save button)

Padding/Margin effects

Rectangle 2->3: Margin of save button. (External spacing between other items and ToolStrip)

Rectangle 1->2: Padding of save button. (Internal spacing added to an autosized item)

Here’s a simplified version of the layout: public override bool Layout( object container, LayoutEventArgs layoutEventArgs) {

// fetch the display rectangle of the TabStrip. This is typically

// the TabStrip.ClientRectangle - TabStrip.Padding

Rectangle displayRect = tabStrip.DisplayRectangle;

// the next location to place the item, start at upper left

Point nextLocation = displayRect.Location; for ( int i = 0; i < tabStrip.Items.Count; i++) {

ToolStripItem item = tabStrip.Items[i] as ToolStripItem ; if (!item.Available) { continue ;

}

// Set the item's location as specified by nextLocation

tabStrip.SetItemLocation(item, new Point (nextLocation.X, nextLocation.Y));

// if the item is AutoSized, set it to the preferred size if (item.AutoSize) {

// use the preferredSize.Width, use the

// DisplayRectangle.Height for the height

Size preferredSize =

item.GetPreferredSize(displayRect.Size);

preferredSize.Height = displayRect.Height;

item.Size = preferredSize;

}

nextLocation.X += item.Width - tabStrip.TabOverlap;

} return tabStrip.AutoSize;

}

The main problem with the simplified version is that we only want to overlap other Tabs.

Additionally, since the ToolStripButton (hence Tab) is hwndless, it paints directly onto the

TabStrip itself, thus it can get clipped by any control hosted on the TabStrip (e.g.

ToolStripTextBox, ToolStripComboBox).

We’ve run into our first limitation: any hwnd-less ToolStripItem cannot paint on top of a

ToolStripControlHost’ed item.

Not to worry, as we can still make the Tabs paint on top of one another. By tweaking our logic around advancing nextLocation, we can solve the overlap issue. If the next item is not a tab, we will advance by the entire item.Width instead of subtracting out the tabStrip.Overlap.

We can replace nextLocation.X += item.Width - tabStrip.TabOverlap;

With

Tab currentTab = item as Tab ;

Tab nextTab = (i + 1 < tabStrip.Items.Count) ? tabStrip.Items[i + 1] as Tab : null ; if (currentTab != null && nextTab != null ) {

// we are a Tab, and the next thing is a Tab - overlap

nextLocation.X += item.Width - tabStrip.TabOverlap;

} else {

nextLocation.X += item.Width;

}

What about Padding/Margin?

It is important to note that item.Padding is included in the result of the item.GetPreferredSize method, so the LayoutEngine does not have to take this into consideration.

However, it can take item.Margin into consideration for inter-item spacing requirements. This should be easy to add in by adjusting nextLocation by the item’s margin.

Bringing a Tab to Front

In order to understand how to bring one tab in front of another, we need to understand Z-

Ordering concepts. Z-order is how controls pile up on top of one another (as if there were a zaxis coming out of your computer screen).

Figure 6. Showing the “Z-Axis” and Z-Order effects.

In order to successfully pile tabs on top of one another, we need to invent the same concept for

TabStrip. We have half of this battle already conquered – the tabs layout on top of one another.

The other half is to get the selected tab to paint on top of the other tabs.

The ToolStrip uses the DisplayedItems collection to paint its child items when it gets a paint event. If we were to peek at its OnPaint method, we would see:

for (int i = 0; i < DisplayedItems.Count; i++) {

ToolStripItem item = DisplayedItems[i];

Call item.OnPaint(…)

}

From this we can see that if all the toolstrip items in the collection overlapped, the last one in the

DisplayedItems collection would paint over all the rest of the items. We can use this to our advantage. The DisplayedItems collection represents the collection of items that are currently physically present on the ToolStrip – this is distinct from the Items collection, which contains all items regardless of their visibility.

The DisplayedItems collection can be controlled/reset by overriding the SetDisplayedItems method. The SetDisplayedItems method is called as a result of a layout being performed on the

ToolStrip itself. In our case, we want to make sure the SelectedTab is the last tab in the collection. Overriding SetDisplayedItems and adding the SelectedTab to the end of the collection brings the SelectedTab to the front of the Z-Order as it will be the last item to paint. protected override void SetDisplayedItems() { base .SetDisplayedItems(); for ( int i = 0; i < DisplayedItems.Count; i++) { if (DisplayedItems[i] == SelectedTab) {

DisplayedItems.Add(SelectedTab); break ;

}

}

}

Adding a Custom Border

Now that the SelectedTab appears on top of the other Tabs in the TabStrip, we can add a custom border to the TabStrip to make the other Tabs appear behind the SelectedTab.

Figure 7. Adding a bottom border to the TabStrip to push the other tabs “behind” the

SelectedTab.

The border can be easily added in the ToolStripRenderer’s OnRenderToolStripBorder method. In order to draw the line we will just need to draw blue and white lines across the bottom of the TabStrip, skipping over the SelectedTab.

Building the rest of the TabStrip application

Now that we’ve tweaked the ToolStrip into a TabStrip – we need an area for the content to go when the control is specified. If we think back to the TabControl, we need an area for our

TabPages. In order to do this, we’ll add two more controls: a TabStripPage and a

TabPageSwitcher.

The TabPageSwitcher will hold all the TabStripPages, and there will be a corresponding

TabStripPage for every Tab in our TabStrip.

Figure 8: Putting it all together to build a TabControl-style application.

The TabPageSwitcher automatically Dock.Fills all the TabStripPages inside of it. It has a

SelectedTabStripPage property, which indicates which TabStripPage should be shown on the top of all of the others. When the SelectedTabStripPage is set, the TabPageSwitcher will call

SelectedTabStripPage.BringToFront() to shuffle the SelectedTabStripPage in front of the other

TabStripPages.

Using z-order to accomplish this tab page switching slightly more performant than adding/removing controls to the controls collection as the number of events to perform in changing z-order over adding/removing controls is significantly reduced.

Adding features to the TabStrip in Designtime

Creating a TabPageSwitcher when a TabStrip is selected in the Toolbox

When we drag a TabStrip onto the form, we also want to create a TabPageSwitcher. This is accomplished by creating a custom toolbox item for TabStrip which overrides

ToolBoxItem.CreateComponentsCore.

To create the TabPageSwitcher, we can ask the IDesignerHost (passed in as an argument to

CreateComponentsCore) to CreateComponent(typeof(TabPageSwitcher)). Using property descriptors, we can set properties associating the TabStrip to the TabStripPage.

To specify that our custom TabStripToolBoxItem should be used when dragging a TabStrip off the Toolbox, we need to use the ToolboxItemAttribute above the TabStrip class:

[ToolboxItem(typeof(TabStripToolboxItem))]

Since we only want to create the TabPageSwitcher when a TabStrip is created, we’ll hide the

TabPageSwitcher from the toolbox by adding a[ToolBoxItem(false)] attribute over the class.

Adding an “Add TabStripPage” verb to the TabPageSwitcher

The System.Windows.Forms.TabControl has a link off the property grid, a context menu item and a link off its designer action panel which add new TabPages. This is accomplished by adding a specialized designer to the TabControl and adding specialized designer verbs to the Verbs collection.

Since we want the TabPageSwitcher to have child controls, we should create a custom designer which inherits from ParentControlDesigner. Our TabPageSwitcherDesigner will override the Verbs collection and add a new DesignerVerb, which will call back on an EventHandler in our class when executed – OnAdd.

The implementation of OnAdd is very much like our custom toolbox item, it will ask the

IDesignerHost to CreateComponent(typeof(TabStripPage)). In addition, if there is an associated

TabStrip with the TabPageSwitcher, it will add a corresponding Tab to the TabStrip.

Syncing selection between the Tabs in the TabStrip and the SelectedTabStripPage

Now that we can have multiple TabStripPages within the TabPageSwitcher, it would make sense to synchronize selection between the Tabs and the TabStripPages. When a Tab is selected, its associated TabStripPage should appear.

In order to determine this, we’ll extend the TabPageSwitcherDesigner to use the

ISelectionService’s SelectionChanged event. We’ll hook onto this event by overriding the

Initialize method and unhook from this event by overriding the Dispose method.

In the SelectionChanged event we’ll check if there is only one component selected in the current selection. If the selected component is a Tab, we’ll change the SelectedTabStripPage on the

TabPageSwitcher.

Controlling Docking Behavior

Our TabStripPage inherits System.Windows.Forms.Panel, which has an action on the Designer

Action Panel called “Dock in Parent Container”. We always want our TabStripPage to Dock.Fill, and the TabPageSwitcher handles this for us; it seems wise to stop advertising this in the designer. In order to control this, we can use the DockingAttribute to specify that we never want this option available.

[ Docking ( DockingBehavior .Never)]

However, our TabPageSwitcher is a different story. If there are no other controls on the form at the time we want to Dock.Fill automatically. To specify this, we will specify DockingBehavior.Ask as an attribute on top of its class.

Summing up

The possibilities for extending the ToolStrip class are quite large – by customizing layout and drawing aspects, you can go far beyond a simple toolbar.

There are some areas this article did not touch: accessibility and high contrast. As with building any custom control, these areas need to be taken into consideration. For more information on active accessibility, consult the MSDN documentation on CreateAccessibilityInstance.

Download