×

A .NET Flat TabControl (CustomDraw)

Kalet Kalet 发表于2009-03-20 12:00:14 浏览429 评论0

抢沙发发表评论

A .NET Flat TabControl (CustomDraw)


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:



  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!).

Collapse

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.


Collapse

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:


Collapse

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();A .NET Flat TabControl (CustomDraw)

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:


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).


Collapse

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:


Collapse

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:


/// <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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.


A list of licenses authors might use can be found here


About the Author






Oscar Londoño



I have been working for 13 years as Analyst Programmer in several companies.

I love the Object Oriented Programming paradigm and now, I love C#.

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

Please, take a look to my last project: Meetgate







A .NET Flat TabControl (CustomDraw)
Occupation: Web Developer
Location: Colombia Colombia


群贤毕至

访客