为什么 ListView 对某些字符的渲染速度如此之慢?

Why is ListView rendering so slow for certain characters?

更新 1: 我已经编写了一个 MFC-C++ 实现和一个 old-school Win32 应用程序,并录制了一个视频来演示这个问题有多严重真的是:

https://www.youtube.com/watch?v=f0CQhQ3GgAM

由于 old-school Win32 应用程序没有出现这个问题,这让我相信 C# 和 MFC 都使用相同的渲染 API 一定会导致这个问题(基本上消除了我的怀疑问题可能出在 OS / 图形 driver 级别)。


原文post:

当必须在 ListView 中显示一些 REST 数据时,我遇到了一个非常特殊的问题:

对于某些输入,ListView 呈现在水平滚动时会慢得像爬行一样。

在我的系统中,使用带有 "OptimizedDoubleBuffer" 的典型子类 ListView,ListView 中只有 6 个项目会在滚动到我可以看到 headers "swimming",即项目的呈现和 headers 在滚动期间不匹配。

对于包含 10 个项目的常规 non-subclassed ListView,我可以在滚动时看到每个项目被单独绘制(重新绘制大约需要 1-2 秒)。

这是示例代码(是的,我知道这些看起来像熊和蝴蝶表情;毕竟这个问题是从 user-provided 数据中发现的):

using System;
using System.Windows.Forms;

namespace SlowLVRendering
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            this.Load += new System.EventHandler(this.Form1_Load);
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            const string slow = "ヽ(  ´。㉨°)ノ Ƹ̴Ӂ̴Ʒ~ ღ ( ヽ(  ´。㉨°)ノ  ༼ つ´º㉨º ༽つ ) (」゚ペ)」ヽ(  ´。㉨°)ノ Ƹ̴Ӂ̴Ʒ~ ღ ( ヽ(  ´。㉨°)ノ  ༼ つ´º㉨º ༽つ ) (」゚ペ)」";
            ListView lv = new ListView();
            lv.Dock = DockStyle.Fill;
            lv.View= View.Details;
            for (int i = 0; i < 2; i++) lv.Columns.Add("Title "+i, 500);
            for (int i = 0; i < 10; i++)
            {
                var lvi = lv.Items.Add(slow);
                lvi.SubItems.Add(slow);
            }
            Controls.Add(lv);
        }
    }
}

谁能解释一下问题是什么以及如何解决?

在尝试了几种不同的方法后,这是我找到的最快的解决方案。仍然有一点犹豫,但与您的原始解决方案相去甚远。在 Microsoft 决定使用比 GDI+ 更好的东西之前,它不会变得更好,除非你使用 .NET 4 及更高版本的 WPF。好吧。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace SlowLVRendering
{
    [System.Runtime.InteropServices.DllImport("user32")]
    private static extern bool SendMessage(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam);

    private uint LVM_SETTEXTBKCOLOR = 0x1026;

    public partial class Form1 : Form
    {

        public Form1()
        {
            InitializeComponent();
            this.Load += new System.EventHandler(this.Form1_Load);

        }
        private void Form1_Load(object sender, EventArgs e)
        {
            const string slow = "ヽ(  ´。㉨°)ノ Ƹ̴Ӂ̴Ʒ~ ღ ( ヽ(  ´。㉨°)ノ  ༼ つ´º㉨º ༽つ ) (」゚ペ)」ヽ(  ´。㉨°)ノ Ƹ̴Ӂ̴Ʒ~ ღ ( ヽ(  ´。㉨°)ノ  ༼ つ´º㉨º ༽つ ) (」゚ペ)」";
            ListView lv = new BufferedListView();
            // new ListView();
            //new ListViewWithLessSuck();

            lv.Dock = DockStyle.Fill;
            lv.View = View.Details;
            for (int i = 0; i < 2; i++) lv.Columns.Add("Title " + i, 500);
            for (int i = 0; i < 10; i++)
            {
                var lvi = lv.Items.Add(slow);
                lvi.SubItems.Add(slow);
            }
            Controls.Add(lv);
        //SendMessage(lv.Handle, LVM_SETTEXTBKCOLOR, IntPtr.Zero, unchecked((IntPtr)(int)0xFFFFFF));

        }

    }
    public class BufferedListView : ListView
    {
        public BufferedListView()
            : base()
        {
            SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
        }
    }
class ListViewWithLessSuck : ListView
{
    [StructLayout(LayoutKind.Sequential)]
    private struct NMHDR
    {
        public IntPtr hwndFrom;
        public uint idFrom;
        public uint code;
    }

    private const uint NM_CUSTOMDRAW = unchecked((uint)-12);

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == 0x204E)
        {
            NMHDR hdr = (NMHDR)m.GetLParam(typeof(NMHDR));
            if (hdr.code == NM_CUSTOMDRAW)
            {
                m.Result = (IntPtr)0;
                return;
            }
        }

        base.WndProc(ref m);
    }

}

