c# - 提高 WPF 列表框的绘制速度

标签 c# wpf listbox

我在 WPF 中创建了一个列表框,当用户单击“生成”时,我会在其中随机绘制二维点。就我而言,当用户单击“生成”时,我将绘制数千个点。我注意到当我生成大约 10,000 甚至 5,000 个点时,它需要很长时间。有人对加快速度的方法有建议吗?

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

enter image description here

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
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = this;

            CityList = new ObservableCollection<City>
            {
                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<City> cityList;
        public ObservableCollection<City> 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 < 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);
            }
        }

        public void populate_all_data()
        {
            CityList.Clear();
            Random rnd = new Random();

            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();
                CityList.Add(new City(name, x, y, count));
            }
        }

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

<Window x:Class="plotting.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:local="clr-namespace:plotting"
        Title="MainWindow" 
        Height="500" 
        Width="800">

    <Window.Resources>
        <ResourceDictionary>
            <local:LatValueConverter x:Key="latValueConverter" />
            <local:LongValueConverter x:Key="longValueConverter" />
            <sys:Double x:Key="mapWidth">750</sys:Double>
            <sys:Double x:Key="mapHeight">500</sys:Double>
        </ResourceDictionary>
    </Window.Resources>

        <StackPanel Orientation="Vertical" Margin="5" >
        <Button Content="Generate Batches" Click="Button_Click"></Button>
        <Button Content="Generate All" Click="Button_Click_All"></Button>

        <ItemsControl ItemsSource="{Binding CityList}">
            <!-- ItemsControlPanel -->
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>

            <!-- ItemContainerStyle -->
            <ItemsControl.ItemContainerStyle>
                <Style TargetType="ContentPresenter">
                    <Setter Property="Canvas.Left" Value="{Binding Longitude, Converter={StaticResource longValueConverter}, ConverterParameter={StaticResource mapWidth}}"/>
                    <Setter Property="Canvas.Top" Value="{Binding Latitude, Converter={StaticResource latValueConverter}, ConverterParameter={StaticResource mapHeight}}"/>
                </Style>
            </ItemsControl.ItemContainerStyle>

            <!-- ItemTemplate -->
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <!--<Button Content="{Binding Name}" />-->
                    <Ellipse Fill="#FFFFFF00" Height="15" Width="15" StrokeThickness="2" Stroke="#FF0000FF"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>

    </StackPanel>

</Window>

更新 1: 完成所有点后分配 ObservableCollection。

public void populate_data()
{
    CityList.Clear();
    Random rnd = new Random();

    List<City> tmpList = new List<City>();
    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<City>(tmpList);
}

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

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

enter image description here

最佳答案

Is it possible to only trigger the update to take place once all points have been generated, assuming that due to the ObservableCollection it's attempting to update the listbox visuals every time a new point is added to the collection.

实际上,这不是一个正确的假设。事实上,ListBox 已经将更新推迟到您添加完项目为止。您可以通过修改您的 Click 处理程序来观察这一点(已将适当的 ElapsedToIdle 属性添加到您的窗口类并将其绑定(bind)到 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 更新的情况下在后台任务中运行初始化。这会产生介于上述两个选项之间的东西。也就是说,它仍然比没有更新的初始化慢,但它比 initialize-and-update-in-UI-thread 选项快一点,因为只涉及一点并发性(我实现它以便它会启动一个任务计算下一批对象,然后在该任务运行时,添加前一批对象并等待更新完成)。但是,我可能不会在实际程序中使用这种方法,因为虽然它比只在 UI 线程中执行所有操作要好一点,但也不是那样好得多,而且它显着增加了代码。

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

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

关于c# - 提高 WPF 列表框的绘制速度,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48067541/

相关文章:

c# - 如何在 WPF 中编辑 DataGrid 中的值?

c# - MVVM ICommand.CanExecute 参数包含以前的值

wpf - 在 ComboBoxItem 中设置文本颜色

wpf - ListBox 总是自动选择第一项

c# - 获取 RadListBox 中的 Checked 复选框 (telerik)

c# - Asp.net ThreadPool 中可用的最大线程数是多少

c# - 检查存储过程是否返回值

javascript - 在发布到 API 之前更改数组 javascript 中的一些值

c# - ListBox Item 将冲突的属性返回给子项

c# - 创建一个国际网站