【发布时间】: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 线程中结束)。