如何在 ProgressBar 上层绘制(使用 C#)





5.00/5 (7投票s)
一个简单的解释,说明如何将自定义绘制与 ProgressBar 的默认绘制同步。
引言
在我开发一个包含背景进度条的实验性复合控件时,我想让标准 ProgressBar 的颜色稍微淡化一点,以便在其上绘制的任何文本、图标或控件更加突出和易读。作为一个简单的解决方案,在控件顶部绘制一层半透明的白色层似乎是可行的方法,所以我创建了一个类来实现所需的绘制代码。
这是那些看似微不足道但一旦开始编写代码来实现所需功能就会发现问题的编程任务之一,正如我很快就发现的那样。请继续阅读,了解我访问过的页面以及我找到的解决方案。
我真诚地希望我的解决方案能为那些仍在处理 Windows Forms 项目、遇到此问题且没有现成第三方控件的程序员提供一个简单的答案。
背景
标准 ProgressBar 的问题在于它不支持 Paint
事件。我在网上搜索了一段时间,希望能找到一些能提供简单答案的示例代码,但徒劳无功。有一些解决方案,例如 这个 和 那个,但没有一个提出的解决方案适合我的项目,主要是因为在那里 ProgressBar 是在一个派生自 NativeWindow
类的类中实现的,而 NativeWindow
类不像 Control
类那样公开 CreateGraphics
方法。
由于 ProgressBar 实际上是 Windows Progress Bar 公共控件的包装器,我随后查阅了 MSDN 文档中关于 msctls_progress32
类支持的 消息和样式。我本以为会找到一些关于如何使用 NM_CUSTOMDRAW
通知实现自定义绘制的解释,但令我非常失望的是,我发现这种情况并不支持。这让我熬夜研究,却没能找到太多关于如何实现的方法,尽管我确信一定有一种简单的方法可以用最少的代码来实现。
然后我找到了 MSDN 上的 关于 ProgressBar 控件 页面,其中解释了一些标准的 Windows 消息以及 Windows ProgressBar 如何处理它们。关于 WM_PAINT
消息,它写道:“绘制进度条。如果 wParam
参数非 NULL
,则控件假定该值是一个 HDC 并使用该设备上下文进行绘制。”
换句话说,你可以在调用基类之前创建自己的设备上下文并将其传递给控件,方法是将其分配给 WM_PAINT
消息的 WParam
参数!我以前从未在任何其他通用控件上见过这种功能,起初,我是在考虑 NM_CUSTOMDRAW
通知的情况下阅读这段文本的,所以只看了几遍页面后,我才意识到采用这种方法的真正可能性。当我想明白时,我决定试一试,结果证明,效果非常好!
Using the Code
本文的源代码是一个解决方案,包含两个项目:一个库和一个测试项目。你需要 VS2015 来打开此解决方案,但代码可以在早期版本的 Visual Studio 中运行。
该库包含一个名为 CustomProgressBar
的类,它继承自 System.Windows.Forms.ProgressBar
类,并重写了 WndProc
方法来处理 WM_PAINT
和 WM_PRINTCLIENT
消息。处理 WM_PRINTCLIENT
消息很重要,因为控件偶尔会收到它,以获取控件的静态图像。例如,当控件在设计器中被拖动到另一个位置时,会发送 WM_PRINTCLIENT
消息来创建拖动图像。当控件未实现 WM_PRINTCLIENT
消息时,拖动时使用的图像与控件在开始拖动操作之前的外观不同,这对于使用该控件的开发人员来说可能会显得奇怪。
以下代码片段处理 WM_PAINT
消息,该消息不提供设备上下文作为参数。此代码通过调用 BeginPaint
Windows 函数创建设备上下文,然后使用此设备上下文进行绘制操作,最后调用 EndPaint
函数释放设备上下文。
private void WmPaint(ref Message m)
{
// Create a Handle wrapper
HandleRef myHandle = new HandleRef(this, Handle);
// Prepare the window for painting and retrieve a device context
NativeMethods.PAINTSTRUCT pAINTSTRUCT = new NativeMethods.PAINTSTRUCT();
IntPtr hDC = UnsafeNativeMethods.BeginPaint(myHandle, ref pAINTSTRUCT);
try
{
// Apply hDC to message
m.WParam = hDC;
// Let Windows paint
base.WndProc(ref m);
// Create a Graphics object for the device context
using (Graphics graphics = Graphics.FromHdc(hDC))
{
Rectangle rect = ClientRectangle;
// Paint a translucent white layer on top, to fade the colors
using (SolidBrush fadeBrush = new SolidBrush(Color.FromArgb(150, Color.White)))
{
graphics.FillRectangle(fadeBrush, rect);
}
// Draw text: the Value and a percent sign
TextRenderer.DrawText(graphics, Value.ToString() + "%", Font, rect, ForeColor);
}
}
finally
{
// Release the device context that BeginPaint retrieved
UnsafeNativeMethods.EndPaint(myHandle, ref pAINTSTRUCT);
}
}
Private Sub WmPaint(ByRef m As Message)
' Create a Handle wrapper
Dim myHandle As New HandleRef(Me, Handle)
' Prepare the window for painting and retrieve a device context
Dim pAINTSTRUCT As New NativeMethods.PAINTSTRUCT()
Dim hDC As IntPtr = UnsafeNativeMethods.BeginPaint(myHandle, pAINTSTRUCT)
Try
' Apply hDC to message
m.WParam = hDC
' Let Windows paint
MyBase.WndProc(m)
' Create a Graphics object for the device context
Using g As Graphics = Graphics.FromHdc(hDC)
Dim rect As Rectangle = ClientRectangle
' Paint a translucent white layer on top, to fade the colors a bit
Using fadeBrush As New SolidBrush(Color.FromArgb(Fade, Color.White))
g.FillRectangle(fadeBrush, rect)
End Using
' Draw text: the Value and a percent sign
TextRenderer.DrawText(g, Value.ToString() + "%", Font, rect, ForeColor)
End Using
Finally
' Release the device context that BeginPaint retrieved
UnsafeNativeMethods.EndPaint(myHandle, pAINTSTRUCT)
End Try
End Sub
此代码的关键部分是下面的代码片段
// Apply hDC to message
m.WParam = hDC;
// Let Windows paint
base.WndProc(ref m);
' Apply hDC to message
m.WParam = hDC
' Let Windows paint
MyBase.WndProc(m)
这就是我们在调用基类之前将设备上下文分配给消息的地方,以便 Windows 使用此提供的设备上下文执行其默认绘制。这使得默认绘制能够与之后进行的任何自定义绘制正确同步。当 Windows 完成控件的默认绘制后,会为设备上下文创建一个 Graphics
对象,以便轻松绘制半透明的白色层以及显示为百分比的进度值文本。
在处理 WM_PAINT
消息时,重要的是要注意 Windows 会大量发送此消息来创建控件激活时的涟漪动画效果,因此任何响应代码最好都要快速且简单。示例项目中的代码实际上比上面的示例更复杂,上面的示例是为了简单起见,一目了然地展示了基本流程。
设计时支持
由于该控件现在能够显示文本,因此它应该能够与其邻居很好地协同工作,因此必须像标准 Label 在设计环境中那样支持其显示文本的对齐。因此,我添加了一个名为 CustomProgressBarDesigner
的简单 ControlDesigner
类,它重写了其基类的 SnapLines
属性,向其返回的集合中添加了用于居中文本的基线。
关注点
我花了大量时间研究诸如 FillRect
和 TextOut
等函数,这些函数据说非常快,并且可以直接在设备上下文上操作,因此无需获取 Graphics
对象。但不幸的是,这些函数似乎不支持透明度。例如,FillRect
函数会忽略叠加颜色的 alpha 通道。任何仅使用本机代码来解决此问题的尝试,很快就会导致代码臃肿而未能实现所有设计目标。这就是为什么我选择使用 Graphics
对象,它公开了许多支持 alpha 混合的方法,只需几行简单的代码即可实现所需的结果。但是,如果您碰巧知道如何用纯本机代码来实现这一点,如果您能给我一封信解释一下技巧,我将不胜感激。
为了避免任何闪烁效果,控件需要使用双缓冲。为了实现这一点,我重写了 CreateParams
属性以应用 WS_EX_COMPOSITED
扩展控件样式,正如 MSDN 所解释的那样,“*使用双缓冲以从下到上绘制窗口的所有后代*”。这对于大多数(如果不是全部)忽略 DoubleBuffered
属性的通用控件来说效果很好。
在处理 WM_PRINTCLIENT
消息时,无需显式调用 BeginPaint
和 EndPaint
函数,因为对于 WM_PRINTCLIENT
消息,WParam
参数中已经提供了设备上下文。
在本文的源代码中,所有本机方法调用都使用 HandleRef
结构来妥善包装任何句柄值,正如 MSDN 所解释的那样:“*确保托管对象在平台调用完成之前不会被垃圾回收*”,这可能会增加代码的健壮性。
文档
本文的示例项目附带了完整的文档,以编译帮助文件的形式提供,解释了这个相当简单的控件的公共接口。该帮助文件是使用 Sandcastle 编译的,Sandcastle 是一个遗留的 Microsoft 开源帮助编译器,可以根据源代码中提供的文档标签以多种格式编译文档,您可以在本文的示例项目中看到。
历史
- 2016 年 3 月 1 日 - 首次发布