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:
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:
- Fill the client area.
- Draw the border.
- Clip the region for drawing tabs, including the Up-Down buttons if they are visible (see below for Up-Down buttons' subclassing).
- Draw each tab page.
- Cover other areas by drawing lines near the borders (tip!).
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;
Brush br = new SolidBrush(SystemColors.Control);
g.FillRectangle(br, TabControlArea);
br.Dispose();
int nDelta = SystemInformation.Border3DSize.Width;
Pen border = new Pen(SystemColors.ControlDark);
TabArea.Inflate(nDelta, nDelta);
g.DrawRectangle(border, TabArea);
border.Dispose();
Region rsaved = g.Clip;
Rectangle rreg;
int nWidth = TabArea.Width + nMargin;
if (bUpDown)
{
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);
for (int i = 0; i < this.TabCount; i++)
DrawTab(g, this.TabPages[i], i);
g.Clip = rsaved;
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.
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);
}
Brush br = new SolidBrush(tabPage.BackColor);
g.FillPolygon(br, pt);
br.Dispose();
g.DrawPolygon(SystemPens.ControlDark, pt);
if (bSelected)
{
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();
}
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);
float nAdj = (float)(nLeftMargin + img.Width + nRightMargin);
rimage.Y += (recBounds.Height - img.Height) / 2;
tabTextArea.X += nAdj;
tabTextArea.Width -= nAdj;
g.DrawImage(img, rimage);
}
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:
internal void DrawIcons(Graphics g)
{
if ((leftRightImages == null) ||
(leftRightImages.Images.Count != 4))
return;
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);
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
:
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).
private void FindUpDown()
{
bool bFound = false;
IntPtr pWnd =
Win32.GetWindow(this.Handle, Win32.GW_CHILD);
while (pWnd != IntPtr.Zero)
{
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)
{
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:
private int scUpDown_SubClassedWndProc(ref Message m)
{
switch (m.Msg)
{
case Win32.WM_PAINT:
{
IntPtr hDC = Win32.GetWindowDC(scUpDown.Handle);
Graphics g = Graphics.FromHdc(hDC);
DrawIcons(g);
g.Dispose();
Win32.ReleaseDC(scUpDown.Handle, hDC);
m.Result = IntPtr.Zero;
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 TabControl
s to FlatTabControl
s. All the properties remain unchanged. You can play with the backcolor
property and icons to get the look that you are looking for:
public class Form1 : System.Windows.Forms.Form
{
private FlatTabControl.FlatTabControl tabControl1;
...
#region Windows Form Designer generated code
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.