如何修复用户控件中的闪烁

Posted

技术标签:

【中文标题】如何修复用户控件中的闪烁【英文标题】:How to fix the flickering in User controls 【发布时间】:2011-02-06 10:28:12 【问题描述】:

在我的应用程序中,我不断地从一个控件移动到另一个控件。我创造了没有。用户控件,但在导航期间我的控件会闪烁。更新需要 1 或 2 秒。我试图设置这个

SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
or
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true); 
SetStyle(ControlStyles.DoubleBuffer, true);

但这并没有帮助...每个控件都有相同的背景图像和不同的控件。 那么有什么解决办法。。 谢谢。

【问题讨论】:

这些语句在哪里?理想情况下,将它们放在构造函数中。设置这些后你打电话给UpdateStyles了吗?它的记录很差,但有时可能是必要的。 【参考方案1】:

不是双缓冲能解决的那种闪烁。也不是 BeginUpdate 或 SuspendLayout。你的控件太多了,BackgroundImage 会让它变得更糟。

它在 UserControl 绘制自身时开始。它绘制 BackgroundImage,在子控件窗口所在的位置留下孔。然后每个子控件都会收到一条消息来绘制自己,他们将用他们的窗口内容填充这个洞。当你有很多控件时,这些漏洞对用户来说是可见的一段时间。它们通常是白色的,与背景图像在黑暗时形成鲜明对比。或者,如果表单设置了 Opacity 或 TransparencyKey 属性,它们可能是黑色的,与几乎任何东西形成鲜明对比。

这是 Windows 窗体的一个非常基本的限制,它与 Windows 呈现窗口的方式有关。顺便说一句,由 WPF 修复,它不使用窗口作为子控件。你想要的是双缓冲整个表单,包括子控件。这是可能的,请检查我在this thread 中的代码以获取解决方案。它虽然有副作用,但实际上并没有提高绘画速度。代码很简单,把它粘贴到你的表单中(不是用户控件):

protected override CreateParams CreateParams 
  get 
    CreateParams cp = base.CreateParams;
    cp.ExStyle |= 0x02000000;  // Turn on WS_EX_COMPOSITED
    return cp;
  
 

您可以做很多事情来提高绘画速度,以至于闪烁不再明显。从处理 BackgroundImage 开始。当源图像很大并且需要缩小以适应控件时,它们可能真的昂贵。将 BackgroundImageLayout 属性更改为“平铺”。如果这样可以显着加快速度,请返回您的绘画程序并调整图像大小以更好地匹配典型控件大小。或者在 UC 的 OnResize() 方法中编写代码来创建一个适当大小的图像副本,这样就不必在每次控件重绘时都调整它的大小。对该副本使用 Format32bppPArgb 像素格式,它的渲染速度比任何其他像素格式快约 10 倍。

接下来您可以做的是防止孔洞如此明显并与图像形成鲜明对比。您可以turn off UC 的 WS_CLIPCHILDREN 样式标志,该标志防止 UC 在子控件所在的区域进行绘画。将此代码粘贴到 UserControl 的代码中:

protected override CreateParams CreateParams 
  get 
    var parms = base.CreateParams;
    parms.Style &= ~0x02000000;  // Turn off WS_CLIPCHILDREN
    return parms;
  

子控件现在将自己绘制在背景图像之上。你可能仍然会看到他们一幅幅画自己,但丑陋的中间白色或黑洞将不可见。

最后但同样重要的是,减少子控件的数量始终是解决绘制缓慢问题的好方法。覆盖 UC 的 OnPaint() 事件并绘制现在在孩子中显示的内容。特定的 Label 和 PictureBox 非常浪费。方便点击,但它们的轻量级替代方案(绘制字符串或图像)只需要在 OnPaint() 方法中编写一行代码。

【讨论】:

关闭 WS_CLIPCHILDREN 改善了我的用户体验。【参考方案2】:

