在基本的TabControl控件使用和功能之上,可以尝试对其进行美化和功能扩展,比如动态删除或添加tab、绘制图标按钮及鼠标hover时的背景变化、Tab从右向左布局的优化处理等。最重要的是推荐参考花木兰控件库中对TabControl美化,现代UI效果的tab,绝对值得一看或者使用。
实现动态增加或删除tab的功能(添加和关闭按钮)
添加、关闭按钮图标
通过ImageList或Resources添加“加号”和“关闭”图片作为全局变量,也可以指定这两个图片的路径为全局变量。
此处使用imageList。
设置DrawMode为OwnerDrawFixed
tabControl1.DrawMode = TabDrawMode.OwnerDrawFixed;
DrawItem中绘制添加、关闭按钮
tabControl1.DrawItem += TabControl1_DrawItem2;
//.....
// 绘制add和close按钮
private void TabControl1_DrawItem2(object sender, DrawItemEventArgs e)
{
var tabPage = tabControl1.TabPages[e.Index];
var tabRect = tabControl1.GetTabRect(e.Index);
// tabRect.Inflate(0, -2); // 似乎未起作用
//e.DrawBackground(); // 背景
if (e.Index == tabControl1.TabCount - 1) // 最后一个TabPage
{
var addImage = imageList1.Images["add"]; // 也可以从路径获取new Bitmap(imagePath);
e.Graphics.DrawImage(addImage,
tabRect.Left + (tabRect.Width - addImage.Width) / 2,
tabRect.Top + (tabRect.Height - addImage.Height) / 2);
}
else // 其他TabPages绘制关闭
{
var closeImage = imageList1.Images["close"];
e.Graphics.DrawImage(closeImage,
(tabRect.Right - closeImage.Width - 2),
tabRect.Top + (tabRect.Height - closeImage.Height) / 2);
TextRenderer.DrawText(e.Graphics, tabPage.Text, tabPage.Font,
new Rectangle(tabRect.X, tabRect.Y, tabRect.Width-closeImage.Width, tabRect.Height), tabPage.ForeColor, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
}
}
设置最后一个“添加”按钮的tab尽可能短
通过SendMessage
使最后一个“添加”按钮的tab比较短。
实际测试要想此方法生效,TabControl.SizeMode
不能为 Fixed
。
// 创建句柄时触发。通过发送消息SendMessage使绘制的(每个)tab尽可能短。SizeMode 不能为 Fixed
tabControl1.HandleCreated += TabControl1_HandleCreated;
//.....
[DllImport("user32.dll")]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
private const int TCM_SETMINTABWIDTH = 0x1300 + 49;
private void TabControl1_HandleCreated(object sender, EventArgs e)
{
SendMessage(tabControl1.Handle, TCM_SETMINTABWIDTH, IntPtr.Zero, (IntPtr)16);
}
可以看到,在使用了非Fixed
的SizeMode
后,由于tab的宽度设置无效,导致默认的宽度仅可以显示文字。在重新绘制时,原本的文字范围内添加了close图片,占用了16px的范围(close图片的大小为16*16),剩余的范围无法一行放下文字,导致了tab文字的换行。
比较好的解决办法就是添加一个padding.X
的值,最好为16px,正好可以放下绘制close图片。
效果如下:
实际可以自行调整padding.X
的值,未必需要指定16,比如13、12都可以正常一行放下文字。
设置鼠标点击关闭和添加按钮时的处理
处理最后一个tab(即"添加"按钮的tab)点击时,动态添加tabpage;点击tab的关闭按钮时关闭移除当前tab。
在MouseDown事件中,获取鼠标按下时的位置,并依次判断是否点击在close按钮上或最后一个“添加”按钮的tab上,依据判断结果执行添加和关闭操作。
tabControl1.MouseDown += TabControl1_MouseDown;
//.....
private void TabControl1_MouseDown(object sender, MouseEventArgs e)
{
// 依次循环判断,鼠标点击位置是否位于close图片范围内;或是否位于“添加”按钮tab内
for (var i = 0; i < tabControl1.TabPages.Count; i++)
{
var tabRect = tabControl1.GetTabRect(i);
if (i == tabControl1.TabPages.Count - 1) // 组后一个 add 按钮
{
if (tabRect.Contains(e.Location))
{
CreateTabPage();
}
}
else
{
var closeImage = imageList1.Images["close"];
var imageRect = new Rectangle(
tabRect.Right - closeImage.Width - 2,
tabRect.Top + (tabRect.Height - closeImage.Height) / 2,
closeImage.Width,
closeImage.Height);
if (imageRect.Contains(e.Location))
{
tabControl1.TabPages.RemoveAt(i);
break;
}
}
}
}
创建tabpage的方法CreateTabPage
(以实际情况实现)
private void CreateTabPage()
{
//insert会导致DrawItem中异常(索引错误)
//tabControl1.TabPages.Insert(tabControl1.TabPages.Count - 1,"新选项卡"+(tabControl1.TabPages.Count - 1));
tabControl1.TabPages.Add("新选项卡" + (tabControl1.TabPages.Count - 1));
var addPage = tabControl1.TabPages["add"];
tabControl1.TabPages.Remove(addPage);
tabControl1.TabPages.Add(addPage);
}
鼠标位于关闭按钮上方时背景变化效果
依据MouseDown处理中判断鼠标位置的方法,通过在MouseMove判断鼠标位置,可以绘制鼠标位于关闭按钮上方时,“关闭”背景相应变化(比如变灰)。
tabControl1.MouseMove += TabControl1_MouseMove;
//.......
/// <summary>
/// 鼠标Hover关闭按钮效果
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void TabControl1_MouseMove(object sender, MouseEventArgs e)
{
// 依次循环判断,鼠标点击位置是否位于close图片范围内
for (var i = 0; i < tabControl1.TabPages.Count - 1; i++)
{
var tabPage = tabControl1.TabPages[i];
var tabRect = tabControl1.GetTabRect(i);
//tabRect.Inflate(0, -2); // 似乎未起作用
var closeImage = imageList1.Images["close"];
var imageRect = new Rectangle(
tabRect.Right - closeImage.Width - 2,
tabRect.Top + (tabRect.Height - closeImage.Height) / 2,
closeImage.Width,
closeImage.Height);
if (imageRect.Contains(e.Location))
{
if (tabPage.Tag?.ToString() != "1")
{
using (var g = tabControl1.CreateGraphics())
{
g.FillRectangle(new SolidBrush(Color.FromArgb(100, 100, 100, 45)), imageRect);
tabPage.Tag = "1";//表示已有透明灰色背景
}
}
break;
}
else
{
if (tabPage.Tag?.ToString() == "1")//清除已有的灰色背景
{
using (var g = tabControl1.CreateGraphics())
{
g.FillRectangle(new SolidBrush(tabPage.BackColor), imageRect);
g.DrawImage(closeImage, imageRect);
tabPage.Tag = null;
}
}
}
}
}
效果如下,分别查看点击添加、鼠标悬停时、点击关闭按钮的效果:
设置RightToLeftLayout = true
和RightToLeft = true
时tab选项卡的绘制优化
上面的实现,如果设置属性RightToLeftLayout = true
和RightToLeft = true
时,将会出现选项卡文字和关闭图标分离混乱的问题。
因此需要优化RightToLeft模式下的tab绘制。
RightToLeft的坐标转换(Rectangle的坐标)可以从原始坐标通过下面的函数,从容器中矩形的坐标转换为RTL坐标:
public static Rectangle GetRTLCoordinates(Rectangle container, Rectangle drawRectangle)
{
return new Rectangle(
container.Right - drawRectangle.Width - drawRectangle.X,
drawRectangle.Y,
drawRectangle.Width,
drawRectangle.Height);
}
因此,扩展下获取TabRect的方法如下:
/// <summary>
/// 绘制tab时需要考虑RTF模式
/// </summary>
/// <param name="tabCtl"></param>
/// <param name="idx"></param>
/// <returns></returns>
public static Rectangle GetTabRect(TabControl tabCtl, int idx)
{
var tabRect = tabCtl.GetTabRect(idx);
if (tabCtl.RightToLeftLayout && tabCtl.RightToLeft == RightToLeft.Yes) // RTL
{
tabRect = GetRTLCoordinates(tabCtl.ClientRectangle, tabRect);
}
return tabRect;
}
通过GetTabRect
获取正确的tab的矩形区域。
DrawItem事件方法对应修改为如下:
private void TabControl1_DrawItem2(object sender, DrawItemEventArgs e)
{
var tabPage = tabControl1.TabPages[e.Index];
var tabRect = GetTabRect(tabControl1,e.Index);
//tabRect.Inflate(0, -2);
//e.DrawBackground(); // 背景
if (e.Index == tabControl1.TabCount - 1) // 最后一个TabPage
{
var addImage = imageList1.Images["add"]; // 也可以从路径获取new Bitmap(imagePath);
e.Graphics.DrawImage(addImage,
tabRect.Left + (tabRect.Width - addImage.Width) / 2,
tabRect.Top + (tabRect.Height - addImage.Height) / 2);
}
else // 其他TabPages绘制关闭
{
using (var sf = new StringFormat(StringFormat.GenericDefault))
{
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Center;
if (tabControl1.RightToLeft == RightToLeft.Yes && tabControl1.RightToLeftLayout == true) // RTL模式
{
sf.FormatFlags |= StringFormatFlags.DirectionRightToLeft;
}
var closeImage = imageList1.Images["close"];
var imgRect = new Rectangle(//tabRect.Right - closeImage.Width - 2,
tabRect.Right - closeImage.Width - (tabControl1.RightToLeftLayout && tabControl1.RightToLeft == RightToLeft.Yes ? 4 : 2),
tabRect.Top + (tabRect.Height - closeImage.Height) / 2, closeImage.Width, closeImage.Height);
e.Graphics.DrawImage(closeImage, imgRect.Location);
var textRect = new Rectangle(tabRect.X, tabRect.Y, tabRect.Width - closeImage.Width, tabRect.Height);
e.Graphics.DrawString(tabPage.Text, tabPage.Font, new SolidBrush(tabPage.ForeColor), textRect, sf);
}
}
}
MouseMove和MouseDown事件处理中,tabRect获取方式也改为此方法:var tabRect = GetTabRect(tabControl1, i);
。
同时注意,判断鼠标位置时,e.Location
在RTF下也要进行转换,才能获取正确的位置:
var mousePos = e.Location;
if (tabControl1.RightToLeftLayout && tabControl1.RightToLeft == RightToLeft.Yes) // RTL调整鼠标位置
{
mousePos.X = tabControl1.Right - mousePos.X;
}
此部分主要参考自 从右到左TabControl的TabPage的关闭按钮C#
TabControl的[终极]美化扩展
关于TabControl的[终极]美化扩展,可以参考花木兰控件库的实现,该控件库通过重写 OnPaint
、OnMouseClick
方法,实现全部的绘制优化和美化。
源码参考TabControlExt.cs
文章介绍 TabControl美化扩展----------WinForm控件开发系列
参考
- C#: WinForm TabControl with Add and Close Button
- Close button for TabPages of Right To Left TabControl C#: https://stackoverflow.com/questions/34508176/close-button-for-tabpages-of-right-to-left-tabcontrol-c-sharp/34509304#34509304
以上的优化没有进行继承TabControl控件,重写重绘方法,如果想要继承优化的,重写OnMouseClick
时注意判断鼠标左右中键的处理、点击时选中tab的处理的,可简要参考下c#重写TabControl控件实现关闭按钮
另,TabControl控件的美化介绍的优化,源代码实在有点长,有兴趣可以了解下