在基本的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();
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;
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控件开发系列
参考
以上的优化没有进行继承TabControl控件,重写重绘方法,如果想要继承优化的,重写
OnMouseClick
时注意判断鼠标左右中键的处理、点击时选中tab的处理的,可简要参考下
c#重写TabControl控件实现关闭按钮
另,
TabControl控件的美化
介绍的优化,源代码实在有点长,有兴趣可以了解下
在桌面软件开发中,有时会需要控制窗体或控件移动以实现某些界面效果,比如幻灯片换页、侧面的展开栏等。
通常情况下我们会使用Timer以每隔一段时间修改一下坐标位置的方式来实现目标对象的位移效果,但通过这个方式实现的动效存在几个问题:
匀速运动效果生硬;
运动过程中不便灵活改变运动状态(如侧栏展开一半时令其收起);
动效多时需要创建多个Timer对象,不易管理且占用资源;
ApeForms中为控件和窗体提供了平滑运动的扩展方法,很好的解决了这些问题。不仅是坐标的平滑运动,还有控件\窗体尺寸的平滑变化、透明度的平滑变化。允许在变化的中途随时更改目标坐标\尺寸\透明度,且使用共享的Timer
TabControl是一个分页切换(tab)控件,不同的页框内可以呈现不同的内容,将主要介绍调整tab的左右侧显示、设置多行tab、禁用或删除tabpage、隐藏TabControl头部的选项卡等
最终优化实现ButtonPro按钮(继承自Button),既提供Button原生功能,又提供扩展功能,除了圆角以外,还实现了圆形、圆角矩形的脚尖效果、边框大小和颜色、背景渐变颜色...
Windows 11下所有控件已经默认采用圆角,其效果更好、相对有着更好的优化...尝试介绍很常见的圆角效果,通过重写控件的OnPaint方法实现绘制,并在后面进一步探索对应的优化和可能的问题
上一篇文章把插件加载好了,并且把插件中的所有控件都显示到了列表框中,这次要做的就是实现拖曳控件的功能,用户选择一个控件拖曳到画布上,松开,在松开位置处自动实例化该控件,这个需要用到dropEvent和dragEnterEvent事件,重新实现这两个事件,对拖曳的对象进行过滤并调用函数实例化该控件,在实例化该控件的同时实例化控件跟随控件以便拉伸调整大小和位置。
原文:WPF自定义控件(三)の扩展控件
扩展控件,顾名思义就是对已有的控件进行扩展,一般继承于已有的原生控件,不排除继承于自定义的控件,不过这样做意义不大,因为既然都自定义了,为什么不一步到位呢,有些不同的需求也可以通过此来完成,不过类似于类继承了。
1.为集合属性设计器识别具体项类型
wpf设计器允许定义集合项的类型,如新发布的WPF的DataGrid控件,其中的Columns包括一下几种类型,Columns集合属性是以下几个类型的抽象类集合.
这个问题其实也可以理解为:怎样在WPF/XAML中使用Winform中的控件(如PictureBox)?首先看看XAML代码:(注意下面加粗的部分) ...
原文:WPF 控件库——轮播控件
一、要做成什么样
bs端的轮播控件千千万,有的甚至能作为一个单独的库来开发,所涉及到的功能也是缤纷多彩。相对来说,cs端的轮播用得不多,我这里只是简单的做了个能满足一般需求的轮播,在项目中凑会凑会还是可以的。
原文:WPF之动态换肤
如何实现换肤呢,对于复杂的换肤操作,如,更换按钮样式、窗口样式等,我们需要写多个资源字典来表示不同的皮肤,通过动态加载不同的资源字典来实现换肤的效果;对于简单的换肤操作,如更改背景颜色、设置窗体透明度,这种换肤操作,我们就不能使用上面的方法了,这个时候,我们只要在一个全局对象中添加几个属性,如背景颜色、前景颜色、窗体透明度等,然后,再绑定这几个属性就能达到我们想要的效果。
原文:WPF 4 DataGrid 控件(基本功能篇)
提到DataGrid 不管是网页还是应用程序开发都会频繁使用。通过它我们可以灵活的在行与列间显示各种数据。本篇将详细介绍WPF 4 中DataGrid 的相关功能。