Click here to Skip to main content
15,889,315 members
Articles / Multimedia / GDI+
Article

FireFox-like Tab Control

Rate me:
Please Sign up or sign in to vote.
4.61/5 (54 votes)
24 Jun 2008CPOL3 min read 351.6K   14K   182   88
An article on Tab Control

Screenshot - image001.png

Introduction

Recently I came across a requirement for a tab control to be closed from the header itself. This means the user doesn't need to switch to any tab page in order to close it. The perfect example for this one is Firefox browser. In Firefox, the user can open as many tabs as he wants and he can close any tab at any time without opening the tab. I tried Googling a solution and didn't find anything that was exactly what I wanted. Then I thought about implementing my own tab control with the same functionality. So finally, here is the control.

Using the Code

The control enables the designer to add/remove tab pages like a normal tab control with the new look and feel. The classes that support this features are:

  1. TabCtlEx.cs (inherited from System.Windows.Forms.TabControl)
  2. TabPage.cs (inherited from System.Windows.Forms.TabPage)

Using this control is straightforward, like the .NET tab control.

C#
private MyControlLibrary.TabCtlEx userControl11;
    TabPageEx tabPage1;
    TabPageEx tabPage2;
    TabPageEx tabPage3;

private TabPageEx tabPage4;
    this.userControl11 = new MyControlLibrary.TabCtlEx();
    this.tabPage1 = new MyControlLibrary.TabPageEx(this.components);
    this.tabPage2 = new MyControlLibrary.TabPageEx(this.components);
    this.tabPage3 = new MyControlLibrary.TabPageEx(this.components);
    this.tabPage4 = new MyControlLibrary.TabPageEx(this.components);
    this.Controls.Add(this. userControl11);

Drawing the Close Button on Each Tab Header

This requires you to override the existing OnDrawItem() function in the .NET tab control.

