DiamondControl - 带有鼠标悬停效果的自定义按钮






4.45/5 (8投票s)
2005年3月6日
9分钟阅读

73172

273
一个拥有自定义 UIEditor 的拥有者绘制控件,适用于 Visual Studio 设计器。
引言
本文将介绍设计自定义用户控件时需要考虑的一些问题和潜在的陷阱。DiamondControl
拥有一个自定义 UIEditor,可在 Visual Studio 设计器中使用时显示控件的形状。它还支持透明度和双缓冲,以提供平滑的鼠标悬停颜色过渡。下载中还包含一个帮助文件(DiamondControl.chm,使用 VBCommenter 和 NDoc 生成)。
背景
我很久以前就设计了这个控件,但我一直对结果不满意。对于不同尺寸和形状的控件,边框似乎总是不够完美。此外,裁剪似乎也不准确,导致控件的右侧和底部边缘看起来很奇怪。从那时起,我用一种新的技术重写了 OnPaint
方法,该技术用于定义控件的裁剪区域,并应用正确的数学计算来绘制边框,无论控件如何调整大小。
使用代码
生成下载中的解决方案,然后将 .dll 文件从 bin 文件夹复制到您的项目文件夹,以便在项目中包含此控件。在项目中添加对控件 .dll 的引用,然后将一个或多个控件实例添加到您的窗体中。您可能还想将控件添加到 Visual Studio 的工具箱中,以便更容易地将其包含在窗体中。如果我使用这种方法,我通常会喜欢将控件的 .dll 文件复制到 PublicAssemblies 文件夹(在我的计算机上位于 C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\IDE\PublicAssemblies),并将工具箱引用添加到该副本。
关注点
为了实现控件的透明度和双缓冲绘制,我在控件的 Sub New()
中,在调用 InitializeComponent()
之后添加了以下代码:
Dim cs As ControlStyles = _
ControlStyles.DoubleBuffer Or _
ControlStyles.AllPaintingInWmPaint Or _
ControlStyles.ResizeRedraw Or _
ControlStyles.UserPaint Or _
ControlStyles.SupportsTransparentBackColor
Me.SetStyle(cs, True)
Me.UpdateStyles()
MyBase.BackColor = Color.Transparent
我最初使用的技术是创建一个 PointF
数组来定义控件形状的点,然后将其添加到 GraphicsPath
中,接着将控件的区域设置为该 GraphicsPath
,并使用 FillPath
来填充控件。在此之后,我计算另一个 GraphicsPath
,使用另一个 PointF
数组,该数组基本上将控件的 BorderWidth
属性除以二,并使用由此计算出的点来调用 DrawPath
来绘制边框。这被证明是不理想的,因为边框并不总是均匀的,并且控件区域似乎裁剪了区域的右侧和底部边缘。
我的新技术与原始技术类似,但现在我从 ClientSize.Width
和 ClientSize.Height
中减去一,以考虑控件的右侧和底部边缘未绘制(我在某处读到这是使用双缓冲绘制控件时遇到的一个问题)。我还决定,填充控件区域的更好方法是避免使用 DrawPath
,而仅使用 FillPath
来绘制中心和边框。
工作原理是:如果控件没有边框,即 BorderWidth
属性设置为零,我只需使用初始的 GraphicsPath
调用 FillPath
,颜色为按钮的内部颜色(ButtonColor
属性)。
当使用边框时,我首先使用初始计算的 GraphicsPath
和边框颜色(qproperty
)调用 FillPath
。然后,我再次调用计算 PointF
数组的 Sub
,即 GetFillArray
Sub
,这次将可选的 isBorder
参数设置为 True
。这会使 Sub
计算一个比边框宽度减小的点数组,然后用它来第二次调用 FillPath
,以 ButtonColor
填充控件的内部。因此,整个控件首先用边框颜色填充,然后内部用按钮颜色重绘。这听起来不如使用 DrawPath
绘制控件周围的边框高效,但我没有注意到与原始代码相比性能有任何实际差异。
哦,那些边框!
正如我提到的,原始代码并不总是能在控件上创建均匀的边框。在画了一些不同尺寸的按钮的方格纸后,我决定“重返校园”,认真研究计算用于绘制控件内部的正确点的数学。这被证明是一项非常艰巨的经历!我最终找到了似乎是确定这些点的完美数学解决方案,使用了部分几何和三角学。
上图 1 显示了控件的角落,使用了控件的右箭头形状。图中用红色勾勒出的两个三角形是全等三角形,即它们除了位置外完全相同。可以看出,边框内角的位置可以通过添加底部三角形的边 b 的长度和上方三角形的边 c 的长度来找到。进一步研究表明,底部三角形的边 b 的长度等于底部三角形左下角的正切值,当然,这只不过是边 c(斜边)的斜率。现在这似乎很简单,就是控件高度的一半,除以控件的宽度(斜率定义为高度变化除以宽度变化,(y2-y1)/(x2-x1))。
此外,由于两个三角形是全等的,或相等的,上方三角形的斜边(边 c)与下方三角形的斜边相同,我们可以很容易地用勾股定理计算出来,即 c2 = a2 + b2。数学计算的简化在于底部三角形的边 a 只是边框的宽度。因此,在计算中,我们将边 a 视为 1,并使用以下公式
(Math.Sqrt(m ^ two + one) + m) * mb
其中 m
是斜率,mb
是边框宽度,one
和 two
是定义为 1.0F 和 2.0F 的常量。内括号内的表达式是边 a 和边 b 的平方和(记住边 b 是正切值,即斜率,边 a 取为 1,表示边框的一个“单位宽度”)。然后,我们只需对其进行平方根运算(这样我们就得到了斜边 c 的长度),加上斜率 m
(边 b),然后将整个结果乘以 mb
,即边框宽度。很简单,不是吗?
在上图 2 中,我们看到了控件的另一个角落,这次是箭头的“尖端”。幸运的是,这个的数学计算比第一个要简单一些!检查图表表明我们感兴趣的长度就是红色突出显示的三角形的边 c。我们知道角度 A
,即我们从线 b 的斜率中得到它的值。我们也知道边 a 仍然是边框的“单位宽度”,我们取为 1。所以长话短说(更短),我们可以使用以下公式来得到边 c 的长度
mb / Math.Sin(Math.Atan(m))
因此,表达式中的内部部分找到其正切值为 m
(斜率)的角度(Atan(m)
),我们使用另一个三角恒等式来完成,即
c = a / sin A
由于上面表达式中的 a
是我们的“单位宽度”边框,取值为 1,因此无需写
mb * (1 / Math.Sin(Math.Atan(m)))
我们只需合并项,将 mb * 1
相乘得到 mb
(还能是什么!),这会简化表达式。
结论
我鼓励您阅读本文附带的源代码,以了解上述值如何在 GetFillArray
Sub
中使用,以及斜率是如何计算的。事实证明,要获得菱形的所有四个边/角,需要两个斜率值,一个正好是另一个的倒数,即 1 / m
。第一个用于左侧和右侧的“点”,第二个用于菱形的顶部和底部“点”。
我还想讨论自定义 UIEditor 的工作原理,但这篇题为文章越来越长,所以我将留给读者自行下载源代码并检查 UIEditor 代码的工作原理。
更新
我儿子问我是否能向他解释文章中描述的点的计算方法。看了我的方格纸图后,他问我为什么不像第二个点那样为第一个点使用类似的方法,而是依赖两个全等三角形的方法。听到这话,我不得不问自己同一个问题。仅仅分割形成的角,并使用基于半角的直角三角形来计算必要长度,真的会更容易吗?为了找出答案,我做了一个新的图如下:
在这里,我们可以看到我们感兴趣的长度是红色三角形的边 b。边 a 是我们的边框宽度,我们仍然将其取为 1。根据三角学,我们知道一个角的正切等于对边除以邻边,所以这里是:tan A = a / b
。由此得出 b = a / tan A
。
但是这个角 A
是什么呢?我们发现它是控件下方斜线的补角的二分之一。多么幸运!我们已经在计算该线的斜率了,我们需要它来计算上图 2 中的长度。所以我们的角 A
等于 (90 - 下方的角度) / 2
。给定我们先前计算出的斜率 m
,这就是 (90 - atan(m)) / 2
度。但是要认识到 .NET 的 Math
类中的三角函数使用弧度而不是度,我们必须这样写:
(Math.PI / 2 - Math.Atan(m)) / 2
所以,以边框宽度作为变量 mb
,我们的简单表达式 a / tan A
变为:
mb / Math.Tan((Math.PI / 2 - Math.Atan(m)) / 2)
看到这个表达式的复杂性后,我决定:
(Math.Sqrt(m ^ 2 + 1) + m) * mb
也许还是更好,即使在概念上理解它是如何得出的会稍微困难一些!
如果您有兴趣了解更多关于三角学的信息,请查看 Dave's Short Trig Course。David E. Joyce(马萨诸塞州伍斯特克拉克大学数学与计算机科学系)的这个网站是三角学信息的宝库。我尤其喜欢他提出的引人入胜的练习,配有提示和答案。干得好,Dave!
历史
- 2005 年 3 月 6 日 - 初始发布。
- 2005 年 3 月 18 日 - 添加了更新,提供了计算图 1 长度的替代方法。