这个问题与a question I recently posted直接相关,但我觉得方向已经发生了变化,足以保证一个新方向。我试图找出在画布上实时移动大量图像的最佳方法。我的XAML目前看起来像这样:
<UserControl.Resources>
<DataTemplate DataType="{x:Type local:Entity}">
<Canvas>
<Image Canvas.Left="{Binding Location.X}"
Canvas.Top="{Binding Location.Y}"
Width="{Binding Width}"
Height="{Binding Height}"
Source="{Binding Image}" />
</Canvas>
</DataTemplate>
</UserControl.Resources>
<Canvas x:Name="content"
Width="2000"
Height="2000"
Background="LightGreen">
<ItemsControl Canvas.ZIndex="2" ItemsSource="{Binding Entities}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
实体类:
[Magic]
public class Entity : ObservableObject
{
public Entity()
{
Height = 16;
Width = 16;
Location = new Vector(Global.rand.Next(800), Global.rand.Next(800));
Image = Global.LoadBitmap("Resources/Thing1.png");
}
public int Height { get; set; }
public int Width { get; set; }
public Vector Location { get; set; }
public WriteableBitmap Image { get; set; }
}
移动对象:
private Action<Entity> action = (Entity entity) =>
{
entity.Location = new Vector(entity.Location.X + 1, entity.Location.Y);
};
void Timer_Tick(object sender, EventArgs e)
{
Task.Factory.StartNew(() =>
{
foreach (var entity in Locator.Container.Entities)
{
action(entity);
}
});
}
如果我在Entities
集合中的参赛作品少于400个,那么动作是顺畅的,但我希望能够将这个数字增加一点点。如果我超过400,运动变得越来越不稳定。起初我认为这是运动逻辑的问题(在这一点上并不是真的很多),但我发现这不是问题。我添加了另一个包含10,000个条目的集合,并将该集合添加到与第一个相同的计时器循环中,但未将其包含在XAML中,并且UI没有任何不同的反应。然而,我觉得奇怪的是,如果我将400个条目添加到集合中,然后再将400个条目设置为null
,则即使未绘制一半项目,移动也会变得不稳定。
那么,我能做什么,如果有的话,能够在画布上绘制和平滑移动更多图像?这种情况我可能想回避WPF&amp; XAML?如果您需要更多代码,我很乐意发布它。
更新:根据Clemens的建议,我的Entity
DataTemplate现在看起来像这样:
<DataTemplate DataType="{x:Type local:Entity}">
<Image Width="{Binding Width}"
Height="{Binding Height}"
Source="{Binding Image}">
<Image.RenderTransform>
<TranslateTransform X="{Binding Location.X}" Y="{Binding Location.Y}" />
</Image.RenderTransform>
</Image>
</DataTemplate>
使用此功能可能会提升性能,但如果存在则非常微妙。另外,我注意到如果我使用DispatcherTimer
作为循环并将其设置为:
private DispatcherTimer dTimer = new DispatcherTimer();
public Loop()
{
dTimer.Interval = TimeSpan.FromMilliseconds(30);
dTimer.Tick += Timer_Tick;
dTimer.Start();
}
void Timer_Tick(object sender, EventArgs e)
{
foreach (var entity in Locator.Container.Entities)
{
action(entity);
}
}
......即使有几千件物品,运动也很平稳,但无论间隔时间都很慢。如果使用DispatcherTimer
且Timer_Tick
如下所示:
void Timer_Tick(object sender, EventArgs e)
{
Task.Factory.StartNew(() =>
{
foreach (var entity in Locator.Container.Entities)
{
action(entity);
}
});
}
......运动非常不稳定。我发现奇怪的是,如果有5,000个条目,Stopwatch
表示Task.Factory
需要1000到1400个刻度来迭代集合。标准foreach
循环需要超过3,000个滴答。为什么Task.Factory
在速度提高两倍时会表现得如此糟糕?是否有不同的方法来迭代集合和/或不同的计时方法,可以在没有任何重大减速的情况下平稳移动?
更新:如果有人可以帮助我提高画布上对象实时移动的效果,或者可以在WPF中建议另一种方式来实现类似的结果,则可以获得100个赏金。
答案 0 :(得分:3)
如此多的控件在屏幕上移动,这往往不会产生平滑的结果。你需要一种完全不同的方法 - 自己渲染。我不确定这适合你,因为现在你将无法使用每个项目的控制功能(例如,接收事件,提供工具提示或使用数据模板。)但是如此大量的项目,其他方法是不切实际的。
这是一个(非常)基本的实现,可能是这样的:
更新:我已修改渲染器类以使用CompositionTarget.Rendering
事件而不是DispatcherTimer
。每次WPF呈现帧时(每次大约60 fps),此事件都会触发。虽然这会提供更平滑的结果,但它也会占用更多CPU,因此请确保在不再需要时关闭动画。
public class ItemsRenderer : FrameworkElement
{
private bool _isLoaded;
public ItemsRenderer()
{
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
_isLoaded = true;
if (IsAnimating)
{
Start();
}
}
private void OnUnloaded(object sender, RoutedEventArgs routedEventArgs)
{
_isLoaded = false;
Stop();
}
public bool IsAnimating
{
get { return (bool)GetValue(IsAnimatingProperty); }
set { SetValue(IsAnimatingProperty, value); }
}
public static readonly DependencyProperty IsAnimatingProperty =
DependencyProperty.Register("IsAnimating", typeof(bool), typeof(ItemsRenderer), new FrameworkPropertyMetadata(false, (d, e) => ((ItemsRenderer)d).OnIsAnimatingChanged((bool)e.NewValue)));
private void OnIsAnimatingChanged(bool isAnimating)
{
if (_isLoaded)
{
Stop();
if (isAnimating)
{
Start();
}
}
}
private void Start()
{
CompositionTarget.Rendering += CompositionTargetOnRendering;
}
private void Stop()
{
CompositionTarget.Rendering -= CompositionTargetOnRendering;
}
private void CompositionTargetOnRendering(object sender, EventArgs eventArgs)
{
InvalidateVisual();
}
public static readonly DependencyProperty ImageSourceProperty =
DependencyProperty.Register("ImageSource", typeof (ImageSource), typeof (ItemsRenderer), new FrameworkPropertyMetadata());
public ImageSource ImageSource
{
get { return (ImageSource) GetValue(ImageSourceProperty); }
set { SetValue(ImageSourceProperty, value); }
}
public static readonly DependencyProperty ImageSizeProperty =
DependencyProperty.Register("ImageSize", typeof(Size), typeof(ItemsRenderer), new FrameworkPropertyMetadata(Size.Empty));
public Size ImageSize
{
get { return (Size) GetValue(ImageSizeProperty); }
set { SetValue(ImageSizeProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof (IEnumerable), typeof (ItemsRenderer), new FrameworkPropertyMetadata());
public IEnumerable ItemsSource
{
get { return (IEnumerable) GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
protected override void OnRender(DrawingContext dc)
{
ImageSource imageSource = ImageSource;
IEnumerable itemsSource = ItemsSource;
if (itemsSource == null || imageSource == null) return;
Size size = ImageSize.IsEmpty ? new Size(imageSource.Width, imageSource.Height) : ImageSize;
foreach (var item in itemsSource)
{
dc.DrawImage(imageSource, new Rect(GetPoint(item), size));
}
}
private Point GetPoint(object item)
{
var args = new ItemPointEventArgs(item);
OnPointRequested(args);
return args.Point;
}
public event EventHandler<ItemPointEventArgs> PointRequested;
protected virtual void OnPointRequested(ItemPointEventArgs e)
{
EventHandler<ItemPointEventArgs> handler = PointRequested;
if (handler != null) handler(this, e);
}
}
public class ItemPointEventArgs : EventArgs
{
public ItemPointEventArgs(object item)
{
Item = item;
}
public object Item { get; private set; }
public Point Point { get; set; }
}
用法:
<my:ItemsRenderer x:Name="Renderer"
ImageSize="8 8"
ImageSource="32.png"
PointRequested="OnPointRequested" />
代码背后:
Renderer.ItemsSource = Enumerable.Range(0, 2000)
.Select(t => new Item { Location = new Point(_rng.Next(800), _rng.Next(800)) }).ToArray();
private void OnPointRequested(object sender, ItemPointEventArgs e)
{
var item = (Item) e.Item;
item.Location = e.Point = new Point(item.Location.X + 1, item.Location.Y);
}
您可以使用OnPointRequested
方法从项目中获取任何数据(例如图像本身。)另外,不要忘记冻结图像,并预先调整它们的大小。
关于先前解决方案中的线程的附注。当您使用Task
时,您实际上是将属性更新发布到另一个线程。由于您已将图像绑定到该属性,并且WPF元素只能从创建它们的线程更新,因此WPF会自动将每个更新发布到要在该线程上执行的Dispatcher队列。这就是为什么循环结束得更快,而你没有计算更新UI的实际工作。它只会增加更多的工作。
答案 1 :(得分:2)
在第一种优化方法中,您可以通过从DataTemplate中移除Canvas并在Canvas.Left
中设置Canvas.Top
和ItemContainerStyle
来将Canvases的数量减少到只有一个:
<DataTemplate DataType="{x:Type local:Entity}">
<Image Width="{Binding Width}" Height="{Binding Height}" Source="{Binding Image}"/>
</DataTemplate>
<ItemsControl ItemsSource="{Binding Entities}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Location.X}"/>
<Setter Property="Canvas.Top" Value="{Binding Location.Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
然后,您可以通过应用TranslateTransform替换设置Canvas.Left
和Canvas.Top
:
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="RenderTransform">
<Setter.Value>
<TranslateTransform X="{Binding Location.X}" Y="{Binding Location.Y}"/>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.ItemContainerStyle>
现在,这可以类似地应用于DataTemplate中的Image控件而不是item容器。因此,您可以删除ItemContainerStyle
并像这样编写DataTemplate:
<DataTemplate DataType="{x:Type local:Entity}">
<Image Width="{Binding Width}" Height="{Binding Height}" Source="{Binding Image}">
<Image.RenderTransform>
<TranslateTransform X="{Binding Location.X}" Y="{Binding Location.Y}"/>
</Image.RenderTransform>
</Image>
</DataTemplate>
答案 2 :(得分:1)
尝试使用TranslateTransform
代替Canvas.Left
和Canvas.Top
。 RenderTransform
和TranslateTransform
可以有效地缩放/移动现有的绘图对象。
答案 3 :(得分:1)
在开发一个名为Mongoose的非常简单的库时,我必须解决这个问题。 我尝试了1000张图像并且它非常流畅(我没有自动移动图像的代码,我通过拖放在Surface上手动移动它们,但是你的代码应该有相同的结果)。
我写了一个你可以使用库运行的快速示例(你只需要一个带有PadContents集合的附加视图模型):
<强> MainWindow.xaml 强>
<Window x:Class="Mongoose.Sample.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:col="clr-namespace:System.Collections;assembly=mscorlib"
xmlns:mwc="clr-namespace:Mongoose.Windows.Controls;assembly=Mongoose.Windows"
Icon="Resources/MongooseLogo.png"
Title="Mongoose Sample Application" Height="1000" Width="1200">
<mwc:Surface x:Name="surface" ItemsSource="{Binding PadContents}">
<mwc:Surface.ItemContainerStyle>
<Style TargetType="mwc:Pad">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Image Source="Resources/MongooseLogo.png" Width="30" Height="30" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</mwc:Surface.ItemContainerStyle>
</mwc:Surface>
</Window>
<强> MainWindow.xaml.cs 强>
using System.Collections.ObjectModel;
using System.Windows;
namespace Mongoose.Sample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
public ObservableCollection<object> PadContents
{
get
{
if (padContents == null)
{
padContents = new ObservableCollection<object>();
for (int i = 0; i < 500; i++)
{
padContents.Add("Pad #" + i);
}
}
return padContents;
}
}
private ObservableCollection<object> padContents;
}
}
以下是1000张图片的样子:
Codeplex上提供了完整的代码,因此即使您不想重用该库,您仍然可以检查代码以了解它是如何实现的。
我依靠一些技巧,但主要使用RenderTransform
和CacheMode
。
在我的电脑上,最多可拍摄3000张照片。如果你想做更多,你可能不得不考虑其他方法来实现它(可能有某种虚拟化)
祝你好运!编辑:
在Surface.OnLoaded方法中添加此代码:
var messageTimer = new DispatcherTimer();
messageTimer.Tick += new EventHandler(surface.messageTimer_Tick);
messageTimer.Interval = new TimeSpan(0, 0, 0, 0, 10);
messageTimer.Start();
这个方法在Surface类中:
void messageTimer_Tick(object sender, EventArgs e)
{
var pads = Canvas.Children.OfType<Pad>();
if (pads != null && Layout != null)
{
foreach (var pad in pads)
{
pad.Position = new Point(pad.Position.X + random.Next(-1, 1), pad.Position.Y + random.Next(-1, 1));
}
}
}
您可以看到单独移动每个对象是完全可以的。 这是一个2000个对象的小例子
答案 4 :(得分:1)
这里的问题是渲染/创建了这么多控件。
第一个问题是你是否需要在画布上显示所有图像。如果是这样,我很抱歉,但我无法帮助(如果您需要绘制所有项目,那么就无法解决它了。)
但是,如果不是所有项目一次都在屏幕上可见 - 那么你对Virtualization
的形状抱有希望。您需要编写自己的VirtualizingCanvas
继承VirtualizingPanel
,并仅创建可见的项目。这也将允许您回收容器,这反过来将消除大量的负载。
有一个虚拟化画布here的示例。
然后,您需要将新画布设置为项目面板,并设置项目以获得画布正常工作所需的信息。
答案 5 :(得分:0)
想到一些想法:
冻结您的位图。
当您将位图的大小设置为与显示位图的大小相同时,请将其大小设置为硬,并将BitmapScalingMode
设置为LowQuality
。
在更新实体时跟踪您的进度,如果您不能并在下一帧抓住它们,请尽早删除。这也需要跟踪他们的最后一帧。
// private int _lastEntity = -1;
// private long _tick = 0;
// private Stopwatch _sw = Stopwatch.StartNew();
// private const long TimeSlice = 30;
// optional: this._sw.Restart();
var end = this._sw.ElapsedMilliseconds + TimeSlice - 1;
this._tick++;
var ee = this._lastEntity++;
do {
if (ee >= this._entities.Count) ee = 0;
// entities would then track the last time
// they were "run" and recalculate their movement
// from 'tick'
action(this._entities[ee], this._tick);
if (this._sw.ElapsedMilliseconds > end) break;
} while (ee++ != this._lastEntity);
this._lastEntity = ee;