WinForms分层控件与背景图像在滚动时会导致撕裂现象。

5

我有一个包含以下属性的表单:

  • 背景图片
  • 可滚动的面板,具有透明背景和 Dock = DockStyle.Fill
  • PictureBox 具有大的 WidthHeight,显示滚动条

现在所有控件都设置为双缓冲,包括表单本身。除了当滚动面板时,表单背景图像会随之滚动并重复自身,显示出垂直和水平撕裂,尽管它是适合表单大小的静态图像,并且停止滚动后才能正确显示。只有在拖动滚动条时才会发生这种情况,如果我点击滚动条上的任何点来移动它,它就会正确显示。

根据我的理解,双缓冲应该消除这种情况,但即使使用双缓冲也是一样的,可能会好一点,但仍然在滚动时存在巨大问题。

我试图将所有控件放置在另一个面板中,而不是使用表单背景图像,并将此面板放置在表单上,但没有任何区别。

3个回答

14

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

启用它后,操作系统将优化窗口的滚动。它执行快速位块传输以移动视频帧缓冲区中的像素,并仅为滚动所揭示的窗口部分生成绘画消息。例如当您向下滚动时底部的几行像素。底层winapi调用是ScrollWindowEx()。意图是为应用程序提供更响应的UI,实现滚动需要做的工作少得多。

您可能已经看到了,ScrollWindowEx()还会移动通过表单的BackgroundImage绘制的像素。您可以看到这一点。接下来您会看到优化的绘画的副作用,它只会重绘被揭示出来的窗口部分。因此,移动的背景图片像素不会被重新绘制。看起来像是一个“模糊”效果。

有一个简单的解决方法,只需为面板的Scroll事件实现一个事件处理程序,并调用Invalidate()即可。这样整个面板就会再次被重新绘制:

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

然而现在你会注意到涂料不再被优化的副作用。 你仍然会看到像素被移动,然后被重绘的情况。这种效果的可见程度取决于BackgroundImage的绘制成本有多高。通常情况下成本都很高,因为它没有最佳像素格式(32bppPArgb),并且大小不合适,需要重新调整大小以适应窗口。视觉效果类似于“pogo”,面板边缘出现快速抖动。

很难想象你会接受这种情况或者想要去优化BackgroundImage。阻止ScrollWindowEx()完成其工作需要一个非常强大的武器,您可以使用pinvoke 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
完美运行,感谢详细的解释和解决方案 :) - YazX

0
最好的解决方案是在控件滚动事件中再次设置表单的背景图片。
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;
}

0

虽然不容易,但是可行的。以下方法对我有效,尽管花费了我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;
        }
    }
}

最后,在paint事件中删除的所有行,您需要再次填充图像值:
private void DataGridViewPaintEventHandler(object sender, PaintEventArgs e)
{
    DataGridView grid = (DataGridView)sender;
    foreach (DataGridViewRow row in grid.Rows)
    {
        row.Cells[1].Value = myImage;
    }
}

如果你有很多行需要进行操作,为了提高性能,你需要仅对可见的行进行操作。这个属性可以实现这一点,因此是可行的。

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