Winforms:SuspendLayout / ResumeLayout 不足吗?

28

我有几个“自定义控件”的库。基本上,我们有自己的按钮、圆角面板和一些带有自定义绘画的分组框。尽管在 OnPaint 方法中涉及到了一些“数学”运算,但这些控件还是相当标准的。大多数情况下,我们只需要绘制圆角并为背景添加渐变色。我们使用 GDI+ 来完成所有这些操作。

这些控件还可以(根据我们客户的反馈)看起来很漂亮,但是即使启用了双缓冲区,当在同一窗体上有 20 多个按钮(例如)时,你仍然会看到一些重绘,这非常让人不爽。

我很确定我们的按钮并不是地球上最快的东西,但我的问题是:如果启用了双缓冲区,那么所有的重绘都应该在后台进行,Windows 子系统应该“立即”显示结果吧?

另一方面,如果有一个“复杂”的 foreach 循环来创建标签、将它们添加到一个面板中(双缓冲区),并更改它们的属性,则如果在循环之前暂停面板的 SuspendLayout,循环结束后恢复面板的 ResumeLayout,那么所有这些控件(标签和按钮)不应该“几乎瞬间”出现吗?但实际上并非如此,你可以看到面板被填充的过程。

这是为什么呢?我知道没有示例代码很难评估,但复制它也很困难。我可以用相机制作视频,但请相信我,速度很慢 :)


你也应该尝试暂停/恢复重绘操作...请查看我的更新答案。 - Adam Robinson
你肯定有性能问题。我认为绘制渐变和四分之一圆应该不会那么慢。 - DonkeyMaster
好的,正如我所说的,UI库并不是最快的,但我们也有很多GDI+绘图代码来使按钮看起来像我们想要的样子。它不仅仅是画四个弧形和用渐变涂抹表面。我想我们也得在这方面努力……但我在想是否有一种方法可以加速它。如果它双缓冲,当它“翻转”时应该能够快速显示,对吧? - Martin Marconcini
我仍在调查这个问题,很快会报告。感谢迄今为止提供的想法。 - Martin Marconcini
10个回答

13

我们也遇到过这个问题。

我们看到一种“修复”它的方法是,在准备好之前完全停止控件的绘制。为了实现这一点,我们向控件发送 WM_SETREDRAW 消息:

// Note that WM_SetRedraw = 0XB

// Suspend drawing.
UnsafeSharedNativeMethods.SendMessage(handle, WindowMessages.WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero);

...

// Resume drawing.
UnsafeSharedNativeMethods.SendMessage(handle, WindowMessages.WM_SETREDRAW, new IntPtr(1), IntPtr.Zero);

那是C ++吗?我没有UsafeSharedNativeMethods... 我是否缺少引用?(嘿嘿嘿,我听起来像vcs编译器);) - Martin Marconcini
可能是C++,但这与我在答案中提供的链接中包含的概念相同。 - Adam Robinson
17
这不是C++,这是一种非常标准的命名PInvoke类的方式。您应该创建自己的静态类并添加一个名为SendMessage的PInvoke方法。WindowMessages显然也是一个用户定义的枚举类型,其中的值从Win32 API正确指定。最后,Adam,您之前没有发布过这个内容,只是发布了一个链接,并且您的态度相当恼人。 - Jon Grant

13

您需要注意的一件事情是检查您是否在任何面板的子控件上设置了BackColor = Transparent。 BackColor = Transparent会显著降低渲染性能,特别是如果父面板正在使用渐变色时。

Windows Forms不使用真正的透明度,而是使用“伪”透明度。每个子控件绘制调用都会在父控件上生成绘制调用,以便父控件可以在其上绘制背景,以便子控件绘制其内容,使其呈现透明效果。

因此,如果您有50个子控件,那么将会在父控件上生成额外的50个绘制调用进行背景绘制。并且由于渐变通常较慢,因此您将看到性能降低。

希望这可以帮助您。


