WPF性能缓慢的原因

15

我正在使用 DrawText 创建大量文本,并将它们添加到单个 Canvas 中。

我需要在每个 MouseWheel 事件中重新绘制屏幕,但我发现性能有点慢,因此我测量了创建对象的时间,只有不到1毫秒!

那么问题出在哪里呢?很久以前,我猜我在某个地方读到过,实际上是 Rendering 花费时间,而不是创建和添加可视内容。

这是我用来创建文本对象的代码,我只包含了必要的部分:

public class ColumnIdsInPlan : UIElement
    {
    private readonly VisualCollection _visuals;
    public ColumnIdsInPlan(BaseWorkspace space)
    {
        _visuals = new VisualCollection(this);

        foreach (var column in Building.ModelColumnsInTheElevation)
        {
            var drawingVisual = new DrawingVisual();
            using (var dc = drawingVisual.RenderOpen())
            {
                var text = "C" + Convert.ToString(column.GroupId);
                var ft = new FormattedText(text, cultureinfo, flowdirection,
                                           typeface, columntextsize, columntextcolor,
                                           null, TextFormattingMode.Display)
                {
                    TextAlignment = TextAlignment.Left
                };

                // Apply Transforms
                var st = new ScaleTransform(1 / scale, 1 / scale, x, space.FlipYAxis(y));
                dc.PushTransform(st);

                // Draw Text
                dc.DrawText(ft, space.FlipYAxis(x, y));
            }
            _visuals.Add(drawingVisual);
        }
    }

    protected override Visual GetVisualChild(int index)
    {
        return _visuals[index];
    }

    protected override int VisualChildrenCount
    {
        get
        {
            return _visuals.Count;
        }
    }
}

每次触发 MouseWheel 事件时,此代码将运行:

var columnsGroupIds = new ColumnIdsInPlan(this);
MyCanvas.Children.Clear();
FixedLayer.Children.Add(columnsGroupIds);

罪魁祸首可能是什么?

同时我在拍摄时也遇到了麻烦:

    private void Workspace_MouseMove(object sender, MouseEventArgs e)
    {
        MousePos.Current = e.GetPosition(Window);
        if (!Window.IsMouseCaptured) return;
        var tt = GetTranslateTransform(Window);
        var v = Start - e.GetPosition(this);
        tt.X = Origin.X - v.X;
        tt.Y = Origin.Y - v.Y;
    }

1
你不应该使用绘制文本。你应该将模板应用到“Label”或“TextBlock”中,并将其放入“ItemControl”中,然后只需向其提供一个字符串数组,它就会自动绘制出来。 - Franck
@Franck 谢谢Frank,每个文本的位置和旋转怎么样? - Vahid
@Franck 谢谢Franck,我会尝试一下,你有类似这种情况的例子吗? - Vahid
这是一个关于项控件的简单教程示例,涉及项内部的简单模板设计。总体来说,ItemControl保存项的集合,然后在内部重复应用格式,以生成该集合中每个项的可视化效果。 - Franck
谢谢Franck,我希望这能解决问题,但我发现即使移动/平移时仍然有点慢的性能,也许这是因为存在大量元素。我在想是否可以在重新绘制后将它们转换为图像以获得更好的性能? - Vahid
显示剩余9条评论
3个回答

23

我目前正在处理一个可能是相同问题的情况,并发现了一些非常意外的事情。 我正在渲染到WriteableBitmap,并允许用户滚动(缩放)和平移以更改所呈现的内容。 无论是缩放还是平移,移动似乎都很断断续续,因此我自然而然地认为渲染时间太长了。 经过一些检测,我验证了我以30-60 fps的速度进行渲染。 无论用户如何缩放或平移,渲染时间都没有增加,因此卡顿肯定来自其他地方。

我转而查看OnMouseMove事件处理程序。 虽然WriteableBitmap每秒更新30-60次,但MouseMove事件每秒只触发1-2次。 如果减小WriteableBitmap的大小,则MouseMove事件会更频繁地触发,平移操作看起来更流畅。 因此,卡顿实际上是由于MouseMove事件不连贯造成的,而不是渲染(例如,WriteableBitmap正在渲染7-10帧看起来相同的图像,MouseMove事件触发,然后WriteableBitmap呈现7-10帧新平移的图像等)。

我尝试通过在每次WriteableBitmap更新时使用Mouse.GetPosition(this)来轮询鼠标位置来跟踪平移操作。 但是结果相同,因为返回的鼠标位置在更改为新值之前会相同7-10帧。

然后我尝试使用PInvoke服务GetCursorPos轮询鼠标位置就像这个SO答案中所示

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool GetCursorPos(out POINT lpPoint);

[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
    public int X;
    public int Y;

    public POINT(int x, int y)
    {
        this.X = x;
        this.Y = y;
    }
}

