Windows Forms: Closeable TabControl

Print Content | More
Today I needed to modify the UI of an application built by my company to enable the support for a multi-tabbed document interface (like the standard Visual Studio interface); since there’s no default support for displaying a close button (or context menu specific to each opened tab, or any other action button you may like to have on the tabs) I needed to implement my own version of this control.

It isn’t too hard to do once you read the documentation: basically you need to inform the control you will be going to render the content of each tab header by yourself (instead of using the default rendering) setting the value of the property DrawMode to TabDrawMode.OwnerDrawFixed. Then you can write your own rendering logic overriding the OnDrawItem function.

The main problem is: you can’t place other controls inside the Tab Header and you have to do all by hand; the same goes for the hit tests to see if you clicked on the custom action buttons you placed there, so you just need to do some basic math calculations to see if the mouse if over the buttons’ sensitive areas.

In my example I decided to render the close button using some standard GDI calls at first, but you can modify the code and use bitmaps or whatever you like.

here’s the full code:
/// <summary>
/// A tab control that can be used to close tabs with a custom drawn button.
/// </summary>
public class SidTabControl : System.Windows.Forms.TabControl
{
    public SidTabControl()
    {
        SetStyle(ControlStyles.DoubleBuffer, true);
        TabStop = false;
        DrawMode = TabDrawMode.OwnerDrawFixed;
        _closeButtonBrush = new SolidBrush(_closeButtonColor);
        ItemSize = new Size(ItemSize.Width, 24);
        // used to expand the tab header, find a better way
        Padding = new Point(16, 0);
    }
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _stringFormat.Dispose();
            _closeButtonBrush.Dispose();
        }
        base.Dispose(disposing);
    }
    public delegate void TabClosedDelegate(object sender, ClosedEventArgs e);
    public delegate void TabClosingDelegate(object sender, ClosingEventArgs e);
    public event TabClosedDelegate TabClosed;
    public event TabClosingDelegate TabClosing;
    private int _buttonWidth = 16;
    [DefaultValue(16), Category("Action Buttons")]
    public int ButtonWidth
    {
        get { return _buttonWidth; }
        set { _buttonWidth = value; }
    }
    private int _crossOffset = 3;
    [DefaultValue(3), Category("Action Buttons")]
    public int CrossOffset
    {
        get { return _crossOffset; }
        set { _crossOffset = value; }
    }
    private readonly StringFormat _stringFormat = new StringFormat
                                        {
                                            Alignment = StringAlignment.Near,
                                            LineAlignment = StringAlignment.Center
                                        };
    private Color _closeButtonColor = Color.Red;
    private Brush _closeButtonBrush;
    [Category("Action Buttons")]
    public Color CloseButtonColor
    {
        get { return _closeButtonColor; }
        set
        {
            _closeButtonBrush.Dispose();
            _closeButtonColor = value;
            _closeButtonBrush = new SolidBrush(_closeButtonColor);
            Invalidate();
        }
    }
    protected override void OnDrawItem(DrawItemEventArgs e)
    {
        if (e.Bounds != RectangleF.Empty)
        {
            e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
            for (int nIndex = 0; nIndex < TabCount; nIndex++)
            {
                Rectangle tabArea = GetTabRect(nIndex);
                Rectangle closeBtnRect = GetCloseBtnRect(tabArea);
                if (nIndex != SelectedIndex)
                {
                    e.Graphics.DrawRectangle(Pens.DarkGray, closeBtnRect);
                    DrawCross(e, closeBtnRect, Color.DarkGray);
                }
                else
                {
                    //Drawing Close Button
                    e.Graphics.FillRectangle(_closeButtonBrush, closeBtnRect);
                    e.Graphics.DrawRectangle(Pens.White, closeBtnRect);
                    DrawCross(e, closeBtnRect, Color.White);
                }
                string str = TabPages[nIndex].Text;
                e.Graphics.DrawString(str, Font, new SolidBrush(TabPages[nIndex].ForeColor), tabArea, _stringFormat);
            }
        }
    }
    private void DrawCross(DrawItemEventArgs e, Rectangle btnRect, Color color)
    {
        using (Pen pen = new Pen(color, 2))
        {
            float x1 = btnRect.X + CrossOffset;
            float x2 = btnRect.Right - CrossOffset;
            float y1 = btnRect.Y + CrossOffset;
            float y2 = btnRect.Bottom - CrossOffset;
            e.Graphics.DrawLine(pen, x1, y1, x2, y2);
            e.Graphics.DrawLine(pen, x1, y2, x2, y1);
        }
    }
    private Rectangle GetCloseBtnRect(Rectangle tabRect)
    {
        Rectangle rect = new Rectangle(tabRect.X + tabRect.Width - ButtonWidth - 4, (tabRect.Height - ButtonWidth) / 2, ButtonWidth, ButtonWidth);
        return rect;
    }
    protected override void OnMouseDown(MouseEventArgs e)
    {
        if (!DesignMode)
        {
            Rectangle rect = GetTabRect(SelectedIndex);
            rect = GetCloseBtnRect(rect);
            Point pt = new Point(e.X, e.Y);
            if (rect.Contains(pt))
            {
                CloseTab(SelectedTab);
            }
        }
    }
    public void CloseTab(int tabindex)
    {
        CloseTab(TabPages[tabindex]);
    }
    public void CloseTab(TabPage tp)
    {
        ClosingEventArgs args = new ClosingEventArgs(TabPages.IndexOf(tp));
        OnTabClosing(args);
        //Remove the tab and fir the event tot he client
        if (!args.Cancel)
        {
            // close and remove the tab, dispose it too
            TabPages.Remove(tp);
            OnTabClosed(new ClosedEventArgs(tp));
            tp.Dispose();
        }
    }
    protected void OnTabClosed(ClosedEventArgs e)
    {
        if (TabClosed != null)
        {
            TabClosed(this, e);
        }
    }
    protected void OnTabClosing(ClosingEventArgs e)
    {
        if (TabClosing != null)
            TabClosing(this, e);
    }
}
Some support classes:
public class ClosingEventArgs
{
    private readonly int _nTabIndex = -1;
    public ClosingEventArgs(int nTabIndex)
    {
        _nTabIndex = nTabIndex;
        Cancel = false;
    }
    public bool Cancel { get; set; }
    /// <summary>
    /// Get/Set the tab index value where the close button is clicked
    /// </summary>
    public int TabIndex
    {
        get
        {
            return _nTabIndex;
        }
    }
}
public class ClosedEventArgs : EventArgs
{
    private readonly TabPage _tab;
    public ClosedEventArgs(TabPage tab)
    {
        _tab = tab;
    }
    /// <summary>
    /// Get/Set the tab index value where the close button is clicked
    /// </summary>
    public TabPage Tab
    {
        get
        {
            return _tab;
        }
    }
}
And this is how this control looks like in action: CloseableTabControl Actually you have very limited possibilities to configure/change the way it renders the Tabs and the Button (you can play with the custom ItemSize, Padding, Margin and some custom properties I’ve added like ButtonWidth and CloseButtonColor). Two events are raised when a tab is about to close (TabClosing) and when it has been closed and removed (TabClosed).

I have to admit that the same thing was much easy to do in WPF due to its fully support for control templates and the extensibility was absolutely higher than creating the same control in Windows Form.

Net, Closeable, Tab, Tabcontrol, Windows forms

0 comments

Related Post

All fields are required and you must provide valid data in order to be able to comment on this post.


(will not be published)
(es: http://www.mysite.com)