这是一个真正的问题,Hans Passant 给出的答案对于节省闪烁非常有用。但是,正如他所提到的,有副作用,它们可能很丑(UI 丑)。如前所述,“您可以为 UC 关闭 WS_CLIPCHILDREN 样式标志”,但这只会为 UC 关闭它。主窗体上的组件仍然存在问题。

例如,面板滚动条不会绘制,因为它在技术上位于子区域中。但是子组件不会绘制滚动条,因此直到鼠标悬停(或其他事件触发它)才会绘制。

此外,动画图标(在等待循环中更改图标)不起作用。删除 tabPage.ImageKey 上的图标不会适当地调整/重绘其他 tabPages。

所以我一直在寻找一种方法来关闭初始绘制时的WS_CLIPCHILDREN,以便我的表单可以很好地加载,或者更好的是仅在使用大量组件调整我的表单大小时才打开它。

诀窍是让应用程序以所需的WS_EX_COMPOSITED/WS_CLIPCHILDREN 样式调用CreateParams。我在这里找到了一个 hack (https://web.archive.org/web/20161026205944/http://www.angryhacker.com/blog/archive/2010/07/21/how-to-get-rid-of-flicker-on-windows-forms-applications.aspx),效果很好。感谢 AngryHacker!

我将TurnOnFormLevelDoubleBuffering() 调用以ResizeBegin 事件的形式和TurnOffFormLevelDoubleBuffering() 以ResizeEnd 事件的形式调用(或者在最初正确绘制后将其保留为WS_CLIPCHILDREN。)

    int originalExStyle = -1;
    bool enableFormLevelDoubleBuffering = true;

    protected override CreateParams CreateParams
    
        get
        
            if (originalExStyle == -1)
                originalExStyle = base.CreateParams.ExStyle;

            CreateParams cp = base.CreateParams;
            if (enableFormLevelDoubleBuffering)
                cp.ExStyle |= 0x02000000;   // WS_EX_COMPOSITED
            else
                cp.ExStyle = originalExStyle;

            return cp;
        
    

    public void TurnOffFormLevelDoubleBuffering()
    
        enableFormLevelDoubleBuffering = false;
        this.MaximizeBox = true;
    

【讨论】:

您的代码不包含 TurnOnFormLevelDoubleBuffering() 方法... @DanW 查看此答案中发布的 URL (angryhacker.com/blog/archive/2010/07/21/…) 此答案中的链接似乎已失效。我很好奇这个解决方案,你有另一个例子的链接吗?【参考方案3】:

如果您在控件中进行任何自定义绘画(即覆盖 OnPaint),您可以自己尝试双缓冲。

Image image;
protected override OnPaint(...) 
    if (image == null || needRepaint) 
        image = new Bitmap(Width, Height);
        using (Graphics g = Graphics.FromImage(image)) 
            // do any painting in image instead of control
        
        needRepaint = false;
    
    e.Graphics.DrawImage(image, 0, 0);

并使用属性NeedRepaint 使您的控件无效

否则上面的 SuspendLayout 和 ResumeLayout 答案可能就是你想要的。

【讨论】:

这是一种模拟双缓冲的创造性方法!。你可以在image = new Bitmap...之前添加if (image != null) image.Dispose();【参考方案4】:

将下面的代码放入您的构造函数或 OnLoad 事件中,如果您使用某种具有子控件的自定义用户控件,则需要确保这些自定义控件也是双缓冲的(即使在 MS他们说它默认设置为 true 的文档)。

如果您正在制作自定义控件,您可能希望将此标志添加到您的 ctor 中:

SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

您可以选择在表单/控件中使用此代码:

foreach (Control control in Controls)

    typeof(Control).InvokeMember("DoubleBuffered",
        BindingFlags.SetProperty | BindingFlags.Instance | BindingFlags.NonPublic,
        null, control, new object[]  true );

我们遍历表单/控件中的所有控件并访问它们的DoubleBuffered 属性,然后我们将其更改为 true 以使表单上的每个控件都具有双缓冲。我们在这里进行反射的原因是,假设您有一个控件,其中包含不可访问的子控件,这样,即使它们是私有控件,我们仍然会将它们的属性更改为 true。

更多关于双缓冲技术的信息可以在here找到。

我通常会覆盖另一个属性来解决这个问题:

protected override CreateParams CreateParams

    get
    
        CreateParams parms = base.CreateParams;
        parms.ExStyle |= 0x00000020; // WS_EX_COMPOSITED
        return parms;
    

WS_EX_COMPOSITED - 使用双缓冲以自下而上的绘制顺序绘制窗口的所有后代。

您可以找到更多这些样式标志here。

希望有帮助!

【讨论】:

【参考方案5】:

尝试 BeginUpdate/EndUpdate 或 SuspendLayout/ResumeLayout 方法。 见以下How to fix nested winform control flicker issuesFlickering during updates to Controls in WinForms (e.g. DataGridView)

【讨论】:

【参考方案6】:

在背景图像所在的主窗体或用户控件上,将BackgroundImageLayout 属性设置为CenterStretch。当用户控件呈现时,您会注意到一个很大的不同。

【讨论】:

【参考方案7】:

只是为了补充汉斯给出的答案:

(TLDR 版本:透明度比你想象的要重,到处只使用纯色)

如果 WS_EX_COMPOSITED、DoubleBuffered 和 WS_CLIPCHILDREN 没有解决您的闪烁问题(对我来说 WS_CLIPCHILDREN 让情况变得更糟),请尝试以下操作:检查您的所有控件和所有代码,以及任何您有 BackColor 的任何透明度或半透明度, ForeColor 或任何其他颜色,只需将其删除,仅使用纯色即可。在大多数情况下,您认为您只是必须使用透明度,但实际上并没有。重新设计您的代码和控件,并使用纯色。 我有可怕的,可怕的闪烁,程序运行缓慢。一旦我删除了透明度,它就会显着加速,并且闪烁为 0。

编辑:进一步补充,我刚刚发现 WS_EX_COMPOSITED 不必是窗口范围的,它可以仅应用于特定控件!这为我省去了很多麻烦。只需从您需要的任何控件继承自定义控件,然后粘贴已发布的 WS_EX_COMPOSITED 覆盖。这样你就可以只在这个控件上获得低级双缓冲,避免在应用程序的其余部分产生讨厌的副作用!

【讨论】:

【参考方案8】:

我尝试将此添加为评论,但我没有足够的积分。这是唯一能帮助我解决闪烁问题的东西,非常感谢 Hans 的帖子。对于像我这样使用 c++ builder 的人来说,这里是翻译

将 CreateParams 声明添加到应用程序的主表单 .h 文件中,例如

class TYourMainFrom : public TForm

protected:
    virtual void __fastcall CreateParams(TCreateParams &Params);

并将其添加到您的 .cpp 文件中

void __fastcall TYourMainForm::CreateParams(TCreateParams &Params)

    Params.ExStyle |= 0x02000000;  // Turn on WS_EX_COMPOSITED
    TForm::CreateParams(Params);

【讨论】:

【参考方案9】:

我知道这个问题很老了,但想谈谈我的经验。

在使用 .NET 4.0 的 Windows 8 中,Tabcontrol 在覆盖了OnPaint 和/或OnPaintBackGround 的表单中闪烁,我遇到了很多问题。

唯一有效的方法是不使用OnPaint 中的Graphics.DrawImage 方法覆盖,换句话说,当直接对PaintEventArgs 提供的图形进行绘制时,即使画出所有的矩形,闪烁消失。但是如果调用DrawImage 方法,即使绘制了一个裁剪的位图,(为双缓冲而创建)也会出现闪烁。

希望对你有帮助!

【讨论】:

【参考方案10】:

我将this flicker fix 和this font fix 组合在一起,然后我必须添加一些我自己的代码来启动绘制计时器,以在 TabControl 离开屏幕和返回时使 TabControl 无效等。

这三个都做到了:

using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public class TabControlEx:TabControl

    [DllImport("user32.dll")]
    private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
    private const int WM_PAINT = 0x0f;
    private const int WM_SETFONT = 0x30;
    private const int WM_FONTCHANGE = 0x1d;
    private System.Drawing.Bitmap buffer;
    private Timer timer = new Timer();
    public TabControlEx()
    
        timer.Interval = 1;
        timer.Tick += timer_Tick;
        this.SetStyle(ControlStyles.UserPaint | ControlStyles.DoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);
    
    void timer_Tick(object sender, EventArgs e)
    
        this.Invalidate();
        this.Update();
        timer.Stop();
    
    protected override void WndProc(ref Message m)
    
        if (m.Msg == WM_PAINT) timer.Start();
        base.WndProc(ref m);
    
    protected override void OnPaint(PaintEventArgs pevent)
    
        this.SetStyle(ControlStyles.UserPaint, false);
        base.OnPaint(pevent);
        System.Drawing.Rectangle o = pevent.ClipRectangle;
        System.Drawing.Graphics.FromImage(buffer).Clear(System.Drawing.SystemColors.Control);
        if (o.Width > 0 && o.Height > 0)
        DrawToBitmap(buffer, new System.Drawing.Rectangle(0, 0, Width, o.Height));
        pevent.Graphics.DrawImageUnscaled(buffer, 0, 0);
        this.SetStyle(ControlStyles.UserPaint, true);
    

    protected override void OnResize(EventArgs e)
    
        base.OnResize(e);
        buffer = new System.Drawing.Bitmap(Width, Height);
    
    protected override void OnCreateControl()
    
        base.OnCreateControl();
        this.OnFontChanged(EventArgs.Empty);
    
    protected override void OnFontChanged(EventArgs e)
    
        base.OnFontChanged(e);
        IntPtr hFont = this.Font.ToHfont();
        SendMessage(this.Handle, WM_SETFONT, hFont, (IntPtr)(-1));
        SendMessage(this.Handle, WM_FONTCHANGE, IntPtr.Zero, IntPtr.Zero);
        this.UpdateStyles();
    

我不是创建者,但据我了解,位图会绕过所有错误。

这是唯一为我彻底解决 TabControl(带图标)闪烁的问题。

差异结果视频:vanilla tabcontrol vs tabcontrolex

http://gfycat.com/FineGlitteringDeermouse

ps。您需要设置 HotTrack = true,因为这也修复了该错误

【讨论】:

【参考方案11】:

你试过Control.DoubleBuffered属性吗?

获取或设置一个值,该值指示此控件是否应使用辅助缓冲区重绘其表面以减少或防止闪烁。

this 和 this 也可能会有所帮助。

【讨论】:

【参考方案12】:

不需要任何双缓冲和所有这些东西......

一个简单的解决方案...

如果您使用的是 MDI 接口,只需将以下代码粘贴到主窗体中即可。它将消除页面上的所有闪烁。然而,一些需要更多时间来加载的页面将在 1 或 2 秒内显示出来。但这比显示一个闪烁的页面要好,其中每个项目都一个一个出现。

这是整个应用程序的唯一最佳解决方案。请参阅要放入主窗体的代码:

protected override CreateParams CreateParams 
  get 
    CreateParams cp = base.CreateParams;
    cp.ExStyle |= 0x02000000;  // Turn on WS_EX_COMPOSITED
    return cp;
  
 

【讨论】:

那么,您的意思是,汉斯两年前提供的答案实际上是正确的?谢谢你,克什蒂兹。这确实很有帮助!

以上是关于如何修复用户控件中的闪烁的主要内容,如果未能解决你的问题,请参考以下文章

如何修复错误从用户控件中的主窗体获取数据?

在winform上添加用户控件过多时出现闪烁

winform 界面加载闪烁问题

用户控件没有被绘制c#

如何修复 WPF 控件的简单 MouseOver 动画?

避免在调整大小时移动其控件的对话框上闪烁