提高WPF列表框的绘制速度

时间:2018-01-02 20:39:16

标签: c# wpf listbox

我在WPF中创建了一个Listbox,我在用户单击Generate时随机绘制2D点。在我的情况下,当用户点击Generate时,我将绘制几千个点。我注意到当我产生大约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

1 个答案:

答案 0 :(得分:2)

  

是否有可能仅在生成所有点后才触发更新,假设由于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并自己处理项目选择更有意义。