这实际上是解决问题的方法。GetCursorPos 每次调用时(当鼠标移动时)都会返回一个新位置,因此在用户平移时每帧渲染在略微不同的位置。同样的卡顿现象似乎也影响了 MouseWheel 事件,我不知道该如何解决它。

因此,虽然以上关于高效维护可视树的建议都是良好的实践,但我认为您的性能问题可能是由于某些干扰鼠标事件频率的原因导致的。在我的情况下,似乎由于某种原因渲染导致 Mouse 事件更新和触发比平常慢得多。如果我找到真正的解决方案而不是这个部分性的解决方案,我会更新这篇文章。


编辑: 好的,我对此进行了更深入的研究,现在我认为我已经理解了正在发生的事情。我将使用更详细的代码示例进行解释:

我通过注册处理 CompositionTarget.Rendering 事件来按帧渲染到位图中,如 此 MSDN 文章所述。 基本上,这意味着每次 UI 渲染时都会调用我的代码,以便我可以更新位图。这与您正在进行的渲染基本相同,只是您的渲染代码根据您设置可视元素的方式在幕后被调用,而我的渲染代码是我可以看到它的地方。我重写了 OnMouseMove 事件以根据鼠标位置更新一些变量。

public class MainWindow : Window
{
  private System.Windows.Point _mousePos;
  public Window()
  {
    InitializeComponent();
    CompositionTarget.Rendering += CompositionTarget_Rendering;
  }

  private void CompositionTarget_Rendering(object sender, EventArgs e)
  {
    // Update my WriteableBitmap here using the _mousePos variable
  }

  protected override void OnMouseMove(MouseEventArgs e)
  {
    _mousePos = e.GetPosition(this);
    base.OnMouseMove(e);
  }
}
渲染时间过长会导致鼠标事件被调用的频率降低。如果渲染代码需要15ms,MouseMove事件将每几毫秒调用一次。如果渲染代码需要30ms,则MouseMove事件将每几百毫秒调用一次。渲染发生在WPF鼠标系统更新其值并触发鼠标事件的同一线程上。WPF循环在此线程上必须有一些条件逻辑,如果渲染在一帧期间花费时间过长,则跳过执行鼠标更新。当我的渲染代码在每一帧都需要“太长”时,问题就出现了。这样,界面不仅因渲染每一帧多花费15ms而变慢,更由于额外的15ms渲染时间在鼠标更新之间引入了数百毫秒的延迟,从而产生明显的卡顿。我之前提到的PInvoke解决方法基本上是绕过了WPF鼠标输入系统。每次渲染发生时,它直接访问源代码,因此饥饿的WPF鼠标输入系统不再阻止我的位图正确更新。
public class MainWindow : Window
{
  private System.Windows.Point _mousePos;
  public Window()
  {
    InitializeComponent();
    CompositionTarget.Rendering += CompositionTarget_Rendering;
  }

  private void CompositionTarget_Rendering(object sender, EventArgs e)
  {
    POINT screenSpacePoint;
    GetCursorPos(out screenSpacePoint);

    // note that screenSpacePoint is in screen-space pixel coordinates, 
    // not the same WPF Units you get from the MouseMove event. 
    // You may want to convert to WPF units when using GetCursorPos.
    _mousePos = new System.Windows.Point(screenSpacePoint.X, 
                                         screenSpacePoint.Y);
    // Update my WriteableBitmap here using the _mousePos variable
  }

  [DllImport("user32.dll")]
  [return: MarshalAs(UnmanagedType.Bool)]
  static extern bool GetCursorPos(out POINT lpPoint);

  [StructLayout(LayoutKind.Sequential)]
  public struct POINT
  {
    public int X;
    public int Y;

    public POINT(int x, int y)
    {
      this.X = x;
      this.Y = y;
    }
  }
}

然而,这种方法并没有解决我的其他鼠标事件(MouseDown,MouseWheel等)问题,而且我也不想为所有鼠标输入采取这种PInvoke方法,所以我决定最好停止限制WPF鼠标输入系统。 我最终做的是仅在确实需要更新时才更新WriteableBitmap。 只有当某些鼠标输入影响它时才需要更新。 因此,结果是我在一帧中接收到鼠标输入,下一帧更新位图,但在同一帧中不会再接收更多鼠标输入,因为更新需要几毫秒,然后下一帧我将接收更多鼠标输入,因为位图不需要再次更新。 这样产生的性能退化更加线性(和合理),因为变量长度的帧时间只是平均化了。

public class MainWindow : Window
{
  private System.Windows.Point _mousePos;
  private bool _bitmapNeedsUpdate;
  public Window()
  {
    InitializeComponent();
    CompositionTarget.Rendering += CompositionTarget_Rendering;
  }

  private void CompositionTarget_Rendering(object sender, EventArgs e)
  {
    if (!_bitmapNeedsUpdate) return;
    _bitmapNeedsUpdate = false;
    // Update my WriteableBitmap here using the _mousePos variable
  }

