dcsimg
 

Adding Close Buttons to Tab Pages with .NET

Thursday Sep 6th 2018 by Hannes DuPreez

Oddly, Microsoft didn't include this feature with its releases. But, never fear. You can add your own! Examples are in C# and VB.NET.

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.

A Close button image for your convenience
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:

  1. Click Project.
  2. Choose ProjectName, Properties.
  3. Click resources.
  4. Select the Type of Resource (in this case, an image).
  5. And, set the Image.

Adding a Resource
Figure 2: Adding a Resource

Build your project. Once built, the new UserControl will be shown inside the ToolBox (see Figure 3).

Toolbox
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.

Design time
Figure 4: Design time

Runtime
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!

Home
Mobile Site | Full Site
Copyright 2018 © QuinStreet Inc. All Rights Reserved