Click here to Skip to main content
15,888,461 members
Articles / Programming Languages / C#
Article

A .NET Flat TabControl (CustomDraw)

Rate me:
Please Sign up or sign in to vote.
4.71/5 (51 votes)
5 Dec 2005Public Domain3 min read 444.8K   22.7K   186   73
This is a CustomDraw TabControl that appears flat and supports icons and is filled with the backcolor property.

Image 1

Introduction

The TabControl included in Visual Studio doesn't support the flat property, so I decided to build my own control. I searched the Internet for something similar, but couldn't find any resource that satisfied my needs.

Well, here is the control, it appears flat and supports icons and is filled with the backcolor property.

Background

First of all, we need the double buffering technique to improve painting and to allow the control to change its appearance:

C#
this.SetStyle(ControlStyles.UserPaint, true);
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles.DoubleBuffer, true);
this.SetStyle(ControlStyles.ResizeRedraw, true);
this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);

Then, you need to override the OnPaint event and draw your own control. The basic steps involved are:

  1. Fill the client area.
  2. Draw the border.
  3. Clip the region for drawing tabs, including the Up-Down buttons if they are visible (see below for Up-Down buttons' subclassing).
  4. Draw each tab page.
  5. Cover other areas by drawing lines near the borders (tip!).
C#
protected override void OnPaint(PaintEventArgs e)
{
  base.OnPaint(e); 
  
  DrawControl(e.Graphics);
}

internal void DrawControl(Graphics g)
{
  if (!Visible)
    return;

  Rectangle TabControlArea = this.ClientRectangle;
  Rectangle TabArea = this.DisplayRectangle;

  //----------------------------
  // fill client area
  Brush br = new SolidBrush(SystemColors.Control);
  g.FillRectangle(br, TabControlArea);
  br.Dispose();
  //----------------------------

  //----------------------------
  // draw border
  int nDelta = SystemInformation.Border3DSize.Width;

  Pen border = new Pen(SystemColors.ControlDark);
  TabArea.Inflate(nDelta, nDelta);
  g.DrawRectangle(border, TabArea);
  border.Dispose();
  //----------------------------


  //----------------------------
  // clip region for drawing tabs
  Region rsaved = g.Clip;
  Rectangle rreg;

  int nWidth = TabArea.Width + nMargin;
  if (bUpDown)
  {
    // exclude updown control for painting
    if (Win32.IsWindowVisible(scUpDown.Handle))
    {
      Rectangle rupdown = new Rectangle();
      Win32.GetWindowRect(scUpDown.Handle, ref rupdown);
      Rectangle rupdown2 = this.RectangleToClient(rupdown);

      nWidth = rupdown2.X;
    }
  }

  rreg = new Rectangle(TabArea.Left, TabControlArea.Top, 
                  nWidth - nMargin, TabControlArea.Height);

  g.SetClip(rreg);

  // draw tabs
  for (int i = 0; i < this.TabCount; i++)
    DrawTab(g, this.TabPages[i], i);

  g.Clip = rsaved;
  //----------------------------


  //----------------------------
  // draw background to cover flat border areas
  if (this.SelectedTab != null)
  {
    TabPage tabPage = this.SelectedTab;
    Color color = tabPage.BackColor;
    border = new Pen(color);
    
    TabArea.Offset(1, 1);
    TabArea.Width -= 2;
    TabArea.Height -= 2;
    
    g.DrawRectangle(border, TabArea);
    TabArea.Width -= 1;
    TabArea.Height -= 1;
    g.DrawRectangle(border, TabArea);

    border.Dispose();
  }
  //----------------------------
}

The DrawTab method uses polygons to draw the border. It also draws the text and the icon for the tab page. I have only implemented alignment to Top and Bottom because the alignment to Left and Right are more complicated (someone can try!) and there are other articles explaining this kind of behavior like .NET style Side Tab Control By helloravi.

C#
internal void DrawTab(Graphics g, TabPage tabPage, int nIndex)
{
  Rectangle recBounds = this.GetTabRect(nIndex);
  RectangleF tabTextArea = (RectangleF)this.GetTabRect(nIndex);

  bool bSelected = (this.SelectedIndex == nIndex);

  Point[] pt = new Point[7];
  if (this.Alignment == TabAlignment.Top)
  {
    pt[0] = new Point(recBounds.Left, recBounds.Bottom);
    pt[1] = new Point(recBounds.Left, recBounds.Top + 3);
    pt[2] = new Point(recBounds.Left + 3, recBounds.Top);
    pt[3] = new Point(recBounds.Right - 3, recBounds.Top);
    pt[4] = new Point(recBounds.Right, recBounds.Top + 3);
    pt[5] = new Point(recBounds.Right, recBounds.Bottom);
    pt[6] = new Point(recBounds.Left, recBounds.Bottom);
  }
  else
  {
    pt[0] = new Point(recBounds.Left, recBounds.Top);
    pt[1] = new Point(recBounds.Right, recBounds.Top);
    pt[2] = new Point(recBounds.Right, recBounds.Bottom - 3);
    pt[3] = new Point(recBounds.Right - 3, recBounds.Bottom);
    pt[4] = new Point(recBounds.Left + 3, recBounds.Bottom);
    pt[5] = new Point(recBounds.Left, recBounds.Bottom - 3);
    pt[6] = new Point(recBounds.Left, recBounds.Top);
  }

  //----------------------------
  // fill this tab with background color
  Brush br = new SolidBrush(tabPage.BackColor);
  g.FillPolygon(br, pt);
  br.Dispose();
  //----------------------------

  //----------------------------
  // draw border
  //g.DrawRectangle(SystemPens.ControlDark, recBounds);
  g.DrawPolygon(SystemPens.ControlDark, pt);

  if (bSelected)
  {
    //----------------------------
    // clear bottom lines
    Pen pen = new Pen(tabPage.BackColor);

    switch (this.Alignment)
    {
      case TabAlignment.Top:
        g.DrawLine(pen, recBounds.Left + 1, recBounds.Bottom, 
                        recBounds.Right - 1, recBounds.Bottom);
        g.DrawLine(pen, recBounds.Left + 1, recBounds.Bottom+1, 
                        recBounds.Right - 1, recBounds.Bottom+1);
        break;

      case TabAlignment.Bottom:
        g.DrawLine(pen, recBounds.Left + 1, recBounds.Top, 
                           recBounds.Right - 1, recBounds.Top);
        g.DrawLine(pen, recBounds.Left + 1, recBounds.Top-1, 
                           recBounds.Right - 1, recBounds.Top-1);
        g.DrawLine(pen, recBounds.Left + 1, recBounds.Top-2, 
                           recBounds.Right - 1, recBounds.Top-2);
        break;
    }
    pen.Dispose();
    //----------------------------
  }
  //----------------------------

  //----------------------------
  // draw tab's icon
  if ((tabPage.ImageIndex >= 0) && (ImageList != null) && 
             (ImageList.Images[tabPage.ImageIndex] != null))
  {
    int nLeftMargin = 8;
    int nRightMargin = 2;

    Image img = ImageList.Images[tabPage.ImageIndex];
    
    Rectangle rimage = new Rectangle(recBounds.X + nLeftMargin, 
                        recBounds.Y + 1, img.Width, img.Height);
    
    // adjust rectangles
    float nAdj = (float)(nLeftMargin + img.Width + nRightMargin);

    rimage.Y += (recBounds.Height - img.Height) / 2;
    tabTextArea.X += nAdj;
    tabTextArea.Width -= nAdj;

    // draw icon
    g.DrawImage(img, rimage);
  }
  //----------------------------

  //----------------------------
  // draw string
  StringFormat stringFormat = new StringFormat();
  stringFormat.Alignment = StringAlignment.Center;  
  stringFormat.LineAlignment = StringAlignment.Center;

  br = new SolidBrush(tabPage.ForeColor);

  g.DrawString(tabPage.Text, Font, br, tabTextArea, 
                                       stringFormat);
  //----------------------------
}

To draw the UpDown buttons I use the method DrawIcons. It uses the leftRightImages ImageList that contains four buttons (left, right, left disabled, right disabled) and it is called when the control receives the WM_PAINT message through subclassing the class as explained below:

C#
internal void DrawIcons(Graphics g)
{
  if ((leftRightImages == null) || 
            (leftRightImages.Images.Count != 4))
    return;

  //----------------------------
  // calc positions
  Rectangle TabControlArea = this.ClientRectangle;

  Rectangle r0 = new Rectangle();
  Win32.GetClientRect(scUpDown.Handle, ref r0);

  Brush br = new SolidBrush(SystemColors.Control);
  g.FillRectangle(br, r0);
  br.Dispose();
  
  Pen border = new Pen(SystemColors.ControlDark);
  Rectangle rborder = r0;
  rborder.Inflate(-1, -1);
  g.DrawRectangle(border, rborder);
  border.Dispose();

  int nMiddle = (r0.Width / 2);
  int nTop = (r0.Height - 16) / 2;
  int nLeft = (nMiddle - 16) / 2;

  Rectangle r1 = new Rectangle(nLeft, nTop, 16, 16);
  Rectangle r2 = new Rectangle(nMiddle+nLeft, nTop, 16, 16);
  //----------------------------

  //----------------------------
  // draw buttons
  Image img = leftRightImages.Images[1];
  if (img != null)
  {
    if (this.TabCount > 0)
    {
      Rectangle r3 = this.GetTabRect(0);
      if (r3.Left < TabControlArea.Left)
        g.DrawImage(img, r1);
      else
      {
        img = leftRightImages.Images[3];
        if (img != null)
          g.DrawImage(img, r1);
      }
    }
  }

  img = leftRightImages.Images[0];
  if (img != null)
  {
    if (this.TabCount > 0)
    {
      Rectangle r3 = this.GetTabRect(this.TabCount - 1);
      if (r3.Right > (TabControlArea.Width - r0.Width))
        g.DrawImage(img, r2);
      else
      {
        img = leftRightImages.Images[2];
        if (img != null)
          g.DrawImage(img, r2);
      }
    }
  }
  //----------------------------
}

Points of interest

Well, here is the trick to paint the UpDown buttons (It was the most difficult task).

First of all, I need to know when they should be painted. It could be achieve by handling three events: OnCreateControl, ControlAdded and ControlRemoved:

C#
protected override void OnCreateControl()
{
  base.OnCreateControl();

  FindUpDown();
}

private void FlatTabControl_ControlAdded(object sender, 
                                      ControlEventArgs e)
{
  FindUpDown();
  UpdateUpDown();
}

private void FlatTabControl_ControlRemoved(object sender, 
                                       ControlEventArgs e)
{
  FindUpDown();
  UpdateUpDown();
}

The function FindUpDown looks for the class msctls_updown32 by using the Win32 GetWindow and looking for the TabControl's child windows (An amazing tip from Fully owner drawn tab control By Oleg Lobach)

If we find the class, we can subclass it for handling the message WM_PAINT (for more information about subclassing, please refer to Subclassing in .NET -The pure .NET way by Sameers and Hacking the Combo Box to give it horizontal scrolling By Tomas Brennan).

C#
private void FindUpDown()
{
  bool bFound = false;

  // find the UpDown control
  IntPtr pWnd = 
      Win32.GetWindow(this.Handle, Win32.GW_CHILD);
  
  while (pWnd != IntPtr.Zero)
  {
    //----------------------------
    // Get the window class name
    char[] className = new char[33];

    int length = Win32.GetClassName(pWnd, className, 32);

    string s = new string(className, 0, length);
    //----------------------------

    if (s == "msctls_updown32")
    {
      bFound = true;

      if (!bUpDown)
      {
        //----------------------------
        // Subclass it
        this.scUpDown = new SubClass(pWnd, true);
        this.scUpDown.SubClassedWndProc += 
            new SubClass.SubClassWndProcEventHandler(
                                 scUpDown_SubClassedWndProc);
        //----------------------------

        bUpDown = true;
      }
      break;
    }
    
    pWnd = Win32.GetWindow(pWnd, Win32.GW_HWNDNEXT);
  }

  if ((!bFound) && (bUpDown))
    bUpDown = false;
}

private void UpdateUpDown()
{
  if (bUpDown)
  {
    if (Win32.IsWindowVisible(scUpDown.Handle))
    {
      Rectangle rect = new Rectangle();

      Win32.GetClientRect(scUpDown.Handle, ref rect);
      Win32.InvalidateRect(scUpDown.Handle, ref rect, true);
    }
  }
}

And here is the subclassing function for WndProc. We process WM_PAINT to draw the icons and validate the client areas:

C#
private int scUpDown_SubClassedWndProc(ref Message m) 
{
  switch (m.Msg)
  {
    case Win32.WM_PAINT:
    {
      //------------------------
      // redraw
      IntPtr hDC = Win32.GetWindowDC(scUpDown.Handle);
      Graphics g = Graphics.FromHdc(hDC);

      DrawIcons(g);

      g.Dispose();
      Win32.ReleaseDC(scUpDown.Handle, hDC);
      //------------------------

      // return 0 (processed)
      m.Result = IntPtr.Zero;

      //------------------------
      // validate current rect
      Rectangle rect = new Rectangle();

      Win32.GetClientRect(scUpDown.Handle, ref rect);
      Win32.ValidateRect(scUpDown.Handle, ref rect);
      //------------------------
    }
    return 1;
  }

  return 0;
}

Using the code

To use the code, simply add reference to the FlatTabControl and change the normal TabControls to FlatTabControls. All the properties remain unchanged. You can play with the backcolor property and icons to get the look that you are looking for:

C#
/// <SUMMARY>
/// Summary description for Form1.
/// </SUMMARY>
public class Form1 : System.Windows.Forms.Form
{
  private FlatTabControl.FlatTabControl tabControl1;
  
  ...
    
  #region Windows Form Designer generated code
  /// <SUMMARY>
  /// Required method for Designer support - do not modify
  /// the contents of this method with the code editor.
  /// </SUMMARY>
  private void InitializeComponent()
  {
  
  ...
    
    this.tabControl1 = new FlatTabControl.FlatTabControl();
  ...
    
  }
  #endregion

References and credits

Please see the following useful resources:

History

  • 7th Nov, 2005: Version 1.0
  • 5th Dec, 2005: Version 1.1
    • myBackColor property added.

Note

Make your comments, corrections or requirements for credits. Your feedback is most welcome.

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication


Written By
Software Developer (Senior) Kinecor Ltee
Canada Canada
I have been working for 16 years as Analyst Programmer in several companies.

I love the Object Oriented Programming paradigm and now, I love C#. Currently, I works with X++ in Microsoft Dynamics AX systems.

Also, I like to perform my work by using methodologies like Rational Unified Process (RUP).

Please, take a look to my last project: Meetgate

Comments and Discussions

 
GeneralRe: tab strip Pin
Thiago Azevedo Falcao14-May-08 10:14
Thiago Azevedo Falcao14-May-08 10:14 
Generalgraet control.....but its flickering..... Pin
Donimoni18-Jun-06 1:28
Donimoni18-Jun-06 1:28 
GeneralRe: graet control.....but its flickering..... Pin
Oscar Londono30-Jun-06 4:22
Oscar Londono30-Jun-06 4:22 
QuestionRe: graet control.....but its flickering..... Pin
rkleahey28-Jul-06 10:49
rkleahey28-Jul-06 10:49 
AnswerRe: graet control.....but its flickering..... Pin
cpparasite29-Apr-09 19:46
cpparasite29-Apr-09 19:46 
GeneralRe: graet control.....but its flickering..... Pin
Thiago Azevedo Falcao14-May-08 9:45
Thiago Azevedo Falcao14-May-08 9:45 
QuestionVertical align Pin
Astagi18-Apr-06 6:04
Astagi18-Apr-06 6:04 
AnswerRe: Vertical align Pin
rwakelan3-May-06 9:06
rwakelan3-May-06 9:06 
ok, this requires a few changes (lets see if i can remember them all)

Change #1: change the clip area to TabControlArea

rreg = ClientRectangle;

This makes it so it will update the correct area when it draws. It updates the entire control, but in my test it did not cause a visible flicker or anything. More precise measurements can be used (similar to what is provided for top and bottom.

Change #2: Add two more else ifs to the block that creates the point array

else if (Alignment == TabAlignment.Right)<br />
            {<br />
                pt[0] = new Point(recBounds.Left, recBounds.Top);<br />
                pt[1] = new Point(recBounds.Left, recBounds.Bottom);<br />
                pt[2] = new Point(recBounds.Right - 3, recBounds.Bottom);<br />
                pt[3] = new Point(recBounds.Right, recBounds.Bottom - 3);<br />
                pt[4] = new Point(recBounds.Right, recBounds.Top + 3);<br />
                pt[5] = new Point(recBounds.Right - 3, recBounds.Top);<br />
                pt[6] = new Point(recBounds.Left, recBounds.Top);<br />
            }<br />
            else if (Alignment == TabAlignment.Left)<br />
            {<br />
                pt[0] = new Point(recBounds.Right, recBounds.Top);<br />
                pt[1] = new Point(recBounds.Right, recBounds.Bottom);<br />
                pt[2] = new Point(recBounds.Left + 3, recBounds.Bottom);<br />
                pt[3] = new Point(recBounds.Left, recBounds.Bottom - 3);<br />
                pt[4] = new Point(recBounds.Left, recBounds.Top + 3);<br />
                pt[5] = new Point(recBounds.Left + 3, recBounds.Top);<br />
                pt[6] = new Point(recBounds.Right, recBounds.Top);<br />
            }


This creates the array to draw the shapes for right and left

Change #3: draw the lines to connect the tab to the tab page

case TabAlignment.Right:<br />
                        g.DrawLine(pen, recBounds.Left, recBounds.Top + 1, recBounds.Left, recBounds.Bottom - 1);<br />
                        g.DrawLine(pen, recBounds.Left - 1, recBounds.Top + 1, recBounds.Left - 1, recBounds.Bottom - 1);<br />
                        g.DrawLine(pen, recBounds.Left - 2, recBounds.Top + 1, recBounds.Left - 2, recBounds.Bottom - 1);<br />
                        g.DrawLine(pen, recBounds.Left - 3, recBounds.Top + 1, recBounds.Left - 3, recBounds.Bottom - 1);<br />
                        break;<br />
                    case TabAlignment.Left:<br />
                        g.DrawLine(pen, recBounds.Right, recBounds.Top + 1, recBounds.Right, recBounds.Bottom - 1);<br />
                        g.DrawLine(pen, recBounds.Right + 1, recBounds.Top + 1, recBounds.Right + 1, recBounds.Bottom - 1);<br />
                        g.DrawLine(pen, recBounds.Right + 2, recBounds.Top + 1, recBounds.Right + 2, recBounds.Bottom - 1);<br />
                        g.DrawLine(pen, recBounds.Right + 3, recBounds.Top + 1, recBounds.Right + 3, recBounds.Bottom - 1);<br />
                        break;


Change #4: rotate the icon (you don't want lazy icons like Visual Studio, right? Big Grin | :-D )

// adjust rectangles<br />
                if (Alignment == TabAlignment.Top || Alignment == TabAlignment.Bottom)<br />
                {<br />
                    nAdj = (float)(nLeftMargin + img.Width + nRightMargin);<br />
<br />
                    rimage.Y += (recBounds.Height - img.Height) / 2;<br />
                    tabTextArea.X += nAdj;<br />
                    tabTextArea.Width -= nAdj;<br />
                }<br />
                else<br />
                {<br />
                    img.RotateFlip(RotateFlipType.Rotate90FlipNone);<br />
                    //rimage.X += (recBounds.Width - img.Width) / 2;<br />
                    rimage.X -= 5;<br />
                    rimage.Y += 3;<br />
<br />
                    nAdj = (float)(10 + img.Height);<br />
                    tabTextArea.Y += img.Height;<br />
                    tabTextArea.Height -= img.Height;<br />
                }


This just flips the icon and moves it correctly depending on location

Change #5: rotate the text
if (Dock == DockStyle.Right || Dock == DockStyle.Left)<br />
            {<br />
                stringFormat.FormatFlags = StringFormatFlags.DirectionVertical;<br />
            }


just stick that after the other stringFormat assignments.

That should work. I might have forgotten something (it's hard to remember what you changed when your control is 1500 lines of code :S) If I forgot something and it broke the control for some reason, let me know and I'll figure out what I forgot Smile | :)
GeneralRe: Vertical align Pin
Jeevan Nagvekar29-Jan-07 6:30
Jeevan Nagvekar29-Jan-07 6:30 
GeneralRe: Vertical align Pin
Michael Vasquez19-Dec-07 10:56
Michael Vasquez19-Dec-07 10:56 
GeneralRe: Vertical align Pin
Michael Vasquez19-Dec-07 11:23
Michael Vasquez19-Dec-07 11:23 
Questionhow tab page change on clicking left and right navigation button Pin
chatanya2k42-Apr-06 20:59
chatanya2k42-Apr-06 20:59 
AnswerRe: how tab page change on clicking left and right navigation button Pin
Oscar Londono7-Apr-06 3:27
Oscar Londono7-Apr-06 3:27 
Generaladding close button Pin
chatanya2k42-Apr-06 20:56
chatanya2k42-Apr-06 20:56 
GeneralRe: adding close button Pin
rwakelan24-Apr-06 8:01
rwakelan24-Apr-06 8:01 
GeneralRe: adding close button Pin
chatanya2k424-Apr-06 19:58
chatanya2k424-Apr-06 19:58 
GeneralRe: adding close button Pin
rwakelan25-Apr-06 3:06
rwakelan25-Apr-06 3:06 
GeneralRe: adding close button Pin
Johnny J.28-Sep-10 22:19
professionalJohnny J.28-Sep-10 22:19 
GeneralRe: adding close button Pin
Member 1103683426-Aug-14 2:12
Member 1103683426-Aug-14 2:12 
GeneralRe: adding close button Pin
Member 1103683426-Aug-14 6:59
Member 1103683426-Aug-14 6:59 
QuestionHow to start CustomDraw projects Pin
miamiman2111-Jan-06 4:15
miamiman2111-Jan-06 4:15 
AnswerRe: How to start CustomDraw projects Pin
Oscar Londono14-Jan-06 4:29
Oscar Londono14-Jan-06 4:29 
QuestionTab Size does'nt work Pin
MinWanKim11-Jan-06 0:27
MinWanKim11-Jan-06 0:27 
AnswerRe: Tab Size does'nt work Pin
Oscar Londono14-Jan-06 4:25
Oscar Londono14-Jan-06 4:25 
GeneralRe: Tab Size does'nt work Pin
lityk@yahoo.com2-Aug-06 6:13
lityk@yahoo.com2-Aug-06 6:13 

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.