【问题标题】:How to prevent a borderless Windows Form from flickering when resizing (C#)?如何防止无边框 Windows 窗体在调整大小时闪烁(C#)?
【发布时间】:2020-06-04 19:26:32
【问题描述】:

[C# .NET 4.0]

我正在学习 C#,我正在尝试使用具有 FormBorderStyle = FormBorderStyle.None 并且可以使用 Windows API 移动/调整大小的 C# 构建 Windows 窗体。例如,我使用用于 Google Chrome 和 Norton 360 的圆角或自定义(可移动/可调整大小)边框设计作为表单的基础。

到目前为止,我已经取得了很大进展,并且一切正常,除了 当我调整表单大小时,当您调整表单大小时,沿右边框和下边框的长度会出现黑/白闪烁快速形成

我尝试在构造函数中添加this.DoubleBuffer = true,也尝试过this.SetStyles(ControlStyles.AllPaintInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true);

因为我是图形方面的傻瓜,所以我喜欢控制表单的完整设计,所以我可以看到这会永远困扰我......所以如果有人可以帮我解决这个问题,让闪烁不再发生,这对我的学习过程非常有用。

我还应该提到我使用的是 Windows XP,所以我不确定 this post 是否会帮助我,因为它似乎专注于 Vista/7(使用 DWM)。 ..并不是说我已经足够先进,无法理解该帖子中的所有内容。

使用 API 的两部分代码如下。我有一个用于 Windows API 的 WM_NCHITTEST 的公共枚举...您可以看到值 in this link

OnPaint 覆盖方法

protected override void OnPaint(PaintEventArgs e)
{
    System.IntPtr ptrBorder = CreateRoundRectRgn(0, 0,
        this.ClientSize.Width, this.ClientSize.Height, 15, 15);

    SetWindowRgn(this.Handle, ptrBorder, true);

    Rectangle rc = new Rectangle(this.ClientSize.Width - cGrip,
        this.ClientSize.Height - cGrip, cGrip, cGrip);
    ControlPaint.DrawSizeGrip(e.Graphics, this.BackColor, rc);
    rc = new Rectangle(0, 0, this.ClientSize.Width, 32);
    e.Graphics.FillRectangle(Brushes.SlateGray, rc);
}

WndProc 覆盖方法

protected override void WndProc(ref Message m)
{
    if (m.Msg == (int)HitTest.WM_NCHITTEST)
    {
        // Trap WM_NCHITTEST
        Point pos = new Point(m.LParam.ToInt32() & 0xffff, m.LParam.ToInt32() >> 16);
        pos = this.PointToClient(pos);

        if (pos.Y < cCaption)
        {
            m.Result = (IntPtr)HitTest.HTCAPTION;
            return;
        }

        if (pos.X <= cGrip && pos.Y >= this.ClientSize.Height - cGrip)
        {
            m.Result = (IntPtr)HitTest.HTBOTTOMLEFT;
            return;
        }

        if (pos.X >= this.ClientSize.Width - cGrip &&
            pos.Y >= this.ClientSize.Height - cGrip)
        {
            m.Result = (IntPtr)HitTest.HTBOTTOMRIGHT;
            return;
        }

        if (pos.X >= this.ClientSize.Width - cBorder)
        {
            m.Result = (IntPtr)HitTest.HTRIGHT;
            return;
        }

        if (pos.Y >= this.ClientSize.Height - cBorder)
        {
            m.Result = (IntPtr)HitTest.HTBOTTOM;
            return;
        }

        if (pos.X <= cBorder)
        {
            m.Result = (IntPtr)HitTest.HTLEFT;
            return;
        }
    }

    base.WndProc(ref m);
}

这是完整的代码

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace PracticeForm
{
    public partial class Form2 : Form
    {
        [DllImport("user32.dll")]
        private static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);

        [DllImport("gdi32.dll")]
        private static extern IntPtr CreateRoundRectRgn(int x1, int y1, int x2, int y2, int cx, int cy);

        [DllImport("gdi32.dll", EntryPoint = "DeleteObject")]
        private static extern bool DeleteObject(System.IntPtr hObject);

        private const int cGrip = 20;
        private const int cCaption = 35;
        private const int cBorder = 7;
        private Point mouseOffset;

        public Form2()
        {
            InitializeComponent();
            this.FormBorderStyle = FormBorderStyle.None;
            this.MaximumSize = new Size(670, 440);
            this.DoubleBuffered = true;
            this.SetStyle(ControlStyles.ResizeRedraw |
                          ControlStyles.OptimizedDoubleBuffer |
                          ControlStyles.AllPaintingInWmPaint |
                          ControlStyles.UserPaint, true);
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            System.IntPtr ptrBorder = CreateRoundRectRgn(0, 0,
                this.ClientSize.Width, this.ClientSize.Height, 15, 15);

            SetWindowRgn(this.Handle, ptrBorder, true);

            Rectangle rc = new Rectangle(this.ClientSize.Width - cGrip,
                this.ClientSize.Height - cGrip, cGrip, cGrip);
            ControlPaint.DrawSizeGrip(e.Graphics, this.BackColor, rc);
        }

        protected override void WndProc(ref Message m)
        {
            if (m.Msg == (int)HitTest.WM_NCHITTEST)
            {
                // Trap WM_NCHITTEST
                Point pos = new Point(m.LParam.ToInt32() & 0xffff, m.LParam.ToInt32() >> 16);
                pos = this.PointToClient(pos);

                if (pos.Y < cCaption)
                {
                    m.Result = (IntPtr)HitTest.HTCAPTION;
                    return;
                }

                if (pos.X <= cGrip && pos.Y >= this.ClientSize.Height - cGrip)
                {
                    m.Result = (IntPtr)HitTest.HTBOTTOMLEFT;
                    return;
                }

                if (pos.X >= this.ClientSize.Width - cGrip &&
                    pos.Y >= this.ClientSize.Height - cGrip)
                {
                    m.Result = (IntPtr)HitTest.HTBOTTOMRIGHT;
                    return;
                }

                if (pos.X >= this.ClientSize.Width - cBorder)
                {
                    m.Result = (IntPtr)HitTest.HTRIGHT;
                    return;
                }

                if (pos.Y >= this.ClientSize.Height - cBorder)
                {
                    m.Result = (IntPtr)HitTest.HTBOTTOM;
                    return;
                }

                if (pos.X <= cBorder)
                {
                    m.Result = (IntPtr)HitTest.HTLEFT;
                    return;
                }
            }

            base.WndProc(ref m);
        }

        private void button1_Click(object sender, EventArgs e)
        {
            this.Close();
        }

        private void button2_MouseClick(object sender, MouseEventArgs e)
        {
            this.WindowState = FormWindowState.Minimized;
        }

        private void panel1_MouseDown(object sender, MouseEventArgs e)
        {
            mouseOffset = new Point(-e.X, -e.Y);
        }

        private void panel1_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                Point p = Control.MousePosition;
                p.Offset(mouseOffset.X, mouseOffset.Y);
                Location = p;
            }
        }

        private void label1_MouseDown(object sender, MouseEventArgs e)
        {
            mouseOffset = new Point(-e.X, -e.Y);
        }

        private void label1_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                Point p = Control.MousePosition;
                p.Offset(mouseOffset.X, mouseOffset.Y);
                Location = p;
            }
        }
    }
}

