Winform控件优化之TabControl控件的美化和功能扩展

简介: 在基本的TabControl控件使用和功能之上,可以尝试对其进行美化和功能扩展,比如动态删除或添加tab、绘制图标按钮及鼠标hover时的背景变化、Tab从右向左布局的优化处理等。最重要...

在基本的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);
}

可以看到,在使用了非FixedSizeMode后,由于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 = trueRightToLeft = true时tab选项卡的绘制优化

上面的实现,如果设置属性RightToLeftLayout = trueRightToLeft = 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的[终极]美化扩展,可以参考花木兰控件库的实现,该控件库通过重写 OnPaintOnMouseClick 方法,实现全部的绘制优化和美化。

文章介绍 TabControl美化扩展----------WinForm控件开发系列

参考

以上的优化没有进行继承TabControl控件,重写重绘方法,如果想要继承优化的,重写OnMouseClick时注意判断鼠标左右中键的处理、点击时选中tab的处理的,可简要参考下c#重写TabControl控件实现关闭按钮

另,TabControl控件的美化介绍的优化,源代码实在有点长,有兴趣可以了解下

相关文章
|
C# 索引 Windows
Winform控件优化之TabControl控件的使用和常用功能
TabControl是一个分页切换(tab)控件,不同的页框内可以呈现不同的内容,将主要介绍调整tab的左右侧显示、设置多行tab、禁用或删除tabpage、隐藏TabControl头部的选项卡等
8178 0
Winform控件优化之TabControl控件的使用和常用功能
|
前端开发 API C#
C#使用外部字体、嵌入字体到程序资源中(Winform)及字体的版权问题
应用程序能够使用一个好的字体,是用户界面很重要的一部分,但是很多字体如果系统没有安装,则需要额外引入,这就涉及到极其重要的字体版权问题,及额外字体的使用和安装。最好的方式应该是将字体嵌入到程序中...
5888 1
C#使用外部字体、嵌入字体到程序资源中(Winform)及字体的版权问题
Winform中Textbox、NumericUpDown等修改高度,禁止输入数字或内容的实现
Winform中的Textbox、NumericUpDown控件通常在单行的情况下,无法直接通过`Height`属性修改高度,但很多时候我们需要调整其高度,使其显示的更加合理,主要介绍三种方法...
3698 0
|
API C# Windows
Winform控件优化之无边框窗体及其拖动、调整大小和实现最大最小化关闭功能的自定义标题栏效果
Winform中实现无边框窗体只需要设置FormBorderStyle = FormBorderStyle.None,但是无边框下我们需要保留移动窗体、拖拽调整大小、自定义美观好看的标题栏等...
5568 0
Winform控件优化之无边框窗体及其拖动、调整大小和实现最大最小化关闭功能的自定义标题栏效果
|
Windows
Winform控件优化之背景透明那些事1:Button控件等背景透明
WinForm不支持真正的透明,其控件透明的实现都是背景颜色设置和对应位置的父控件背景相同。 Winform中控件的背景透明只有三种:Button控件的透明、其他控件的透明...
3804 0
Winform控件优化之背景透明那些事1:Button控件等背景透明
|
C# 图形学 Windows
Winform控件优化之背景透明那些事2:窗体背景透明、镂空穿透、SetStyle、GDI透明效果等
两行代码就能实现Form窗体的(背景)透明效果,它不是Opacity属性的整个窗体透明,`TransparencyKey`实现窗体的透明、窗体中间部分镂空效果...
4609 0
Winform控件优化之背景透明那些事2:窗体背景透明、镂空穿透、SetStyle、GDI透明效果等
|
C# Windows 容器
C#或Winform中的消息通知之系统托盘的气泡提示窗口(系统toast通知)、ToolTip控件和ToolTipText属性
NotifyIcon控件表示系统右下角任务栏上的托盘图标,其ShowBalloonTip方法用于显示气球状提示框(Win10只有为本地Toast通知),ToolTip\oolTipText可以...
3471 0
C#或Winform中的消息通知之系统托盘的气泡提示窗口(系统toast通知)、ToolTip控件和ToolTipText属性
|
人工智能 API Apache
推荐3款开源、美观且免费的WinForm UI控件库
推荐3款开源、美观且免费的WinForm UI控件库
2645 6
|
开发框架 前端开发 JavaScript
在Winform界面使用自定义用户控件及TabelPanel和StackPanel布局控件
在Winform界面使用自定义用户控件及TabelPanel和StackPanel布局控件
|
算法 API C#
Winform控件优化之圆角按钮【各种实现中的推荐做法】(下)
最终优化实现ButtonPro按钮(继承自Button),既提供Button原生功能,又提供扩展功能,除了圆角以外,还实现了圆形、圆角矩形的脚尖效果、边框大小和颜色、背景渐变颜色...
3695 0
Winform控件优化之圆角按钮【各种实现中的推荐做法】(下)