您可以亲眼看到其中的区别。如果您取消注释 SendMessage 并将 new BufferedListView(); 更改为 new ListViewWithLessSuck(); 您可以看到更改。

我相信我已经将问题缩小到 视觉样式。在 static void Main 中注释掉 Application.EnableVisualStyles(); 会导致滚动期间的性能大幅提升,但远不及我在更新 1 中提到的视频中显示的 Win32 应用程序的性能。

这样做的缺点当然是应用程序中的所有控件看起来 "old"。因此,我尝试通过

有选择地禁用/启用视觉样式
    [DllImport("uxtheme", ExactSpelling = true, CharSet = CharSet.Unicode)]
    public extern static Int32 SetWindowTheme(IntPtr hWnd, String textSubAppName, String textSubIdList);

并使用 Win32.SetWindowTheme(lv.Handle, " ", " ");,如 MSDN 文档中所述。最合乎逻辑的做法是为大多数控件保持视觉样式处于活动状态,如果关闭则关闭性能关键控件。然而,ListView的一部分似乎故意忽略了视觉样式是禁用还是启用,即报告模式下列表视图的列headers:

(注意 header 列与滚动条相比的外观)

所以除非有人知道如何在列表视图列 header 上强制关闭视觉样式,否则这是一种 "pick your poison" 的情况:要么注释掉 Application.EnableVisualStyles();并有一个丑陋的外观 UI 或保留它并冒着不可预知的渲染器减速的风险。

如果你选择第一个,你可以通过子类化 ListView 和 short-circuiting WM_REFLECT_NOTIFY 消息(感谢 SteveFerg 的原创)来获得另一个巨大的性能提升:

public class ListViewWithoutReflectNotify : ListView
{
    [StructLayout(LayoutKind.Sequential)]
    private struct NMHDR
    {
        public IntPtr hwndFrom;
        public uint idFrom;
        public uint code;
    }

    private const uint NM_CUSTOMDRAW = unchecked((uint) -12);


    public ListViewWithoutReflectNotify()
    {

    }
    protected override void WndProc(ref Message m)
    {
        // WM_REFLECT_NOTIFY
        if (m.Msg == 0x204E)
        {
            m.Result = (IntPtr)0;
            return;

            //the if below never was true on my system so i 'shorted' it
            //delete the 2 lines above if you want to test this yourself
            NMHDR hdr = (NMHDR) m.GetLParam(typeof (NMHDR));
            if (hdr.code == NM_CUSTOMDRAW)
            {
                Debug.WriteLine("Hit");
                m.Result = (IntPtr) 0;
                return;
            }
        }

        base.WndProc(ref m);
    }
}

禁用视觉样式和子类化允许渲染速度几乎与 Win32 C 应用程序相当。但是,我并不完全理解短路 WM_REFLECT_NOTIFY 的潜在后果,因此请谨慎使用。

我还检查了 Win32 应用程序并确认您可以通过简单地添加清单来真正地降低应用程序的渲染性能,例如:

// enable Visual Styles
#pragma comment( linker, "/manifestdependency:\"type='win32' \
                         name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
                         processorArchitecture='*' publicKeyToken='6595b64144ccf1df' \
                         language='*'\"")