在玻璃上渲染控件:已找到解决方案,需要双缓冲/完善。

46

我(终于!)找到了一种在玻璃背景上呈现 Windows.Forms 控件的方法,它看起来没有任何主要缺点或任何大的实现时间。这是受 这篇文章 的启发,该文章基本上解释了如何本地覆盖控件的绘制以进行绘制。

我使用了这种方法将控件渲染到位图上,并使用 GDI+ 和适当的 Alpha 通道在 NativeWindow 的绘图区域上重新绘制它。这个实现很简单,但可以完善易用性,但这不是这个问题的重点。然而,结果相当令人满意:

Real textbox on glass

然而,还有两个需要修复的地方才能使其真正可用。

  1. 双缓冲,因为此覆盖图像和实际控件之间的闪烁频繁且可怕(通过代码自行测试)。使用 SetStyles(this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true) 设置基本控件为双缓存无效,但我猜测我们可以通过一些试错使其起作用。
  2. 某些控件无法工作。我已经能够让以下内容正常工作:

    • 文本框
    • 掩码组合框
    • 组合框(DropDownStyle == DropDownList)
    • 列表框
    • 复选框列表框
    • 列表视图
    • 树形视图
    • 日期选择器
    • 月历

    但我无法让这些工作,虽然我不知道为什么。我的猜测是,我引用了整个控件的实际 NativeWindow 句柄,而我需要引用它的“输入”(文本)部分,可能是子级。WinAPI 专家如何获取该输入窗口句柄的任何帮助都是受欢迎的。

    • 组合框(DropDownStyle!= DropDownList)
    • 数字选择框
    • 富文本框

但解决双缓冲将成为易用性的主要焦点

下面是一个示例用法:

new GlassControlRenderer(textBox1);

以下是代码:

public class GlassControlRenderer : NativeWindow
{
    private Control Control;
    private Bitmap Bitmap;
    private Graphics ControlGraphics;

    protected override void WndProc(ref Message m)
    {
        switch (m.Msg)
        {
            case 0xF: // WM_PAINT
            case 0x85: // WM_NCPAINT
            case 0x100: // WM_KEYDOWN
            case 0x200: // WM_MOUSEMOVE
            case 0x201: // WM_LBUTTONDOWN
                this.Control.Invalidate();
                base.WndProc(ref m);
                this.CustomPaint();
                break;

            default:
                base.WndProc(ref m);
                break;
        }
    }

    public GlassControlRenderer(Control control)
    {
        this.Control = control;
        this.Bitmap = new Bitmap(this.Control.Width, this.Control.Height);
        this.ControlGraphics = Graphics.FromHwnd(this.Control.Handle);
        this.AssignHandle(this.Control.Handle);
    }

    public void CustomPaint()
    {
        this.Control.DrawToBitmap(this.Bitmap, new Rectangle(0, 0, this.Control.Width, this.Control.Height));
        this.ControlGraphics.DrawImageUnscaled(this.Bitmap, -1, -1); // -1, -1 for content controls (e.g. TextBox, ListBox)
    }
}

我很愿意修复这个问题,并且一劳永逸地为所有.NET控件找到一个真正的在玻璃上呈现的方法,而不需要WPF。

编辑:双缓冲/抗闪烁的可能解决方案:

  • 删除this.Control.Invalidate()会消除闪烁,但会破坏文本框中的输入。
  • 我尝试过WM_SETREDRAW方法和SuspendLayout方法,但没有成功:

    [DllImport("user32.dll")]
    public static extern int SendMessage(IntPtr hWnd, Int32 wMsg, bool wParam, Int32 lParam);
    
    private const int WM_SETREDRAW = 11;
    
    public static void SuspendDrawing(Control parent)
    {
        SendMessage(parent.Handle, WM_SETREDRAW, false, 0);
    }
    
    public static void ResumeDrawing(Control parent)
    {
        SendMessage(parent.Handle, WM_SETREDRAW, true, 0);
        parent.Refresh();
    }
    
    protected override void WndProc(ref Message m)
    {
        switch (m.Msg)
        {
            case 0xF: // WM_PAINT
            case 0x85: // WM_NCPAINT
            case 0x100: // WM_KEYDOWN
            case 0x200: // WM_MOUSEMOVE
            case 0x201: // WM_LBUTTONDOWN
                //this.Control.Parent.SuspendLayout();
                //GlassControlRenderer.SuspendDrawing(this.Control);
                //this.Control.Invalidate();
                base.WndProc(ref m);
                this.CustomPaint();
                //GlassControlRenderer.ResumeDrawing(this.Control);
                //this.Control.Parent.ResumeLayout();
                break;
    
            default:
                base.WndProc(ref m);
                break;
        }
    }
    

