我在WPF中创建了一个Listbox,我在用户单击Generate时随机绘制2D点。在我的情况下,当用户点击Generate时,我将绘制几千个点。我注意到当我产生大约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在添加对象时更新?
最终目标是仅绘制表示2D空间中每个坐标的点。
答案 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
并自己处理项目选择更有意义。