这个问题与 a question I recently posted 直接相关,但我觉得方向已经改变到足以保证一个新的方向。我试图找出在 Canvas 上实时移动大量图像的最佳方法。我的 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 没有任何不同的 react 。然而,我觉得奇怪的是,如果我在集合中添加 400 个条目,然后在 Image 设置为 null 的情况下再添加 400 个,即使一半的项目没有被绘制,运动也会变得不稳定。

那么,如果有的话,我该怎么做才能在 Canvas 上绘制和平滑移动更多图像?这是一种我可能想避开 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 在速度是原来的两倍时表现如此糟糕?是否有不同的方法来迭代集合和/或不同的计时方法,可以允许平滑移动而不会出现任何重大减速?

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

最佳答案

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

这是一个(非常)基本的实现:

更新: 我修改了渲染器类以使用 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 时,您实际上是将属性更新发布到另一个线程。由于您已将图像绑定(bind)到该属性,并且 WPF 元素只能从创建它们的线程进行更新,因此 WPF 会自动将每个更新发布到要在该线程上执行的 Dispatcher 队列。这就是为什么循环结束得更快,并且您没有为更新 UI 的实际工作计时。它只会增加更多的工作。

关于c# - Canvas 上大量物品的断断续续运动,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/18884283/

10-13 23:49