【问题标题】:Choppy movement of large collection of items on a canvas画布上大量物品的不稳定运动
【发布时间】:2013-09-19 00:01:00
【问题描述】:

这个问题与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 个条目,然后在 Image 设置为 null 的情况下再添加 400 个条目,即使有一半的项目没有被绘制,移动也会变得不稳定。

那么,如果有的话,我该怎么做才能在画布上绘制和平滑移动更多图像?这是我可能想回避 WPF 和 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);
        }
    });
}

... 动作非常不稳定。我觉得奇怪的是Stopwatch 表明如果有 5,000 个条目,Task.Factory 需要 1000 到 1400 个滴答来迭代集合。标准的foreach 循环需要超过 3,000 个滴答声。为什么Task.Factory 的速度是原来的两倍?是否有不同的方法来遍历集合和/或不同的计时方法,可以允许平稳移动而不会出现任何重大减速?


更新:如果有人可以帮助我提高画布上对象实时移动的性能,或者可以在 WPF 中提出另一种实现类似结果的方法,100 赏金等待着。

【问题讨论】:

  • 如果你的图片绑定为空,你会得到绑定错误。在调试器中运行时,这些错误会大大降低整体性能。确保在DataTemplate 中添加DataTrigger 以避免这种情况。
  • 即使没有将图像绑定为 null,在集合中的大约 400 个项目之后,我仍然开始受到明显的性能影响。从我的代码来看,我可以做些什么来增加屏幕上的动态图像数量,同时保持一切顺利运行?
  • 也许这是疯话,但你为什么要移动图像而不是容器?图像是否单独移动?
  • 最终,我打算让集合中的每个项目能够执行不同的任务。我现在只有非常简单的逻辑,不要把任何事情复杂化。
  • 您是否有正在运行的示例重现我们可以解决的问题?

标签: c# wpf xaml canvas


【解决方案1】:

在屏幕上移动如此多的控件,这将永远不会产生流畅的结果。您需要一种完全不同的方法 - 自己渲染。我不确定这是否适合您,因为现在您将无法使用每个项目的控制功能(例如接收事件、具有工具提示或使用数据模板。)但是对于如此大量的项目,其他方法是不切实际。

这是一个(非常)基本的实现,可能看起来像这样:

更新:我已修改渲染器类以使用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 会自动将每个更新发布到调度程序队列以在该线程上执行。这就是循环结束得更快的原因,而且您没有为更新 UI 的实际工作计时。它只是增加了更多的工作。

【讨论】:

  • 到目前为止,这种方法运行良好。我遇到的唯一问题是 DispatcherTimer 导致运动不一致。我尝试用 Timers.Timer 替换它,但没有图像移动。你知道我可以如何解决这个问题吗?即便如此,这也是一个巨大的进步,如果我能让一切都保持一致,我很乐意奖励你。
  • 试用更新版本。如果这仍然不够流畅,我认为您应该考虑使用 DirectX 渲染此场景并使用 D3DImage 进行互操作。
  • 我只是想到了另一个选项(使用以前的版本)——增加DispatcherTimer的优先级。它可能运行良好并且 CPU 密集度较低。也试试吧。
  • 感谢您的帮助。随着更大的集合(2000+),运动仍然大大减慢,但在我收到的所有答案中,这个肯定是最好的。我会按照你的建议研究 DirectX。
【解决方案2】:

在第一种优化方法中,您可以通过从 DataTemplate 中删除 Canvas 并在 ItemContainerStyle 中设置 Canvas.LeftCanvas.Top 来将 Canvas 的数量减少到一个:

<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.LeftCanvas.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 控件而不是项容器。因此,您可以删除 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>

【讨论】:

  • 我尝试按照您的建议使用RenderTransform。如果性能有所提升,则几乎不会引起注意。
  • 当然,在 30 毫秒间隔内可以平滑渲染多少 WPF 控件是有限制的。我猜你不会绕过某种低级绘图实现,例如按照 Eli 的建议重载 OnRender。
【解决方案3】:

尝试使用TranslateTransform 代替Canvas.LeftCanvas.TopRenderTransformTranslateTransform 在缩放/移动现有绘图对象方面非常有效。

【讨论】:

    【解决方案4】:

    这是我在开发一个名为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 上提供了完整代码,因此即使您不想重复使用该库,您仍然可以检查代码以了解它是如何实现的。

    我依靠一些技巧,但主要是使用RenderTransformCacheMode

    在我的电脑上最多可以容纳 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 个对象的小示例

    【讨论】:

    • 我相信如果您想将所有项目一起移动,这种方法效果很好。但单独来看,它会产生与原始代码几乎相同的糟糕性能。
    • 实际上,即使是一个一个地移动元素,它也能很好地工作。我将添加一个包含 2000 个元素的示例。
    • 我似乎无法让 Mongoose 工作。添加 dll 后,我可以在 .cs 文件中使用它,但在 XAML 中,intellisense 无法识别它,如果我添加您发布的相同代码,我会收到一大堆错误。
    • 我手动添加了这些类,但我仍然在这些行上收到 NullReferenceExceptions:var pads = Canvas.Children.OfType&lt;Pad&gt;();
    • 如果你还想尝试,你可以通过WPF聊天室,我会解释并制作一个完整的项目zip供你测试!祝你的项目好运!
    【解决方案5】:

    这里的问题是渲染/创建这么多控件。

    第一个问题是您是否需要在画布上显示所有图像。如果是这样,我很抱歉,但我无能为力(如果您需要绘制所有项目,那么就没有办法了)。

    但是,如果不是所有项目都同时在屏幕上可见 - 那么你就有希望了 Virtualization 的形状。您需要编写自己的 VirtualizingCanvas 继承 VirtualizingPanel 并仅创建可见的项目。这也将允许您回收容器,从而减少大量负载。

    有一个虚拟化画布here的示例。

    然后您需要将新画布设置为您的项目面板,并设置项目以获取画布正常工作所需的信息。

    【讨论】:

      【解决方案6】:

      想到的一些想法:

      1. 冻结您的位图。

      2. 在读取位图时硬设置位图的大小与显示它们的大小相同,并将BitmapScalingMode 设置为LowQuality

      3. 在更新实体的同时跟踪您的进度,如果不能,请尽早退出并在下一帧抓取它们。这也需要跟踪他们的最后一帧。

        // 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;
        

      【讨论】:

        猜你喜欢
        • 2013-07-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-04-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多