Introduction
Every now and then, an interesting question pops up on various developer forums, such as CodeGuru. These types of questions let you wonder why Microsoft didn’t include a certain feature in one of their controls. Curiosity then steps in (I am very curious about anything—it is a blessing and a curse) and you have to find a decent proposal for the person that has asked the question.
Today, I will cover such a topic. The topic I will cover in today’s “How to” article is: “How to add a close button on Tab pages.”
The inherent Tab control Microsoft provides in their suite of controls doesn’t contain a way to add a Close button—or any button, for that matter—onto a Tab Page of a Tab control. This is where UserControls and Inheritance come into play. A UserControl is a control that either extends built-in controls (such as the topic I am covering today), or fills a gap where an ordinary group of controls simply will not fulfill your desired purpose. Inheritance allows you to obtain functionalities and properties from built-in classes and build upon the given foundation to fulfill your desired goals.
Let’s start!
Practical
Start Visual Studio and create a new Windows Forms Project in either C# or Visual Basic.NET. There is no design needed for the form yet; you will be creating a UserControl and adding it to the form later. You can resize the form, though, to make it wide enough to fit in the custom TabControl nicely. Add a new class to your project and give it a name of ClosableTabControl; you cannot get more descriptive than that. Once the class is loaded, add the necessary Namespaces so that you will be able to make use of their functionalities properly inside your UserControl.
C#
using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Linq; using System.Runtime.InteropServices; using System.Windows.Forms;
VB.NET
Imports System.ComponentModel Imports System.Runtime.InteropServices
The System.ComponentModel Namespace allows you to add nice descriptions to your UserControl’s various properties as well as to assign the properties to their associated Property Sections inside the Properties Window. The System.Runtime.InteropServices namespace allows you to make use of the Windows API in a memory managed manner.
Edit your Class description to resemble the following.
C#
[ToolboxBitmap(typeof(TabControl))] public class ClosableTabControl : TabControl {
VB.NET
<ToolboxBitmap(GetType(TabControl))> Public Class ClosableTabControl Inherits TabControl
This sets the picture displayed inside the Toolbox to the image similar to the built in TabControl’s. Then, you specify that the control you are creating now has TabControl properties and methods.
Add the code for the SetParent API.
C#
[DllImport("user32.dll", SetLastError = true)] static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
VB.NET
<DllImport("user32.dll")> Public Shared Function SetParent(ByVal hWndChild As IntPtr, _ ByVal hWndNewParent As IntPtr) As IntPtr End Function
The SetParent Windows API is present inside the user32.dll file. You supply its arguments and return type. The SetParent API allows you to set a different parent handle to a certain window—in this case, the Close button that you will add to the TabPage window. Add the UserControl’s fields.
C#
private Dictionary<Button, TabPage> dicButtons = new Dictionary<Button, TabPage>(); private bool blnShow = true; private Image imgImage;
VB.NET
Private dicButtons As New Dictionary(Of Button, TabPage) Private blnShow As Boolean = True Private imgImage As Image
dicButtons will contain a Dictionary object containing each button that should be added to the TabControl on its various Tab Pages. blnShow is a Boolean that allows you to set the Visible property of the Close button to True or False. imgImage is the Image object containing the image of the Close button.
Add the custom Events and Properties for the UserControl.
C#
public event CancelEventHandler CloseClick; [Browsable(true)] [DefaultValue(true)] [Category("Behavior")] [Description("Show / Hide Close Button(s)")] public bool Show { get { return blnShow; } set { blnShow = value; foreach (var btn in dicButtons.Keys) btn.Visible = blnShow; Repos(); } } [Browsable(true)] [DefaultValue(true)] [Category("Appearance")] [Description("Close Image")] public Image TabPageImage { get { return imgImage; } set { imgImage = value; } }
VB.NET
Public Event CloseClick As CancelEventHandler <Browsable(True), DefaultValue(True), Category("Behavior"), _ Description("Show / Hide Close Button(s)")> Public Property Show() As Boolean Get Return blnShow End Get Set(ByVal value As Boolean) blnShow = value For Each btn In dicButtons.Keys btn.Visible = blnShow Next Repos() End Set End Property <Browsable(True), DefaultValue(True), Category("Appearance"), _ Description("Close Image")> Public Property TabPageImage() As Image Get Return imgImage End Get Set(ByVal value As Image) imgImage = value End Set End Property
You created a CloseClick event that will be fired when the user clicks the Close button. More on this a bit later. Then, you created two custom properties named ‘Show’ and ‘TabPageImage,’ respectively. You also designated where this property should be displayed inside the Properties Window and gave it a description.
Because you inherited the built-in TabControl’s properties and methods, to allow to perform custom actions, you need to override its built-in events. This is why all the following properties will be overridden to compensate for the Close button’s addition. Add the remaining events for your UserControl.
C#
protected override void OnCreateControl() { base.OnCreateControl(); Repos(); } protected override void OnControlAdded(ControlEventArgs e) { base.OnControlAdded(e); TabPage tpCurrent = (TabPage)e.Control; Rectangle rtCurrent = this.GetTabRect(this.TabPages.IndexOf(tpCurrent)); Button btnClose = new Button(); btnClose.Image = Properties.Resources.Close; btnClose.ImageAlign = ContentAlignment.MiddleRight; btnClose.TextAlign = ContentAlignment.MiddleLeft; btnClose.Size = new Size(rtCurrent.Height - 1, rtCurrent.Height - 1); btnClose.Location = new Point(rtCurrent.X + rtCurrent.Width - rtCurrent.Height - 1, rtCurrent.Y + 1); SetParent(btnClose.Handle, this.Handle); btnClose.Click += OnCloseClick; dicButtons.Add(btnClose, tpCurrent); } protected override void OnLayout(LayoutEventArgs lea) { base.OnLayout(lea); Repos(); } protected virtual void OnCloseClick(object sender, EventArgs e) { if (!DesignMode) { Button btnClose = (Button)sender; TabPage tpCurrent = dicButtons[btnClose]; CancelEventArgs cea = new CancelEventArgs(); CloseClick?.Invoke(sender, cea); if (!cea.Cancel) { if (TabPages.Count > 1) { TabPages.Remove(tpCurrent); btnClose.Dispose(); Repos(); } else MessageBox.Show("Must Have At Least 1 Tab Page"); } } } public void Repos() { foreach (var but in dicButtons) Repos(but.Value); } public void Repos(TabPage tpCurrent) { Button btnClose = CloseButton(tpCurrent); if (btnClose != null) { int tpIndex = TabPages.IndexOf(tpCurrent); if (tpIndex >= 0) { Rectangle rctCurrent = GetTabRect(tpIndex); if (SelectedTab == tpCurrent) { btnClose.Size = new Size(rctCurrent.Height - 1, rctCurrent.Height - 1); btnClose.Location = new Point(rctCurrent.X + rctCurrent.Width - rctCurrent.Height, rctCurrent.Y + 1); } else { btnClose.Size = new Size(rctCurrent.Height - 3, rctCurrent.Height - 2); btnClose.Location = new Point(rctCurrent.X + rctCurrent.Width - rctCurrent.Height - 1, rctCurrent.Y + 1); } btnClose.Visible = Show; btnClose.BringToFront(); } } } protected Button CloseButton(TabPage tpCurrent) { return (from item in dicButtons where item.Value == tpCurrent select item.Key).FirstOrDefault(); }
VB.NET
Protected Overrides Sub OnCreateControl() MyBase.OnCreateControl() Repos() End Sub Protected Overrides Sub OnControlAdded(ByVal e As _ ControlEventArgs) MyBase.OnControlAdded(e) Dim tpCurrent As TabPage = DirectCast(e.Control, TabPage) Dim rtCurrent As Rectangle = _ Me.GetTabRect(Me.TabPages.IndexOf(tpCurrent)) Dim btnClose As New Button btnClose.Image = My.Resources.Close btnClose.ImageAlign = ContentAlignment.MiddleRight btnClose.TextAlign = ContentAlignment.MiddleLeft btnClose.Size = New Size(rtCurrent.Height - 1, _ rtCurrent.Height - 1) btnClose.Location = New Point(rtCurrent.X + _ rtCurrent.Width - rtCurrent.Height - 1, rtCurrent.Y + 1) SetParent(btnClose.Handle, Me.Handle) AddHandler btnClose.Click, AddressOf OnCloseClick dicButtons.Add(btnClose, tpCurrent) End Sub Protected Overrides Sub OnLayout(ByVal lea As LayoutEventArgs) MyBase.OnLayout(lea) Repos() End Sub Protected Overridable Sub OnCloseClick(ByVal sender As Object, _ ByVal e As EventArgs) If Not DesignMode Then Dim btnClose As Button = DirectCast(sender, Button) Dim tpCurrent As TabPage = dicButtons(btnClose) Dim cea As New CancelEventArgs RaiseEvent CloseClick(sender, cea) If Not cea.Cancel Then If TabPages.Count > 1 Then TabPages.Remove(tpCurrent) btnClose.Dispose() Repos() Else MessageBox.Show("Must Have At Least 1 Tab Page") End If End If End If End Sub Public Sub Repos() For Each but In dicButtons Repos(but.Value) Next End Sub Public Sub Repos(ByVal tpCurrent As TabPage) Dim btnClose As Button = CloseButton(tpCurrent) If btnClose IsNot Nothing Then Dim tpIndex As Integer = TabPages.IndexOf(tpCurrent) If tpIndex >= 0 Then Dim rctCurrent As Rectangle = GetTabRect(tpIndex) If SelectedTab Is tpCurrent Then btnClose.Size = New Size(rctCurrent.Height - 1, _ rctCurrent.Height - 1) btnClose.Location = New Point(rctCurrent.X + _ rctCurrent.Width - rctCurrent.Height, _ rctCurrent.Y + 1) Else btnClose.Size = New Size(rctCurrent.Height - 3, _ rctCurrent.Height - 2) btnClose.Location = New Point(rctCurrent.X + _ rctCurrent.Width - rctCurrent.Height - 1, _ rctCurrent.Y + 1) End If btnClose.Visible = Show btnClose.BringToFront() End If End If End Sub Protected Function CloseButton(ByVal tpCurrent As TabPage) _ As Button Return (From item In dicButtons Where item.Value Is _ tpCurrent Select item.Key).FirstOrDefault End Function
In the preceding code, you have added the Close button from the Resources. I am including a Close button image for you with this article (see Figure 1). You then re-aligned the image to show on the right corner of each associated Tab Page. Lastly, you have given the Close Button a function.
Figure 1: A Close button image for your convenience
In case you do not know how to add a Resource to your project, follow these steps, as shown in Figure 2:
- Click Project.
- Choose ProjectName, Properties.
- Click resources.
- Select the Type of Resource (in this case, an image).
- And, set the Image.
Figure 2: Adding a Resource
Build your project. Once built, the new UserControl will be shown inside the ToolBox (see Figure 3).
Figure 3: Toolbox
Double-click this control and it will be added to your form. Figures 4 and 5 show the control in design time as well as in Runtime.
Figure 4: Design time
Figure 5: Runtime
Conclusion
Being able to override existing functionality in existing controls is a vital skill to learn. I wanted to add an “Add” button to the TabControl itself as well, but I didn’t get time for it. Perhaps in a later article, we can explore that functionality. Until then, happy coding!