9
我会从性能角度来解决您的问题。
使用foreach循环创建标签,将它们添加到面板(双缓冲区),然后更改它们的属性。如果按照这个顺序进行操作,那么有改进的空间。首先创建所有标签,更改它们的属性,当它们全部准备好时,再将它们添加到面板中:Panel.Controls.AddRange(Control[]) 大部分时间,我们只需要绘制圆角并在背景上添加渐变。您是否一遍又一遍地做同样的事情?您的渐变是如何生成的?编写图像不能太慢。我曾经不得不在内存中创建一个1680x1050的渐变,速度非常快,甚至比Stopwatch还要快,所以绘制渐变不可能如此困难。我的建议是尝试缓存一些东西。打开Paint,绘制您的角落并保存到磁盘中,或者仅在内存中生成一次图像。然后根据需要加载(和调整大小)。渐变也是如此。即使不同的按钮具有不同的颜色,但是相同的主题,您也可以使用Paint或其他工具创建位图,并在运行时加载它并将Color值乘以另一种颜色。
编辑:如果我们在循环之前挂起面板的布局,并在循环结束时恢复面板的布局,那么这不是SuspendLayout和ResumeLayout的用途。它们暂停布局逻辑,即控件的自动定位。与FlowLayoutPanel和TableLayoutPanel最相关。
至于双缓冲区,我不确定它是否适用于自定义绘制代码(没有尝试过)。我想你应该实现自己的双缓冲区。
简而言之,双缓冲区非常简单,只需要几行代码。在绘制事件中,将其渲染到位图上,而不是渲染到Graphics对象上,然后将该位图绘制到Graphics对象上。

如果你想走这条路,我还有几个想法。(星期一,因为5月8日是法国的假日) - DonkeyMaster
会考虑所有这些。非常好 ;) 不是在西班牙度假。我会告诉你更多关于我正在做的事情,也许有很多改进的空间。;) 谢谢。 - Martin Marconcini
1
那个有关创建整个 Control[] 并使用 AddRange 一次性设置它的提示完美地解决了我的问题。非常感谢您,因为我认为我自己很长一段时间都不会尝试这样做 ;) - julealgon

4

除了 DoubleBuffered 属性外,还可以尝试在控件的构造函数中添加以下内容:

SetStyle(ControlStyles.OptimizedDoubleBuffer | 
         ControlStyles.AllPaintingInWmPaint, true);

如果这还不够(我敢说这点可能不够),可以看一下我对此问题的回答,并暂停/恢复面板或窗体的重新绘制。这将让您的布局操作完成,然后在完成后执行所有绘图。


我们已经这样做了,还使用了ControlStyles.UserPaint;我想我们已经尝试了所有可能的组合。结果总是差不多 :S - Martin Marconcini
我遇到了另一个问题中提问者所遇到的相同行为,“它没有帮助……我还注意到当弹出菜单覆盖一些按钮并迫使它们重新绘制时,重新绘制尤其缓慢(例如,您可以看到按钮边界被绘制,按钮填充了实心颜色,最后按钮才绘制出它的图片,然后才是下一个按钮)”。现在不仅速度变得非常慢,而且还有一个边框...这很奇怪:S - Martin Marconcini
在您的自定义控件绘制代码中,您是每次重绘整个控件还是仅重绘需要的部分? - Adam Robinson
1
根据 https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Control.cs,这似乎与设置“DoubleBuffered”属性的效果完全相同。 - Mike Henry

3

2
也许首先可以在一个仅控制可见性的“私有”缓冲区上进行绘制,然后再进行渲染:

在你的控件中

BufferedGraphicsContext gfxManager;
BufferedGraphics gfxBuffer;
Graphics gfx;

安装图形的函数

private void InstallGFX(bool forceInstall)
{
    if (forceInstall || gfxManager == null)
    {
        gfxManager = BufferedGraphicsManager.Current;
        gfxBuffer = gfxManager.Allocate(this.CreateGraphics(), new Rectangle(0, 0, Width, Height));
        gfx = gfxBuffer.Graphics;
    }
}

在它的paint方法中
protected override void OnPaint(PaintEventArgs e)
{
    InstallGFX(false);
    // .. use GFX to draw
    gfxBuffer.Render(e.Graphics);
}

