Click here to Skip to main content
16,015,583 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 452.8K   22.9K   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

 
QuestionRE. Flat TabControl (CustomDraw) Pin
Member 1303204721-May-20 8:29
Member 1303204721-May-20 8:29 
Questiontab title not working for big font size. Pin
SachinSutar16-Apr-18 16:43
SachinSutar16-Apr-18 16:43 
QuestionHello, I want to use alignment for the tabs and rotate the text Pin
Eng-ahmed Ismail9-Mar-18 4:31
Eng-ahmed Ismail9-Mar-18 4:31 
QuestionRTL Not supported Pin
Member 1179420229-Jun-15 12:33
Member 1179420229-Jun-15 12:33 
QuestionControls move off the page or become size 0,0 Pin
Mickey Marshall24-Jun-15 12:28
Mickey Marshall24-Jun-15 12:28 
AnswerRe: Controls move off the page or become size 0,0 Pin
graysuit12-Sep-22 23:29
graysuit12-Sep-22 23:29 
QuestionLicense Pin
MSD211-Feb-15 1:12
MSD211-Feb-15 1:12 
AnswerRe: License Pin
Oscar Londono11-Feb-15 3:01
Oscar Londono11-Feb-15 3:01 
Generalamazing work! Pin
lzp72924-Sep-14 8:37
lzp72924-Sep-14 8:37 
GeneralRe: amazing work! Pin
lzp72924-Sep-14 10:17
lzp72924-Sep-14 10:17 
QuestionLeft Right Images Size Pin
schuster19uk5-Sep-12 2:57
schuster19uk5-Sep-12 2:57 
BugProblem when used in MDI Child forms Pin
Francisco Freitas15-Jan-12 23:09
Francisco Freitas15-Jan-12 23:09 
QuestionCross-threaded operations Pin
Dennis Babyak3-Mar-10 1:48
Dennis Babyak3-Mar-10 1:48 
AnswerRe: Cross-threaded operations Pin
Dennis Babyak3-Mar-10 4:08
Dennis Babyak3-Mar-10 4:08 
GeneralThanks! Pin
Lee_Lee13-Oct-09 1:39
Lee_Lee13-Oct-09 1:39 
QuestionSpace between tabs Pin
Member 39415309-Oct-08 11:19
Member 39415309-Oct-08 11:19 
Questiontab header's size changed Pin
netnoease10-Sep-08 23:25
netnoease10-Sep-08 23:25 
GeneralResize Pin
hell-citizen20-Aug-08 3:03
hell-citizen20-Aug-08 3:03 
GeneralProblem Pin
SoundAddicted30-Apr-08 4:51
SoundAddicted30-Apr-08 4:51 
QuestionRTL Support Pin
Nader Al Khatib26-Oct-07 16:31
Nader Al Khatib26-Oct-07 16:31 
QuestionMultiline support? Pin
Notre20-Aug-07 10:49
Notre20-Aug-07 10:49 
GeneralAmazing Pin
Dev1234567893-Aug-07 4:15
Dev1234567893-Aug-07 4:15 
Generalcollection editor Pin
Zahrashahla15-Jun-07 21:41
Zahrashahla15-Jun-07 21:41 
GeneralRe: collection editor Pin
surfang10-May-08 14:44
surfang10-May-08 14:44 
GeneralI try to Make Tab Heading bold Pin
qumer10126-May-07 9:37
qumer10126-May-07 9:37 

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.