带有背景图像的WinForms分层控件在滚动时导致撕裂

本文关键字:滚动 撕裂 控件 分层 背景 图像 WinForms | 更新日期: 2023-09-27 18:10:00

我有一个Form具有以下属性:

    背景图片
  • 可滚动Panel与透明背景,和Dock = DockStyle.Fill
  • PictureBox, WidthHeight显示滚动条

现在所有控件都被设置为DoubleBuffered,包括表单本身。一切都像预期的那样工作,除了当滚动面板的PictureBox,表单背景图像滚动,它重复显示垂直和水平撕裂,虽然它的静态图像适合表单的大小,当你停止滚动它显示正确。这只发生在拖拽滚动条的时候,如果我点击滚动条上的任何一点来移动它,它会正确显示。

根据我的理解,双重缓冲应该消除这种情况,但即使有双重缓冲也是一样的,也许更好一点,但在滚动时仍然是一个巨大的问题。

我试着把所有的控件放在另一个面板中,而不是使用表单背景图像,并把这个面板放在表单上,但它没有任何区别

带有背景图像的WinForms分层控件在滚动时导致撕裂

你正在与一个名为"在拖动时显示窗口内容"的Windows系统选项进行战斗。所有现代版本的Windows都是开启的。关闭它不是一个现实的目标,因为它是一个系统选项,它会影响所有应用程序的所有窗口。没有后门可以选择性地绕过此选项。

启用后,操作系统会优化窗口的滚动。它执行一个快速比特来移动视频帧缓冲区中的像素,并仅为滚动显示的窗口部分生成一个绘制消息。当你向下滚动时,就像底部的几行像素。底层winapi调用是ScrollWindowEx()。目的是为应用程序提供一个更响应的UI,更少的工作要做,以实现滚动。

你可能会看到这是什么方向,ScrollWindowEx()也移动了由表单的BackgroundImage绘制的像素。你可以看到。接下来你看到的是优化的油漆的副作用,它只重新绘制窗口的部分,显示。所以移动的背景图像像素不会被重新绘制。看起来像"涂抹"效果。

有一个简单的解决方法,只需为面板的Scroll事件实现一个事件处理程序并调用Invalidate()。所以整个面板重新绘制:

    private void panel1_Scroll(object sender, ScrollEventArgs e) {
        panel1.Invalidate();
    }

但是现在您会注意到油漆不再被优化的副作用。您仍然可以看到像素被移动,然后被透支。它的可见程度很大程度上取决于绘制BackgroundImage的成本。通常不便宜,因为它没有最佳的像素格式(32bppPArgb),也没有合适的大小,所以需要重新缩放以适应窗口。视觉效果类似于"pogo",在面板的一侧快速抖动。

你不太可能会发现这是可以接受的,或者想要做优化backgrounimage的工作。阻止scrollwindowwex()完成它的工作需要一个相当大的武器,您可以调用LockWindowUpdate()。这样的:

 using System.Runtime.InteropServices;
 ...
    private void panel1_Scroll(object sender, ScrollEventArgs e) {
        if (e.Type == ScrollEventType.First) {
            LockWindowUpdate(this.Handle);
        }
        else {
            LockWindowUpdate(IntPtr.Zero);
            panel1.Update();
            if (e.Type != ScrollEventType.Last) LockWindowUpdate(this.Handle);
        }
    }
    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool LockWindowUpdate(IntPtr hWnd);

工作得很好,背景图像像素现在非常稳定。其他像素点,就不多了。另一种视觉效果,我们称之为"皱纹"。可以通过将窗口置于合成模式来消除该工件。它对整个窗口表面进行双重缓冲,包括子控件:

    protected override CreateParams CreateParams {
        get {
            const int WS_EX_COMPOSITED = 0x02000000;
            var cp = base.CreateParams;
            cp.ExStyle |= WS_EX_COMPOSITED;
            return cp;
        }
    }

唯一剩下的工件是这不是很便宜的代码的副作用。当你滚动的时候,它可能看起来不那么平滑。这就告诉了你为什么28年前窗户被设计成不透明的。

这并不容易,但它是可行的,以下工作对我来说很好,虽然花了我2个小时才发现:

首先,在将列添加到网格之前,您需要确保列的值为空,没有默认的'null'图标:

    DataGridViewImageColumn imagecol = new DataGridViewImageColumn { ImageLayout = DataGridViewImageCellLayout.Stretch };
    imagecol.DefaultCellStyle.NullValue = null;
    grid.Columns.Add(imagecol);

然后,您需要在任何事件(这里的示例是滚动事件)中为所有行删除该列值,该事件会调整或移动特定列的行大小:

private void DataGridViewScrollEventHandler(object sender, ScrollEventArgs e)
{
    if (e.ScrollOrientation == ScrollOrientation.VerticalScroll)
    {
        DataGridView grid = (DataGridView)sender;
        foreach (DataGridViewRow row in grid.Rows)
        {
            row.Cells[1].Value = null;
        }
    }
}

最后,您需要再次填充图像值的所有行,您删除它在油漆事件:

private void DataGridViewPaintEventHandler(object sender, PaintEventArgs e)
{
    DataGridView grid = (DataGridView)sender;
    foreach (DataGridViewRow row in grid.Rows)
    {
        row.Cells[1].Value = myImage;
    }
}

如果您有很多行,您需要为可见行执行此操作只是为了性能。这里有一个属性,所以它是可行的

最好的解决方案是在控件滚动事件上再次设置表单的背景图像

private void panel1_Scroll(object sender, ScrollEventArgs e) {
    /*
         Your Code if any exists
    */
    //reset the form's background image again in the scroll event
    this.BackgroundImage = Properties.Resources.your_background_image;
}