Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is ListView rendering so slow for certain characters?

Update 1: I've written both a MFC-C++ implementation and an old-school Win32 app and recorded a video demonstrating how bad the issue really is:

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

Since the old-school Win32 app is not exhibiting this issue, this leads me to believe that C# and MFC both use the same rendering API that must cause this issue (basically discharging my suspicion that the problem might be at the OS / graphics driver level).


Original post:

While having to display some REST data inside a ListView, I encountered a very peculiar problem:

For certain inputs, the ListView rendering would literally slow to a crawl while scrolling horizontally.

On my system and with the typical subclassed ListView with "OptimizedDoubleBuffer", having a mere 6 items in a ListView will slow down rendering during scrolling to the point that i can see the headers "swimming", i.e. the rendering of the items and headers during the scrolling mismatches.

For a regular non-subclassed ListView with 10 items, I can literally see each item being drawn separately while scrolling (the repainting takes around 1-2s).

Here's example code (and yes, I am aware that these look like bear and butterfly emotes; this issue was found from user-provided data, after all):

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);
        }
    }
}

Can someone explain what the issue is, and how to resolve it?

like image 509
MrCC Avatar asked Oct 31 '22 01:10

MrCC


2 Answers

After trying a few different things, here is the fastest solution I found. There is still a little hesitation, but not anywhere near as your original solution. Until Microsoft decides to use something better than GDI+, it doesn't get better unless you go to WPF with .NET 4 and above. Oh well.

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);
    }

}

You can see for yourself the difference. If you uncomment the SendMessage and change new BufferedListView(); to new ListViewWithLessSuck(); You can see the change.

like image 182
SteveFerg Avatar answered Nov 08 '22 05:11

SteveFerg


I believe I have narrowed the problem down to Visual Styles. Commenting out Application.EnableVisualStyles(); in static void Main results in a huge performance boost during scrolling, though nowhere near the performance of the Win32 app as shown in the video that I mentioned in Update 1.

The downside of this of course is that all controls in your application will look "old". I've therefore experimented with selectively disabling / enabling of visual styles through

    [DllImport("uxtheme", ExactSpelling = true, CharSet = CharSet.Unicode)]
    public extern static Int32 SetWindowTheme(IntPtr hWnd, String textSubAppName, String textSubIdList);

and using Win32.SetWindowTheme(lv.Handle, " ", " "); as described in the MSDN docs. The most logical thing would be to keep Visual Styles active for most of the controls and turn if off for performance critical ones. However, a part of the ListView seems to deliberately ignore whether visual styles are disabled or enabled, namely the column headers of the listview in report mode:

Column Header ignores turning off visual style

(Note how the column header looks in comparison to the scroll bars)

So unless someone knows how to force visual styles off on listview column headers, this is a "pick your poison" kind of situation: Either comment out Application.EnableVisualStyles(); and have an ugly looking UI or leave it in and risk unpredictable renderer slowdowns.

If you go for the first choice, you can get another huge performance boost by subclassing the ListView and short-circuiting the WM_REFLECT_NOTIFY message (thanks to SteveFerg for the original):

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);
    }
}

Disabling visual styles and subclassing allow for rendering speeds nearly on par of that of the Win32 C app. However, I do not fully understand the potential ramifications of shorting WM_REFLECT_NOTIFY, so use with care.

I've also checked the Win32 app and confirmed that you can literally kill the rendering performance of your app simply by adding a manifest, for example like so:

// enable Visual Styles
#pragma comment( linker, "/manifestdependency:\"type='win32' \
                         name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
                         processorArchitecture='*' publicKeyToken='6595b64144ccf1df' \
                         language='*'\"")
like image 35
MrCC Avatar answered Nov 08 '22 07:11

MrCC