一个 .NET 平面 TabControl (CustomDraw)






4.71/5 (49投票s)
这是一个 CustomDraw TabControl,外观扁平,支持图标,并填充 backcolor 属性。
引言
Visual Studio 中包含的 TabControl
不支持 flat
属性,所以我决定构建自己的控件。 我在互联网上搜索了类似的东西,但找不到任何满足我需求的资源。
好了,这是控件,它看起来是扁平的,支持图标,并用 backcolor
属性填充。
背景
首先,我们需要双缓冲技术来改善绘制并允许控件改变其外观
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);
然后,您需要覆盖 OnPaint
事件并绘制您自己的控件。 涉及的基本步骤是
- 填充客户端区域。
- 绘制边框。
- 裁剪用于绘制选项卡的区域,包括 Up-Down 按钮(如果可见)(有关 Up-Down 按钮的子类化,请参见下文)。
- 绘制每个选项卡页面。
- 通过在边框附近绘制线条来覆盖其他区域(提示!)。
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();
}
//----------------------------
}
DrawTab
方法使用多边形绘制边框。 它还绘制选项卡页面的文本和图标。 我只实现了对顶部和底部的对齐,因为对左侧和右侧的对齐更加复杂(有人可以尝试!),并且还有其他文章解释了这种行为,例如 .NET 样式的侧面选项卡控件 作者是 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);
}
//----------------------------
// 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);
//----------------------------
}
为了绘制 UpDown 按钮,我使用了 DrawIcons
方法。 它使用包含四个按钮(左、右、禁用左、禁用右)的 leftRightImages
ImageList
,并在通过子类化类接收到 WM_PAINT
消息时调用它,如下所述
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);
}
}
}
//----------------------------
}
关注点
好了,这是绘制 UpDown 按钮的技巧(这是最困难的任务)。
首先,我需要知道何时应该绘制它们。 可以通过处理三个事件来实现:OnCreateControl
、ControlAdded
和 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();
}
函数 FindUpDown
通过使用 Win32 GetWindow
并查找 TabControl
的子窗口来查找类 msctls_updown32
(来自 Oleg Lobach 的 完全所有者绘制的选项卡控件 的一个了不起的提示)
如果我们找到该类,我们可以对其进行子类化以处理消息 WM_PAINT
(有关子类化的更多信息,请参阅 Sameers 的 .NET 中的子类化 - 纯 .NET 方法 和 Tomas Brennan 的 黑掉组合框使其具有水平滚动条)。
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);
}
}
}
这是 WndProc
的子类化函数。 我们处理 WM_PAINT
以绘制图标并验证客户端区域
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;
}
使用代码
要使用该代码,只需添加对 FlatTabControl
的引用,并将普通的 TabControl
更改为 FlatTabControl
。 所有属性保持不变。 您可以尝试使用 backcolor
属性和图标来获得您想要的外观
/// <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
参考和致谢
请参阅以下有用资源
- Mick Dohertys DotNet Tips - TabControl.
- 在 Visual Studio .NET 中使用资源 作者:Dejan Jelovic
- 启动选项卡控件 作者:Özcan Deðirmenci
- 完全所有者绘制的选项卡控件 作者:Oleg Lobach
- .NET 中的子类化 - 纯 .NET 方法 作者:Sameers
- 黑掉组合框使其具有水平滚动条 作者:Tomas Brennan
历史
- 2005 年 11 月 7 日:1.0 版本
- 2005 年 12 月 5 日:1.1 版本
- 添加了
myBackColor
属性。
- 添加了
注意
请发表您的评论、更正或学分要求。 欢迎您的反馈.