Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

A .NET Flat TabControl (CustomDraw)

4.71/5 (51 votes)
5 Dec 2005Public Domain3 min read 4   22.9K  
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