提高WPF列表框的绘制速度 [英] Improve draw speeds for WPF Listbox

查看:55
本文介绍了提高WPF列表框的绘制速度的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在WPF中创建了一个列表框,当用户单击生成"时,我会在其中随机绘制2D点.就我而言,当用户单击生成"时,我将绘制数千个点.我注意到当我产生大约10,000甚至5,000点时,它会永远存在.有没有人建议如何加快速度?

I have created a Listbox in WPF, where I plot 2D points randomly when the user clicks Generate. In my case I'm going to be plotting several thousand points when the user clicks Generate. I noticed when I generate around 10,000 or even 5,000 points, it takes forever. Does anyone have advice on ways to speed this up?

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

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.

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.

Update 1: Assign the ObservableCollection once all the points have been made.

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在添加对象时进行更新?

This change does not affect the UI experience much, if at all. Is there a way to allow the UI to update as the objects are added?

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

End goal is plotting just points representing each coordinate in 2D space.

推荐答案

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

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属性添加到窗口类并将其绑定到TextBlock进行显示)来观察到这一点:

Actually, that's not a correct assumption. In fact, ListBox already will defer updates until you're done adding items. You can observe this by modifying your Click handler (having added the appropriate ElapsedToIdle property to your window class and bound it to a TextBlock for display, of course):

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

    populate_data();
    ElapsedToIdle = sw.Elapsed;
}

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

The problem is that even though it's deferring updates, when it finally gets around to processing all the new data, it still does that in the UI thread. With the above, I see the elapsed time at around 800 ms on my computer. So, the populate_data() method is only taking that long. If, however, I change the method so it measures the time until the UI thread returns to an idle state:

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秒范围内(变化).

…the actual time is in the 10-12 second range (it varies).

从用户的角度来看,操作花费的时间可能不那么重要,而在进行初始化时整个程序似乎都已锁定,这一点可能不那么重要.这可以通过更改代码来解决,以便在初始化发生时让UI有机会进行更新.

From the user point of view, it may be less important that the operation takes so much time, than that the entire program appears to lock up while the initialization is taking place. This can be addressed by changing the code so that the UI gets chances to update while the initialization occurs.

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

We can modify the initialization code like this to accomplish that:

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秒.出于明显的原因,它的速度较慢.但是,用户看到的是一个逐渐填充的用户界面,它为用户提供了有关发生情况的更好反馈,并减少了等待时间.

This adds 4-5 seconds to the initialization time. For obvious reasons, it's slower. But, what the user sees is a gradually populated UI, giving them better feedback as to what's going on, and making the wait less onerous.

关于它的价值,我还尝试了在允许UI更新的同时在后台任务中运行初始化.这在以上两个选项之间产生了一些东西.也就是说,它仍然比不进行更新的初始化要慢,但是它比UI线程中的initialize-and-update-in-update选项要快一些,因为涉及到一些并发性(我实现了并发性,以便它可以启动任务来计算下一批对象,然后在该任务运行时,添加上一批对象并等待该更新完成.但是,我可能不会在实际程序中使用该方法,因为虽然它比仅在UI线程中完成所有操作要好一些,但它并不是 更好,并且显着增加了代码.

For what it's worth, I also experimented with running the initialization in a background task while the UI was allowed to update. This produces something in between the above two options. That is, it's still slower than initializing without updates, but it's a bit faster than the initialize-and-update-in-UI-thread option, because there's just a bit of concurrency involved (I implemented it so that it would start a task to compute the next batch of objects, and then while that task was running, add the previous batch of objects and wait for that update to complete). But, I probably wouldn't use that approach in a real program, because while it's a bit better than just doing everything in the UI thread, it's not that much better, and it significantly increases the complexity of the code.

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

Note that adjusting the batch size has important effects on the trade-off between responsiveness and speed. Larger batch sizes will run faster overall, but the UI is more likely to stall and/or be completely unresponsive.

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

Now, all that said, one important question is, do you really need to use ListBox here? I ran the code using a plain ItemsControl instead, and it was 2x to 3x faster, depending on the exact scenario. I assume you are using the ListBox control to give selection feedback, and that's fine. But if speed is really important, you might find it makes more sense to use ItemsControl and handle item selection yourself.

这篇关于提高WPF列表框的绘制速度的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