15
我对你的进步感到印象深刻,但我也很好奇你选择这条路的原因(毫无疑问是一个好的理由,但我想了解一下!)。既然有WPF,为什么要经历这些麻烦呢? - Dennis Smit
1
非常好的工作,不介意在完成后分享完整代码和示例项目吗?根据您所说的,闪烁是由this.Control.Invalidate()引起的,您是否尝试过在没有this.Control.Invalidate()的情况下进行修复,可能需要创建一个keyDown处理程序,将所有的keyDown操作传递给当前焦点对象,然后调用重绘?(这只是一个想法,我知道很容易陷入复杂性中) - Barkermn01
1
当您设置双缓冲标志时,是否也像文档建议的那样设置了ControlStyles.AllPaintingInWmPaint? - Bryce Wagner
1
@Dennis Smit:我讨厌Xaml。我觉得设计应用程序控件不应该像设计网站控件一样:可样式化,无标准等。我认为WPF渲染的基础很好,但灵活性被高估了。但我不想在这里进行那个辩论。 - Lazlo
@series0ne 不,下面我写的答案是我能做到的最好的(尽管仍然非常实用)。 - Lazlo
显示剩余5条评论
2个回答

6
这里有一个减少了很多闪烁的版本,但仍不完美。
public class GlassControlRenderer : NativeWindow
{
    private Control Control;
    private Bitmap Bitmap;
    private Graphics ControlGraphics;

    private object Lock = new object();

    protected override void WndProc(ref Message m)
    {
        switch (m.Msg)
        {
            case 0x14: // WM_ERASEBKGND
                this.CustomPaint();
                break;

            case 0x0F: // WM_PAINT
            case 0x85: // WM_NCPAINT

            case 0x100: // WM_KEYDOWN
            case 0x101: // WM_KEYUP
            case 0x102: // WM_CHAR

            case 0x200: // WM_MOUSEMOVE
            case 0x2A1: // WM_MOUSEHOVER
            case 0x201: // WM_LBUTTONDOWN
            case 0x202: // WM_LBUTTONUP
            case 0x285: // WM_IME_SELECT

            case 0x300: // WM_CUT
            case 0x301: // WM_COPY
            case 0x302: // WM_PASTE
            case 0x303: // WM_CLEAR
            case 0x304: // WM_UNDO
                base.WndProc(ref m);
                this.CustomPaint();
                break;

            default:
                base.WndProc(ref m);
                break;
        }
    }

    private Point Offset { get; set; }

    public GlassControlRenderer(Control control, int xOffset, int yOffset)
    {
        this.Offset = new Point(xOffset, yOffset);
        this.Control = control;
        this.Bitmap = new Bitmap(this.Control.Width, this.Control.Height);
        this.ControlGraphics = Graphics.FromHwnd(this.Control.Handle);
        this.AssignHandle(this.Control.Handle);
    }

    public void CustomPaint()
    {
        this.Control.DrawToBitmap(this.Bitmap, new Rectangle(0, 0, this.Control.Width, this.Control.Height));
        this.ControlGraphics.DrawImageUnscaled(this.Bitmap, this.Offset); // -1, -1 for content controls (e.g. TextBox, ListBox)
    }
}

1

我之前遇到过闪烁的问题(窗体上有很多控件,用户控件)。尝试了几乎所有方法。这是对我有效的方法:

你尝试将这个方法放在你的窗体类中了吗?

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

在你的构造函数中,你必须启用双缓冲,否则它将无法工作:
this.DoubleBuffered = true;
this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

只有在启用了Aero时才能正常工作,否则可能会使闪烁问题更加严重。

你也可以添加这个。

  protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;

                cp.ExStyle |= 0x02000000; // WS_EX_COMPOSITED
            return cp;
        }
    } 

返回您的用户控件类。


WS_EX_COMPOSITED帮助我解决了一个略微不同的问题 - 使用作为频繁更新状态显示的RichTextBox - 我将其与带有WS_EX_COMPOSITED ExStyle + DoubleBuffered的面板一起封装,但它有一个小副作用 - 甚至插入符号也停止闪烁 - firda

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