感谢您的帮助。

【问题讨论】:

    标签: c# .net winforms winapi windows-xp


    【解决方案1】:

    闪烁的发生是因为您的显示区域正在迅速改变颜色,而这反过来又是因为您过度绘制 - 在同一个像素上绘制多个东西。

    发生这种情况是因为:

    • 如果您的重绘速度很慢,那么您正在绘制的屏幕上的内容(例如窗口边框)将在一段时间内可见。例如用户可能会看到滚动条的两个副本,直到您用表单的内容擦除旧的滚动条。
    • windows 会自动为您擦除窗口的背景,通常为白色。因此,绘图中任何不是白色的区域都会闪烁白色,然后再用正确的图像对其进行过度绘制。
    • 如果您在同一个位置绘制多个东西,当您不断更改屏幕该区域的颜色时,您会看到闪烁

    要解决这些问题,您需要综合考虑(越多越好)

    • 禁用擦除背景,或将擦除颜色设置为图像中的主色
    • 优化您的重绘代码,使其更快,使闪烁不那么明显
    • 优化您的重绘代码以消除过度绘制。例如。要在矩形页面周围放置边框,您可以绘制背景颜色并将其与页面过度绘制,但这会闪烁。相反,将顶部边框绘制为矩形,然后将左下角和右下角绘制为中间,然后在中间绘制页面。由于 nopixels 被多次绘制,它不会闪烁
    • 在您的控件上启用双缓冲模式。这样一来,你所有的绘图实际上都是在内存中生成位图图像,然后将最终图像复制到屏幕上,这样每个像素只显示一次,没有闪烁。

    【讨论】:

    • 感谢您抽出宝贵时间来解释...但是,由于我仍在学习,您写的很多内容还没有任何意义(尽管我会查找一些内容) )。我确实想指出,我在帖子中指出,我不仅使用了您提到的 DoubleBuffered 模式,而且 SetStyles(ControlStyles...) 也无济于事。
    • 我确实看到我同时在同一个地方画了两件东西,但我还没有足够的经验来实施你的建议来解决我的矩形之间的像素绘画冲突'正在绘制圆角我的表单和我正在绘制的矩形用作 SizeGrip。
    • 你正在为“经验不足”做一些相当高级的事情 :-) 尝试覆盖 OnPaintBackground (什么都不做,以避免重复绘制),并优化你的代码,这样你就不会这样做OnPaint 中不需要的任何内容 - 例如在 OnSizeChanged 中创建您的区域/矩形,而不是在每个绘制请求上。另请注意,更改窗口区域可能会导致重绘发生,这本身可能会导致闪烁。
    • 嗯,我有一点基本的 C++ 控制台经验,所以基本代码并没有那么不同,我能够从我从网上收集的示例中拼凑出很多这些。我会在 OnPaintBackground 的覆盖中添加什么...只是默认的 base.OnPaintBackground(e) 就是这样?
    • 不,你想阻止它调用 base.OnPaintPackground - 要么不画任何东西(如果你的画会填满整个区域),要么画一种可以最小化闪烁的颜色。通常最好什么都不做,然后在 OnPaint 中填充背景,这样可以最大限度地减少从清除窗口到绘制其内容的时间。
    【解决方案2】:

    虽然这是一个相当老的线程,并且 OP 可能已经找到了解决他的问题的方法并继续前进,但我想添加一些额外的点,以防它们被证明对正在工作的 .NET 开发人员有益类似的问题。

    首先,感谢您尝试在 Windows XP 上解决此问题。我去过那里,在那里呆了很多小时,因此学到了所有的艰难教训。不幸的是,由于 Windows XP 缺少我们大多数人已经习惯的 DWM,因此没有简单的解决方案。

    正确设置 ControlStyles 是绝对重要的——我还要包括:

    SetStyle(ControlStyles.Opaque, True)
    

    对要绘制的控件进行双重缓冲很重要,因为闪烁主要是由在监视器垂直回扫中间重绘的控件引起的。仅仅因为您调用了 Invalidate(),这并不一定意味着控件将在您想要的时候重绘——您受 Windows 的摆布,操作系统会在它准备好时执行此操作。您可以通过利用 DirecDraw 7 中的 WaitForVerticalBlank 之类的函数(Windows XP 上对该 API 的大量支持)以及使用 GetVerticalBlankStatus 和 GetScanLine 来相应地为您的渲染和演示计时来解决这个问题(就像我所做的那样)。

    【讨论】:

      【解决方案3】:

      仅在 Form 实际改变 SIZE 时设置 Region,而不是每次在 Paint() 事件中设置:

          protected override void OnSizeChanged(EventArgs e)
          {
              base.OnSizeChanged(e);
      
              System.IntPtr ptrBorder = CreateRoundRectRgn(0, 0,
                  this.ClientSize.Width, this.ClientSize.Height, 15, 15);
      
              SetWindowRgn(this.Handle, ptrBorder, true);
          }
      
          protected override void OnPaint(PaintEventArgs e)
          {
      
              Rectangle rc = new Rectangle(this.ClientSize.Width - cGrip,
                  this.ClientSize.Height - cGrip, cGrip, cGrip);
              ControlPaint.DrawSizeGrip(e.Graphics, this.BackColor, rc);
          }
      

      【讨论】:

      • 这可能让它稍微好一点,但它仍然闪烁得很厉害。另外,由于某种原因,您的代码似乎覆盖了我的表单开始位置(CenterScreen)。
      • 您是否有图像作为表单的背景?...或者可能在 Paint() 事件中进行了一些更复杂的绘画?
      • 不,表单中的所有内容都是在代码中创建的(我的所有代码,除了枚举类,都在我的帖子中列出)。它只是一个矩形区域,覆盖无边界形式以创建圆角(据我所知)。当然,还有用于自定义 SizeGrip 的附加矩形。我认为创建圆角的矩形是导致问题的原因,因为当我完全删除 CreateRoundRectRgn 代码时,它似乎没有闪烁(或闪烁几乎一样糟糕)。但是圆角对我来说是一个重要的视觉实现。
      • 我运行的是 Win 8,所以我不确定问题是否出在 XP 上。我的系统几乎没有闪烁。
      【解决方案4】:

      试试这个:

      how to stop flickering C# winforms

      我的无边框表单上有面板作为标题栏,我在标题栏面板上闪烁时遇到问题,并在表单加载中添加了此代码,闪烁消失了。

      var prop = TitleBar_panel.GetType().GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic);
                  prop.SetValue(TitleBar_panel, true, null);
      

      TitleBar_panel 是闪烁的控件。

      编辑:现在只有当我从表单的左侧调整它的大小时它才会闪烁。所以这段代码不是 100% 解决的

      【讨论】:

        猜你喜欢
        • 2010-11-22
        • 2019-01-09
        • 1970-01-01
        • 2011-01-22
        • 2019-02-27
        • 2012-08-30
        • 1970-01-01
        • 2020-09-11
        • 1970-01-01
        相关资源
        最近更新 更多