如何解决用户控件中的闪烁


107

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

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

但这无济于事...每个控件都有相同的背景图像,但控件不同。那么解决方案是什么..
谢谢。


这些陈述在哪里?理想情况下,将它们放入构造函数中。UpdateStyles设置完这些后您打过电话吗?记录不充分,但有时可能是必要的。
托马斯

Answers:


306

这不是双缓冲可以解决的那种闪烁。也没有BeginUpdate或SuspendLayout。你有太多的控制,和backgroundImage可以把它很多更坏。

它在UserControl绘制自身时开始。它绘制BackgroundImage,在子控件窗口所在的位置留下孔。然后,每个子控件都会获得一条消息以对其进行绘制,然后将其窗口内容填充到孔中。当您有很多控件时,这些孔在一段时间内对用户可见。它们通常是白色的,与黑暗时的BackgroundImage形成鲜明对比。或者,如果表单设置了Opacity或TransparencyKey属性,则它们可以是黑色的,与几乎任何东西都形成鲜明对比。

这是Windows窗体的一个相当基本的限制,它被Windows渲染窗口的方式所困扰。由WPF btw修复,它不将窗口用于子控件。您想要的是对整个表单(包括子控件)进行双缓冲。可能的话,请在此线程中检查我的代码以获取解决方案。虽然它有副作用,但实际上并没有提高绘画速度。代码很简单,将其粘贴到您的表单中(而不是用户控件):

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

您可以采取许多措施来提高绘画速度,以至于不再出现闪烁。首先处理BackgroundImage。当源图像很大并且需要缩小以适合控件时,它们可能真的很昂贵。将BackgroundImageLayout属性更改为“ Tile”。如果这样可以显着提高速度,请返回您的绘画程序并调整图像大小,使其与典型控件尺寸更好地匹配。或在UC的OnResize()方法中编写代码以创建适当大小的图像副本,这样就不必在每次重新绘制控件时都调整其大小。对于该副本使用Format32bppPArgb像素格式,它的渲染速度比任何其他像素格式快10倍。

接下来,您可以做的是防止孔洞变得如此明显,并与图像形成鲜明对比。您可以关闭 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为我改善了用户体验。
Mahesh

绝对完美!..非常感谢
AlejandroAlis

8

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

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

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

因此,我一直在寻找一种方法来关闭WS_CLIPCHILDREN初始绘画,以便我的窗体可以很好地加载绘画,或者更好的是,只有在使用许多组件调整表单大小时才打开它。

诀窍是使应用程序以CreateParams所需的WS_EX_COMPOSITED/WS_CLIPCHILDREN样式进行调用。我在这里找到了骇客(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()调用放置在form 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()方法...
Dan W

@DanW看看此答案中张贴的URL(Angryhacker.com/blog/archive/2010/07/21/…
ChrisB

该答案中的链接似乎已失效。我对解决方案感到好奇,您是否有其他示例的链接?
普拉特(Pratt)Hinds

6

如果您正在控件中进行任何自定义绘制(即覆盖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的答案可能就是您想要的。


这是模拟doublebuffer!的创新方法。您可以if (image != null) image.Dispose();image = new Bitmap...
S.Serpooshan's


2

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


2

我尝试将其添加为评论,但我的观点不够。非常感谢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);
}

2

将下面的代码放入构造函数或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。

有关双缓冲技术的更多信息,请参见此处

我通常会重写另一个属性来解决此问题:

protected override CreateParams CreateParams
{
    get
    {
        CreateParams parms = base.CreateParams;
        parms.ExStyle |= 0x00000020; // WS_EX_COMPOSITED
        return parms;
    }
}

WS_EX_COMPOSITED -使用双缓冲按从下到上的绘制顺序绘制窗口的所有后代。

您可以在此处找到更多这些样式标志。

希望有帮助!


1

只是为了增加汉斯的回答:

(TLDR版本:透明度比您想象的要重,请在各处仅使用纯色)

如果WS_EX_COMPOSITED,DoubleBuffered和WS_CLIPCHILDREN不能解决您的闪烁(对我来说WS_CLIPCHILDREN更加糟糕),请尝试以下操作:遍历所有控件和所有代码,以及对BackColor,ForeColor或任何透明或半透明的地方任何其他颜色,只需将其删除,仅使用纯色即可。在大多数情况下,您认为只需要使用透明度,而不必使用透明度。重新设计代码和控件,并使用纯色。我的闪烁非常可怕,程序运行缓慢。删除透明度后,它会显着加速,并且闪烁为0。

编辑:要进一步补充,我只是发现WS_EX_COMPOSITED不必是整个窗口的,它可以仅应用于特定控件!这省了我很多麻烦。只需使一个自定义控件继承自您需要的任何控件,然后粘贴已发布的WS_EX_COMPOSITED替代即可。这样,您只能在此控件上获得低级双缓冲区,从而避免了应用程序其余部分的讨厌副作用!


0

我知道这个问题很老,但是想提供我的经验。

在使用.NET 4.0在Windows 8中使用Tabcontrol覆盖OnPaint和/或OnPaintBackGroundWindows 形式闪烁时,我遇到很多问题。

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

希望能帮助到你!


0

我组合了此闪烁修复程序此字体修复程序,然后我不得不添加一些我自己的代码以在绘画上启动一个计时器,以使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(带有图标)闪烁的问题。

差异结果视频:香草tabcontrol与tabcontrolex

http://gfycat.com/FineGlitteringDeermouse

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



-9

不需要任何双缓冲和所有类似的东西...

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

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

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

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

12
那么,您是说汉斯两年前提供的答案是正确的吗?谢谢你,克希兹(Kshitiz)。确实非常有帮助!
费尔南多(Fernando)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.