提高WPF Listbox的绘制速度

我在WPF中创建了一个Listbox,我在用户单击Generate时随机绘制2D点。 在我的情况下,当用户点击Generate时,我将绘制几千个点。 我注意到当我产生大约10,000或甚至5,000点时,它需要永远。 有没有人就如何加快这个问题提出建议?

是否有可能仅在生成所有点后才触发更新,假设由于ObservableCollection它每次将新点添加到集合时都尝试更新列表框视觉效果。

在此处输入图像描述

MainWindow.xaml.cs

using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Threading.Tasks; using System.Windows; using System.Windows.Data; using System.Windows.Threading; namespace plotting { ///  /// Interaction logic for MainWindow.xaml ///  public partial class MainWindow : Window, INotifyPropertyChanged { public MainWindow() { InitializeComponent(); this.DataContext = this; CityList = new ObservableCollection { new City("Duluth", 92.18, 46.83, 70), new City("Redmond", 121.15, 44.27, 50), new City("Tucson", 110.93, 32.12, 94), new City("Denver", 104.87, 39.75, 37), new City("Boston", 71.03, 42.37, 123), new City("Tampa", 82.53, 27.97, 150) }; } private ObservableCollection cityList; public ObservableCollection CityList { get { return cityList; } set { cityList = value; RaisePropertyChanged("CityList"); } } // INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged = delegate { }; private void RaisePropertyChanged(string propName) { PropertyChanged(this, new PropertyChangedEventArgs(propName)); } public async Task populate_data() { CityList.Clear(); const int count = 5000; const int batch = 100; int iterations = count / batch, remainder = count % batch; Random rnd = new Random(); for (int i = 0; i < iterations; i++) { int thisBatch = _GetBatchSize(batch, ref remainder); for (int j = 0; j  { }, DispatcherPriority.ApplicationIdle); } } public void populate_all_data() { CityList.Clear(); Random rnd = new Random(); for (int i = 0; i  0) { thisBatch = batch + 1; remainder--; } else { thisBatch = batch; } return thisBatch; } private async void Button_Click(object sender, RoutedEventArgs e) { Stopwatch sw = Stopwatch.StartNew(); await populate_data(); Console.WriteLine(sw.Elapsed); } private void Button_Click_All(object sender, RoutedEventArgs e) { Stopwatch sw = Stopwatch.StartNew(); populate_all_data(); Console.WriteLine(sw.Elapsed); } } public class City { public string Name { get; set; } // east to west point public double Longitude { get; set; } // north to south point public double Latitude { get; set; } // Size public int Population { get; set; } public City(string Name, double Longitude, double Latitude, int Population) { this.Name = Name; this.Longitude = Longitude; this.Latitude = Latitude; this.Population = Population; } } public static class Constants { public const double LongMin = 65.0; public const double LongMax = 125.0; public const double LatMin = 25.0; public const double LatMax = 50.0; } public static class ExtensionMethods { public static double Remap(this double value, double from1, double to1, double from2, double to2) { return (value - from1) / (to1 - from1) * (to2 - from2) + from2; } } public class LatValueConverter : IValueConverter { // Y Position public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { double latitude = (double)value; double height = (double)parameter; int val = (int)(latitude.Remap(Constants.LatMin, Constants.LatMax, height, 0)); return val; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } public class LongValueConverter : IValueConverter { // X position public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { double longitude = (double)value; double width = (double)parameter; int val = (int)(longitude.Remap(Constants.LongMin, Constants.LongMax, width, 0)); return val; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } } 

MainWindow.xaml

      750 500                       <!--

更新1:一旦完成所有点,就分配ObservableCollection。

 public void populate_data() { CityList.Clear(); Random rnd = new Random(); List tmpList = new List(); for (int i = 0; i < 5000; i++) { int x = rnd.Next(65, 125); int y = rnd.Next(25, 50); int count = rnd.Next(50, 200); string name = x.ToString() + "," + y.ToString(); tmpList.Add(new City(name, x, y, count)); } CityList = new ObservableCollection(tmpList); } 

如果有的话,此更改不会对UI体验产生太大影响。 有没有办法允许UI在添加对象时更新?

最终目标是仅绘制表示2D空间中每个坐标的点。

在此处输入图像描述

是否有可能仅在生成所有点后才触发更新,假设由于ObservableCollection它每次将新点添加到集合时都尝试更新列表框视觉效果。

实际上,这不是一个正确的假设。 事实上, ListBox已经推迟更新,直到你完成添加项目。 你可以通过修改你的Click处理程序来观察这一点(在你的窗口类中添加了适当的ElapsedToIdle属性并将其绑定到TextBlock以供显示):

 private void Button_Click(object sender, RoutedEventArgs e) { Stopwatch sw = Stopwatch.StartNew(); populate_data(); ElapsedToIdle = sw.Elapsed; } 

问题在于即使它推迟了更新,当它最终处理所有新数据时,它仍然在UI线程中执行。 有了上述内容,我在计算机上看到了大约800毫秒的经过时间。 所以, populate_data()方法只花了这么长时间。 但是,如果我更改了方法,那么它会测量UI线程返回空闲状态的时间:

 private async void Button_Click(object sender, RoutedEventArgs e) { Stopwatch sw = Stopwatch.StartNew(); var task = Dispatcher.InvokeAsync(() => sw.Stop(), DispatcherPriority.ApplicationIdle); populate_data(); await task; ElapsedToIdle = sw.Elapsed; } 

…实际时间在10-12秒范围内(变化)。

从用户的角度来看,操作花费这么多时间可能不太重要,因为整个程序在初始化过程中似乎锁定了。 这可以通过更改代码来解决,以便UI在初始化发生时有机会更新。

我们可以像这样修改初始化代码来实现:

 public async Task populate_data() { CityList.Clear(); const int count = 5000; const int batch = 50; int iterations = count / batch, remainder = count % batch; Random rnd = new Random(); for (int i = 0; i < iterations; i++) { int thisBatch = _GetBatchSize(batch, ref remainder); for (int j = 0; j < batch; j++) { int x = rnd.Next(65, 125); int y = rnd.Next(25, 50); int popoulation = rnd.Next(50, 200); string name = x.ToString() + "," + y.ToString(); CityList.Add(new City(name, x, y, popoulation)); } await Dispatcher.InvokeAsync(() => { }, DispatcherPriority.ApplicationIdle); } } private static int _GetBatchSize(int batch, ref int remainder) { int thisBatch; if (remainder > 0) { thisBatch = batch + 1; remainder--; } else { thisBatch = batch; } return thisBatch; } private async void Button_Click(object sender, RoutedEventArgs e) { Stopwatch sw = Stopwatch.StartNew(); await populate_data(); ElapsedToIdle = sw.Elapsed; ButtonEnabled = true; } 

这会使初始化时间增加4-5秒。 出于显而易见的原因,它的速度较慢。 但是,用户看到的是逐渐填充的用户界面,为他们提供了更好的反馈,使得等待更加繁重。

为了它的价值,我还尝试在允许UI更新的同时在后台任务中运行初始化。 这产生了上述两个选项之间的东西。 也就是说,它仍然比没有更新的初始化慢,但它比初始化和更新在UI线程选项快一点,因为只涉及一点并发(我实现它以便它将启动任务计算下一批对象,然后在该任务运行时,添加上一批对象并等待该更新完成)。 但是,我可能不会在真正的程序中使用该方法,因为虽然它比仅仅在UI线程中执行所有操作要好一些,但它并没有那么好,并且它显着增加了代码的复杂性。

请注意,调整批量大小对响应速度和速度之间的权衡有重要影响。 较大的批量大小将整体运行得更快,但UI更可能停滞和/或完全没有响应。

现在,所有这一切,一个重要的问题是,你真的需要在这里使用ListBox吗? 我使用普通的ItemsControl来运行代码,它的速度提高了2到3倍,具体取决于具体情况。 我假设你使用ListBox控件来提供选择反馈,这没关系。 但是如果速度非常重要,您可能会发现使用ItemsControl并自己处理项目选择更有意义。