在其resize方法中
protected override void OnSizeChanged(EventArgs e)
{
    base.OnSizeChanged(e);
    InstallGFX(true); // To reallocate drawing space of new size
}

上述代码已经进行了一定的测试。


2
听起来你需要的是“组合”显示,即整个应用程序一次性绘制,几乎像一个大位图。这就是WPF应用程序的工作方式,除了“chrome”(如标题栏、调整大小手柄和滚动条)围绕着应用程序。
请注意,通常情况下,除非您已经更改了一些窗口样式,否则每个Windows窗体控件都负责自己的绘制。也就是说,每个控件都有机会处理WM_PAINT、WM_NCPAINT、WM_ERASEBKGND等与绘制相关的消息,并独立地处理这些消息。对于您而言,这意味着双缓冲仅适用于您正在处理的单个控件。为了实现接近清晰、组合的效果,您需要关注不仅您正在绘制的自定义控件,还要关注它们放置的容器控件。例如,如果您有一个包含GroupBox的窗体,该GroupBox又包含多个自定义绘制按钮的窗体,则这些控件中的每一个都应将其DoubleBuffered属性设置为True。请注意,此属性受保护,因此这意味着您必须继承各种控件(只是为了设置双缓冲属性),或者使用反射来设置受保护的属性。此外,并非所有的Windows窗体控件都尊重DoubleBuffered属性,因为在内部,其中一些只是本地“常见”控件的包装器。
如果您的目标是Windows XP(以及可能更高版本),则可以设置组合标志。有WS_EX_COMPOSITED窗口样式。我以前用过它来混合结果。它与WPF / WinForm混合应用程序不兼容,也无法与DataGridView控件很好地配合使用。如果您选择这条路,请务必在不同的机器上进行大量测试,因为我看到过奇怪的结果。最终,我放弃了使用这种方法。

我玩这个标志很开心,但最终我无法解决一个错误,它会导致一个CPU核心旋转到100%并保持在那里,对于任何带有打开选项卡的表格。 - Daniel Keogh

0

当我切换想要显示的用户控件时,我在tablelayoutpanel上遇到了同样的问题。

通过创建一个继承自该表格的类并启用双缓冲,我完全消除了闪烁。

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;

namespace myNameSpace.Forms.UserControls
{
    public class TableLayoutPanelNoFlicker : TableLayoutPanel
    {
        public TableLayoutPanelNoFlicker()
        {
            this.DoubleBuffered = true;
        }
    }
}

0

我曾经看到过在窗体上出现闪烁的坏WinForms,其中控件引用了缺失的字体。

这可能不常见,但如果您已尝试了其他所有方法,那么值得研究一下。


0

我过去遇到了很多类似的问题,解决方法是使用第三方UI套件(即DevExpress),而不是标准的Microsoft控件。

一开始我使用的是Microsoft标准控件,但我发现我经常需要调试由它们的控件引起的问题。问题更加严重的是,Microsoft通常不会修复任何已经被识别出来的问题,并且他们很少提供合适的解决方法。

我转向了DevExpress,我只有好话要说。这个产品非常稳定,他们提供了很好的支持和文档,而且他们确实听取客户的意见。每当我有问题或者疑问时,我都能在24小时内得到友好的回应。在某些情况下,我确实发现了一个bug,在两个实例中,他们都在下一个服务发布中实施了修复。


谢谢Dennis,我很想转换到类似的东西,但我们使用自定义绘制的UI(“unique”),这是我们应用程序的商标。对于我们来说,切换到“只是另一组Windows控件”行不通。我们的界面是触觉导向+TabletPC,因此我们只使用WinCtl中的“少量控件”,但几乎总是在其上进行自定义绘制。 - Martin Marconcini
@Martin,我们这里也做了同样的选择。我认为这是个错误。我们正在考虑升级到WPF,但这需要很多工作... - Benjol
@benjol 是啊,将这个应用程序改成WPF需要几个月的工作!甚至更长时间!:S - Martin Marconcini

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接