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 方法,实现全部的绘制优化和美化。

源码参考TabControlExt.cs

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

参考

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

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

相关文章
|
C# 索引 Windows
Winform控件优化之TabControl控件的使用和常用功能
TabControl是一个分页切换(tab)控件,不同的页框内可以呈现不同的内容,将主要介绍调整tab的左右侧显示、设置多行tab、禁用或删除tabpage、隐藏TabControl头部的选项卡等
4946 0
Winform控件优化之TabControl控件的使用和常用功能
|
算法 API C#
Winform控件优化之圆角按钮【各种实现中的推荐做法】(下)
最终优化实现ButtonPro按钮(继承自Button),既提供Button原生功能,又提供扩展功能,除了圆角以外,还实现了圆形、圆角矩形的脚尖效果、边框大小和颜色、背景渐变颜色...
1947 0
Winform控件优化之圆角按钮【各种实现中的推荐做法】(下)
|
C# 图形学 Windows
Winform控件优化之圆角按钮【各种实现中的推荐做法】(上)
Windows 11下所有控件已经默认采用圆角,其效果更好、相对有着更好的优化...尝试介绍很常见的圆角效果,通过重写控件的OnPaint方法实现绘制,并在后面进一步探索对应的优化和可能的问题
1414 0
Winform控件优化之圆角按钮【各种实现中的推荐做法】(上)
|
XML 程序员 C语言
Qt编写控件属性设计器2-拖曳控件
一、前言 上一篇文章把插件加载好了,并且把插件中的所有控件都显示到了列表框中,这次要做的就是实现拖曳控件的功能,用户选择一个控件拖曳到画布上,松开,在松开位置处自动实例化该控件,这个需要用到dropEvent和dragEnterEvent事件,重新实现这两个事件,对拖曳的对象进行过滤并调用函数实例化该控件,在实例化该控件的同时实例化控件跟随控件以便拉伸调整大小和位置。
916 0
|
开发工具 C语言
Qt编写自定义控件9-导航按钮控件
一、前言 导航按钮控件,主要用于各种漂亮精美的导航条,我们经常在web中看到导航条都非常精美,都是html+css+js实现的,还自带动画过度效果,Qt提供的qss其实也是无敌的,支持基本上所有的CSS2属性,配合QPainter这个无敌大法工具,没有什么不能绘制的。
1275 0
|
C# 数据格式 算法
C# WPF 歌词控件(支持逐字定位描色效果)
原文:C# WPF 歌词控件(支持逐字定位描色效果) 之前做了一个模仿网易云歌词的控件,实现了加载网易云歌词并能随音乐播放进度定位歌词。今天呢将在这个控件的基础上增加逐字定位描色功能,如下图效果(QQ音乐PC)所示:   我所使用的实现方法很简单粗暴,把每句歌词每个字切开,单独显示在一个描色的控件中,然后拼成一行完整的歌词,随音乐播放进度去找相应的字进行描色。
1407 0
|
C#
WPF 控件库——轮播控件
原文:WPF 控件库——轮播控件 一、要做成什么样   bs端的轮播控件千千万,有的甚至能作为一个单独的库来开发,所涉及到的功能也是缤纷多彩。相对来说,cs端的轮播用得不多,我这里只是简单的做了个能满足一般需求的轮播,在项目中凑会凑会还是可以的。
1898 0
|
C#
WPF 4 DataGrid 控件(基本功能篇)
原文:WPF 4 DataGrid 控件(基本功能篇)      提到DataGrid 不管是网页还是应用程序开发都会频繁使用。通过它我们可以灵活的在行与列间显示各种数据。本篇将详细介绍WPF 4 中DataGrid 的相关功能。
1559 0
|
C# C++ Windows
WPF中不规则窗体与WindowsFormsHost控件的兼容问题完美解决方案
原文:WPF中不规则窗体与WindowsFormsHost控件的兼容问题完美解决方案          首先先得瑟一下,有关WPF中不规则窗体与WindowsFormsHost控件不兼容的问题,网上给出的解决方案不能满足所有的情况,是有特定条件的,比如  WPF中不规则窗体与WebBrowser控件的兼容问题解决办法。
1313 0