  protected override void OnMouseMove(MouseEventArgs e)
  {
    _mousePos = e.GetPosition(this);
    _bitmapNeedsUpdate = true;
    base.OnMouseMove(e);
  }
}

将这些知识应用到你自己的情况中:对于导致性能问题的复杂几何体,我建议尝试使用某种类型的缓存。例如,如果这些几何体本身从不改变或者不经常改变,可以尝试将它们呈现为 RenderTargetBitmap 然后将 RenderTargetBitmap 添加到你的可视化树中,而不是添加几何体本身。这样,当 WPF 执行其呈现路径时,它只需要传输这些位图而不是从原始几何数据重构像素数据。


1
谢谢Dan,你提供的信息对我来说非常有价值。我已经通过绘制可视化图形等方式缓解了这个问题,但是当我在屏幕上有大量几何图形时,仍然会出现卡顿。虽然如此,我仍然无法理解你上面提供的代码,请给我提供一个示例吗?如果可以的话,这将真正让我开心不已! - Vahid
我已经更新了我的答案,提供了更多细节和更好的理解。 - Dan Oliphant

7

@Vahid:WPF系统正在使用[保留图形]。你最终应该做的是设计一个系统,只发送“与上一帧相比发生了什么变化”的内容——没有更多、也没有更少,你不应该创建任何新对象。这不是关于“创建对象需要零秒”,而是关于它如何影响渲染和时间。这是让WPF使用缓存来完成工作的问题。

将新对象发送到GPU进行渲染=。只发送更新到告诉GPU哪些对象移动=

此外,可以在任意线程中创建Visuals以提高性能(多线程UI:HostVisual-Dwayne Need)。话虽如此,如果你的项目在3D方面相当复杂——很可能WPF无法胜任。直接使用DirectX会更加高效!

以下是我建议您阅读并理解的一些文章:

[编写更高效的ItemsControls - Charles Petzold] - 了解如何在WPF中实现更好的绘图速度的过程。

至于为什么您的UI会出现滞后,Dan的答案似乎很准确。如果您尝试渲染超过WPF处理能力的内容,则输入系统将受到影响。


谢谢Chris的回答。我知道WPF使用保留图形,但为什么会影响平移呢?我在平移时没有创建任何新的图形,所以我认为没有任何东西被渲染。或者我错了吗? - Vahid
我尝试了Dan提出的解决方案,你可以在这里看到我的其他问题:http://stackoverflow.com/questions/27584324/slow-pan-and-zoom-in-wpf 但它仍然没有解决问题 :( - Vahid
@Vahid:你有进展吗?现在很难看出你到底做了什么。 - Erti-Chris Eelmaa
你看了我上面评论中的链接吗?我已经按照Dan的建议实现了CompositionTargetOnRendering,但仍然没有改善。我还没有看过Multithreaded UI: HostVisual方法。虽然我还没有时间去看它是否真正有效。 - Vahid

4
很可能的原因是您在每次滚动事件中清除并重建了可视树。根据您自己的帖子,该树包括“大量”的文本元素。对于每个进来的事件,必须重新创建、重新格式化、测量并最终呈现每个文本元素。这不是实现简单文本缩放的方法。
而不是在每个FormattedText元素上设置一个ScaleTransform,请在包含文本的元素上设置一个。根据您的需求,可以设置RenderTransform或LayoutTransform。然后,当您收到滚动事件时,相应地调整Scale属性。不要在每个事件上重新构建文本。
我还建议按照其他人的建议将ItemsControl绑定到列列表,并以此方式生成文本。没有理由手工完成这项工作。

你看到我上一条评论了吗?我在移动/平移时也遇到了这个问题,这并不涉及重建可视树。我必须对每个对象单独应用“ScaleTransform”,因为它们都有自己在屏幕上的特定位置。而且我需要保持它们与其他对象的比例关系。 - Vahid
@MikeStrobel 我更新了问题。ttTranslateTransform - Vahid
@Gusdor 是正确的,文本渲染是 WPF 性能开始崩溃的地方。你是使用 LayoutTransform 还是 RenderTransform 来进行平移?当你浏览完所有文本后,它是否开始变得流畅,就好像第一次显示一个项目时有额外的开销?此外,根据你的代码,看起来你正在使用单个变换而不是每个文本块都有一个变换(这很好),但为了确认:你只将变换应用于一个元素,对吗? - Mike Strobel
1
@Vahid 画少一些文本或少画相同的文本!这些是图形性能的黄金规则。只绘制所需内容,并在需要时绘制所需内容。您正在绘制所需内容,但绘制得太频繁了。在回到 OnRender 之前,请信任 WPF 为您解决问题。 - Gusdor
@MikeStrobel 我正在使用 RenderTransform,但它不够流畅。我在 Canvas 上有很多线条和多边形,它们并没有任何问题,但一旦我添加文本,性能就会下降。 - Vahid
显示剩余13条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接