C#
/// <summary>
/// override to draw the close button
/// </summary>
/// <param name="e"></param>
protected override void OnDrawItem(DrawItemEventArgs e)
{
    RectangleF tabTextArea = RectangleF.Empty;
    for(int nIndex = 0 ; nIndex < this.TabCount ; nIndex++)
    {
        if( nIndex != this.SelectedIndex )
        {
            /*if not active draw ,inactive close button*/
            tabTextArea = (RectangleF)this.GetTabRect(nIndex);
            using(Bitmap bmp = new Bitmap(GetContentFromResource(
                "closeinactive.bmp")))
            {
                e.Graphics.DrawImage(bmp,
                    tabTextArea.X+tabTextArea.Width -16, 5, 13, 13);
            }
        }
        else
        {
            tabTextArea = (RectangleF)this.GetTabRect(nIndex);
            LinearGradientBrush br = new LinearGradientBrush(tabTextArea,
                SystemColors.ControlLightLight,SystemColors.Control,
                LinearGradientMode.Vertical);
            e.Graphics.FillRectangle(br,tabTextArea);

            /*if active draw ,inactive close button*/
            using(Bitmap bmp = new Bitmap(
                GetContentFromResource("close.bmp")))
            {
                e.Graphics.DrawImage(bmp,
                    tabTextArea.X+tabTextArea.Width -16, 5, 13, 13);
            }
            br.Dispose();
        }
        string str = this.TabPages[nIndex].Text;
        StringFormat stringFormat = new StringFormat();f
        stringFormat.Alignment = StringAlignment.Center; 
        using(SolidBrush brush = new SolidBrush(
            this.TabPages[nIndex].ForeColor))
        {
            /*Draw the tab header text
            e.Graphics.DrawString(str,this.Font, brush,
            tabTextArea,stringFormat);
        }
    }
}

Here, the close button is actually the bitmap image drawn over each tab header. So, in just giving the look and feel of a button used in Firefox, three different bitmap images are used:

  • closeinactive.bmp
  • close.bmp
  • onhover.bmp

These images are embedded with the control and extracted using reflection. There is also a function used to get the embedded image resource. This function returns the stream and creates an image by passing the stream to a bitmap class, as described above.

C#
/// <summary>
/// Get the stream of the embedded bitmap image
/// </summary>
/// <param name="filename"></param>
/// <returns></returns>
private Stream GetContentFromResource(string filename)
{
    Assembly asm = Assembly.GetExecutingAssembly();
    Stream stream =asm.GetManifestResourceStream(
        "MyControlLibrary."+filename);
    return stream;
}

Firefox asks the user to confirm whether he wants to close the tab page or not. I like to have the same functionality for my control, since sometimes the user may press the close button accidentally and doesn't want to lose the changes done. This is accomplished by setting the Boolean property.

C#
private bool confirmOnClose = true;
public bool ConfirmOnClose
{
    get
    {
        return this.confirmOnClose;
    }
    set
    {
        this.confirmOnClose = value;
    }
}

Property

ConfirmOnClose confirms with a message box before closing the tab page. Here is the code to check if the clicked area is inside the bitmap rectangle or not.

C#
protected override void OnMouseDown(MouseEventArgs e)
{
    RectangleF tabTextArea = (RectangleF)this.GetTabRect(SelectedIndex);
    tabTextArea = 
        new RectangleF(tabTextArea.X+tabTextArea.Width -16,5,13,13);
    Point pt = new Point(e.X,e.Y);
    if(tabTextArea.Contains(pt))
    {
        if(confirmOnClose)
        {
            if(MessageBox.Show("You are about to close "+
                this.TabPages[SelectedIndex].Text.TrimEnd()+
                " tab. Are you sure you want to continue?","Confirm close",
                MessageBoxButtons.YesNo) == DialogResult.No)
            return;
        }
        //Fire Event to Client
        if(OnClose != null)
        {
            OnClose(this,new CloseEventArgs(SelectedIndex));
        }
    }
}

Whenever the close button on the tab header is pressed, the control will fire an OnClose() event to the client with the clicked tab index as the argument. This will give some more flexibility to the client to do something before closing the tab.

Conclusion

It's just yet another tab control that provides some flexibility and an improved user interface. I found this functionality very useful in some scenarios. Feedbacks are welcome!

History

Updated with menu button functionality. The above description is just for adding the close button, but the article source code contains the actual implementation for both the close button and the menu button. The major advantage with this control is that menu item functionality is applied to each tab page instead of tab control. In other words, we can have different menu items for different tab pages.

If there is no menu attached with any of the tab pages, it won't display the menu button. The below picture shows that tabPage3 doesn't have a menu attached and so doesn't display the Menu button for that tab page only.

Screenshot - image004.jpg

The actual code for attaching the menu item is:

C#
this.tabPage1.Menu
    = contextMenuStrip1;
………
this.tabPage2.Menu
    = contextMenuStrip1;
………
this.tabPage3.Menu
    = contextMenuStrip1;
………
this.tabPage4.Menu
    = contextMenuStrip1;

Either you can assign the same menu item or a different menu item for each tab page.

History

  • 16 August, 2007 -- Original version posted
  • 28 August, 2007 -- Article and download updated
  • 24 June, 2008 -- source updated

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
worst fellow working as a consultant in a Telecom company (USA)
knows little bit on C,C++ and C# with 5+ years of experience.

Comments and Discussions

 
QuestionFlickering Pin
AKDM14-Apr-09 8:19
AKDM14-Apr-09 8:19 
QuestionRe: Flickering Pin
dansam10017-Apr-09 9:47
dansam10017-Apr-09 9:47 
AnswerRe: Flickering Pin
Herman Vos19-Apr-09 18:26
Herman Vos19-Apr-09 18:26 
GeneralRe: Flickering Pin
dansam10019-Apr-09 18:36
dansam10019-Apr-09 18:36 
GeneralRe: Flickering [modified] Pin
Herman Vos20-Apr-09 9:00
Herman Vos20-Apr-09 9:00 
GeneralRe: Flickering Pin
dansam10021-Apr-09 3:11
dansam10021-Apr-09 3:11 
AnswerRe: Flickering Pin
DelphiCoder22-Nov-09 19:39
DelphiCoder22-Nov-09 19:39 
AnswerRe: Flickering Pin
XuSCo9-Sep-10 8:28
XuSCo9-Sep-10 8:28 
Sorry if my English is not very good.

This is a fantastic component! Thank you for your effort vijayaprasen!

I have tried to resolve this problem and I think the flickering is resolved. I think the flickering is due to the mouse_move event that makes redrawing all pages. So the first thing to do is avoiding redraw all headers. To do so I have made some changes:

First I have saved the last header on wich the mouse had the pointer. So if the mouse change between headears I only redraw the last header and the current header. It can be do like this:

The code is different from the original:

protected override void OnMouseMove(MouseEventArgs e)
{
if (!DesignMode)
{
using (Graphics g = CreateGraphics())
{
g.SmoothingMode = SmoothingMode.AntiAlias;

if (<b>this.nLastPageButtonPainted != -1 &amp;&amp; this.nLastPageButtonPainted &gt;= 0 &amp;&amp; nLastPageButtonPainted &lt; this.TabCount</b>)
{
RectangleF tabTextArea = GetTabRect(nLastPageButtonPainted);

using (LinearGradientBrush _Brush = new LinearGradientBrush(tabTextArea, SystemColors.Control, SystemColors.ControlLight, LinearGradientMode.Vertical))
{
DrawButtons(g, _Brush, tabTextArea, nLastPageButtonPainted, false);
}
}
for (int nIndex = 0; nIndex &lt; this.TabCount; nIndex++)
{
//Close Button
RectangleF tabTextArea = (RectangleF)this.GetTabRect(nIndex);

Point pt = new Point(e.X, e.Y);

if (tabTextArea.Contains(pt))
{
using (LinearGradientBrush _Brush = new LinearGradientBrush(tabTextArea, SystemColors.Control, SystemColors.ControlLight, LinearGradientMode.Vertical))
{
DrawButtons(g, _Brush, tabTextArea, nIndex, false);
<b>nLastPageButtonPainted = nIndex;</b>
}

<b>break;</b>
}
}
}
}
base.OnMouseMove(e);
}

Then we have the same problem inside the header in which is the mouse so we need to avoid redrawing the button if is not necessary. To do so I have two booleans variables that save the state of the button.

These two variables are:


<b>private bool bCloseButtonIsBold = false;</b>
private object closeButtonMutex = new object();

<b>private bool bContextMenuButtonIsBold = false;</b>
private object contextMenuMutex = new object();

The mutex object is to avoid concurrence problems. I'll make another mutex to control the access to TabPages.Count and index (actually I'm changing the style of the buttons and the behaviour of the headers to may requisites).

The draw buttons function is like this:

private void DrawButtons(Graphics g,
LinearGradientBrush _Brush,
RectangleF tabTextArea,
int nIndex,
bool bDrawItemEvent)
{

System.Drawing.Point ptMouse = PointToClient(System.Windows.Forms.Control.MousePosition);

DrawCloseButton(g, _Brush, nIndex, bDrawItemEvent, ptMouse, tabTextArea);
DrawContextMenuButton(g, _Brush, nIndex, bDrawItemEvent, ptMouse, tabTextArea);
}

I redraw the close and menu button in equal but different functions.

The close button function is like this:

private void DrawCloseButton(Graphics g, LinearGradientBrush _Brush, int nIndex, bool bDrawItemEvent, System.Drawing.Point ptMouse, RectangleF tabTextArea)
{
lock (closeButtonMutex)
{

RectangleF closeTabTextArea =
new RectangleF(tabTextArea.X + tabTextArea.Width - 16, tabTextArea.Height / 2 - 5, tabTextArea.Height - 10, tabTextArea.Height - 10);

if (bDrawItemEvent)
{
using (Pen pen = new Pen(Color.White, 0.5f))
{
StringFormat sf = new StringFormat();
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Center;
sf.FormatFlags = StringFormatFlags.NoWrap;

g.FillRectangle(_Brush, closeTabTextArea);
g.DrawString("X", new Font(this.Font.FontFamily, 8, FontStyle.Regular), new SolidBrush(TabPages[nIndex].ForeColor), closeTabTextArea, sf);

bCloseButtonIsBold = false;

}
}
else
{
if (!closeTabTextArea.Contains(ptMouse))
{
if (bCloseButtonIsBold)
{
using (Pen pen = new Pen(Color.White, 0.5f))
{
StringFormat sf = new StringFormat();
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Center;
sf.FormatFlags = StringFormatFlags.NoWrap;

//g.FillRectangle(_Brush, closeTabTextArea);
g.DrawString("X", new Font(this.Font.FontFamily, 8, FontStyle.Regular), new SolidBrush(TabPages[nIndex].ForeColor), closeTabTextArea, sf);
bCloseButtonIsBold = false;
}
}
}
else
{
if (!bCloseButtonIsBold)
{
using (Pen pen = new Pen(Color.White, 0.5f))
{
StringFormat sf = new StringFormat();
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Center;
sf.FormatFlags = StringFormatFlags.NoWrap;

//g.FillRectangle(_Brush, closeTabTextArea);
g.DrawString("X", new Font(this.Font.FontFamily, 8, FontStyle.Bold), new SolidBrush(TabPages[nIndex].ForeColor), closeTabTextArea, sf);
bCloseButtonIsBold = true;
}
}
}
}
}
}

If the redraw is motivated by the Item_Draw Event I always redraw the buttons always. If not I check their states and if the mouse is inside of them.
GeneralHelp With Events Pin
AnaliaIbargoyen16-Oct-08 6:18
AnaliaIbargoyen16-Oct-08 6:18 
AnswerRe: Help With Events Pin
vijayaprasen10-Nov-08 10:36
vijayaprasen10-Nov-08 10:36 
QuestionWhat type of event... Pin
andryw1-Aug-08 5:00
andryw1-Aug-08 5:00 
Generalplz help me Pin
JKR00718-Jul-08 0:05
JKR00718-Jul-08 0:05 
GeneralRe: plz help me Pin
vijayaprasen18-Jul-08 1:54
vijayaprasen18-Jul-08 1:54 
Generalset tab size Pin
Member 235184330-Jun-08 23:42
Member 235184330-Jun-08 23:42 
GeneralRe: set tab size Pin
Ilíon1-Jul-08 1:10
Ilíon1-Jul-08 1:10 
GeneralRe: set tab size Pin
Member 23518431-Jul-08 19:22
Member 23518431-Jul-08 19:22 
GeneralI'm sorry [modified] Pin
Ilíon2-Jul-08 7:18
Ilíon2-Jul-08 7:18 
GeneralRe: set tab size Pin
Member 23518431-Jul-08 20:32
Member 23518431-Jul-08 20:32 
GeneralRe: set tab size Pin
Ilíon29-Jul-08 8:59
Ilíon29-Jul-08 8:59 
GeneralRe: set tab size Pin
Member 235184329-Jul-08 19:37
Member 235184329-Jul-08 19:37 
GeneralHCI issue Pin
Derek Bartram25-Jun-08 1:17
Derek Bartram25-Jun-08 1:17 
GeneralRe: HCI issue Pin
vijayaprasen28-Jun-08 4:57
vijayaprasen28-Jun-08 4:57 
GeneralException Pin
gborges20-May-08 10:11
gborges20-May-08 10:11 
GeneralRe: Exception Pin
vijayaprasen20-May-08 13:34
vijayaprasen20-May-08 13:34 
GeneralRe: Exception; suggested solution [modified] Pin
Ilíon27-Jun-08 4:26
Ilíon27-Jun-08 4:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.