【问题标题】:Improve draw speeds for WPF Listbox提高 WPF 列表框的绘制速度
【发布时间】:2018-01-02 20:39:16
【问题描述】:

我在 WPF 中创建了一个列表框,当用户单击“生成”时,我会在其中随机绘制 2D 点。就我而言,当用户单击“生成”时,我将绘制几千个点。我注意到当我生成大约 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
{
    /// <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 在添加对象时更新?

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

【问题讨论】:

  • 生成 10,000、5,000 甚至 1,000 个列表框项容器将是资源密集型的。这就是ListBox 使用VirtualizingStackPanel 作为其默认面板的原因——它只为实际在屏幕上的对象生成容器。我假设您不会要求您的所有点同时可见 - 如果是这种情况,我会考虑为您的点创建一个自定义虚拟化面板。
  • 一次替换 CityList 可能会更好,而不是让单独的 CollectionChangedEvents 关闭并为添加的每个城市引发重绘。
  • 在大多数情况下,所有点都将同时可见
  • 我想知道是否有办法让进度条指示进程但是每 10% 或从后台线程更新 ui 一次
  • “我想知道是否有办法设置进度条”——当然会有。您可以异步填充数据,以便 UI 有机会更新。但是,既然您说这些点几乎都是一直可见的,那么添加元素的进度是否足以作为进度指示?那么您本身就不需要进度条,只需将填充代码移出 UI 线程(或者,更可能的是,定期在 UI 线程中 Task.Yield() ......因为大部分工作都是 UI 工作,所以您'无论如何都会一直在 UI 线程中结束)。

标签: c# wpf listbox


【解决方案1】:

是否可以仅在生成所有点后才触发更新,假设由于 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 的同时在后台任务中运行初始化。这会在上述两个选项之间产生一些东西。也就是说,它仍然比没有更新的初始化慢,但它比 initialize-and-update-in-UI-thread 选项快一点,因为只涉及一点并发(我实现它是为了它会启动一个任务计算下一批对象,然后在该任务运行时,添加上一批对象并等待该更新完成)。但是,我可能不会在实际程序中使用这种方法,因为虽然它比只在 UI 线程中做所有事情要好一点,但并没有那么好得多,而且它显着增加了复杂性代码。

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

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

【讨论】:

  • (很抱歉复制了主要问题的评论,但我认为您可能也有兴趣查看那里) - 看到一些数据点的名称如丹佛,所以ZoomableCanvas 可能会有所帮助。 OTOH,如果自己画,请参阅ten million pixel challenge in WPF。我最近posted 了一小部分提示和一个集合以及其他一些非常相关的文章。
  • @Peter 批处理方法是个好主意,它看起来简单而有效。我想知道您是否愿意使用 Items 控件共享您的方法。我自己处理选择的东西没有问题。我宁愿像你提到的那样提高速度。理想情况下,我实际上不会显示所有坐标,而是会在每个点显示一个圆形指示器。类似于我添加到 OP 中的那个。
  • 我将我的更改为使用 ItemsControl,但它看起来很慢。所以我很想知道你是如何让它变得更快的。我已经更新了我的代码,以向您展示我到目前为止所拥有的!我没有看到 itemscontrol 的速度有很大提高。
  • 我没有做任何特别的事情来切换到ItemsControl。我刚刚更改了 XAML,将“ListBox”替换为“ItemsControl”,并删除了对 ItemsControlSelectionModeBorderThickness)无效的属性和设置器。您当前的代码(供将来参考,请不要更改问题)与我的主要区别在于您已从 ItemsPanelTemplate 画布中删除了 IsItemsHost 和宽度/高度,并且是现在对项目进行模板化,而不仅仅是显示它们的名称。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-28
  • 2011-10-16
  • 1970-01-01
  • 2021-06-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多