WPF 性能缓慢的原因

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/24119311/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 11:52:19  来源:igfitidea点击:

The reason behind slow performance in WPF

c#wpfperformancedrawtextdrawingvisual

提问by Vahid

I'm creating a large number of texts in WPF using DrawTextand then adding them to a single Canvas.

我正在使用 WPF 创建大量文本DrawText,然后将它们添加到单个Canvas.

I need to redraw the screen in each MouseWheelevent and I realized that the performance is a bit slow, so I measured the time the objects are created and it was less than 1 milliseconds!

我需要在每个MouseWheel事件中重新绘制屏幕,我意识到性能有点慢,所以我测量了创建对象的时间,它不到 1 毫秒!

So what could be the problem? A long time ago I guess I read somewhere that it actually is the Renderingthat takes the time, not creating and adding the visuals.

那么可能是什么问题呢?很久以前我想我在某处读到它实际上是Rendering需要时间的,而不是创建和添加视觉效果。

Here is the code I'm using to create the text objects, I've only included the essential parts:

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

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

And this code is run each time the MouseWheelevent is fired:

每次MouseWheel触发事件时都会运行此代码:

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

What could be the culprit?

罪魁祸首可能是什么?

I'm also having trouble while panning:

我在平移时也遇到了问题:

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

回答by Dan Oliphant

I'm currently dealing with what is likely the same issue and I've discovered something quite unexpected. I'm rendering to a WriteableBitmap and allowing the user to scroll (zoom) and pan to change what is rendered. The movement seemed choppy for both the zooming and panning, so I naturally figured the rendering was taking too long. After some instrumentation, I verified that I'm rendering at 30-60 fps. There is no increase in render time regardless of how the user is zooming or panning, so the choppiness must be coming from somewhere else.

我目前正在处理可能是相同的问题,我发现了一些非常出乎意料的事情。我正在渲染到 WriteableBitmap 并允许用户滚动(缩放)和平移以更改渲染的内容。缩放和平移的运动似乎断断续续,所以我很自然地认为渲染时间太长了。经过一些检测后,我确认我正在以 30-60 fps 进行渲染。无论用户如何缩放或平移,渲染时间都不会增加,因此波动一定来自其他地方。

I looked instead at the OnMouseMove event handler. While the WriteableBitmap updates 30-60 times per second, the MouseMove event is only fired 1-2 times per second. If I decrease the size of the WriteableBitmap, the MouseMove event fires more often and the pan operation appears smoother. So the choppiness is actually a result of the MouseMove event being choppy, not the rendering (e.g. the WriteableBitmap is rendering 7-10 frames that look the same, a MouseMove event fires, then the WriteableBitmap renders 7-10 frames of the newly panned image, etc).

我转而查看 OnMouseMove 事件处理程序。虽然 WriteableBitmap 每秒更新 30-60 次,但 MouseMove 事件每秒仅触发 1-2 次。如果我减小 WriteableBitmap 的大小,MouseMove 事件会更频繁地触发,并且平移操作看起来更流畅。所以波动实际上是 MouseMove 事件波动的结果,而不是渲染(例如 WriteableBitmap 渲染 7-10 帧看起来相同,MouseMove 事件触发,然后 WriteableBitmap 渲染新平移图像的 7-10 帧, 等等)。

I tried keeping track of the pan operation by polling the mouse position every time the WriteableBitmap updates using Mouse.GetPosition(this). That had the same result, however, because the returned mouse position would be the same for 7-10 frames before changing to a new value.

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

I then tried polling the mouse position using the PInvoke service GetCursorPos like in this SO answereg:

然后我尝试使用 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;
    }
}

and this actually did the trick. GetCursorPos returns a new position each time it is called (when the mouse is moving), so each frame is rendered at a slightly different position while the user is panning. The same sort of choppiness seems to be affecting the MouseWheel event, and I have no idea how to work around that one.

这实际上做到了。GetCursorPos 每次被调用时都会返回一个新位置(当鼠标移动时),因此当用户平移时,每一帧都呈现在稍微不同的位置。同样的波动似乎正在影响 MouseWheel 事件,我不知道如何解决这个问题。

So, while all of the above advice about efficiently maintaining your visual tree is good practice, I suspect that your performance issues may be a result of something interfering with the mouse event frequency. In my case, it appears that for some reason the rendering is causing the Mouse events to update and fire much slower than usual. I'll update this if I find a true solution rather than this partial work-around.

因此,虽然上述所有关于有效维护可视化树的建议都是很好的做法,但我怀疑您的性能问题可能是由于某些因素干扰了鼠标事件频率。就我而言,似乎由于某种原因渲染导致鼠标事件更新和触发比平时慢得多。如果我找到一个真正的解决方案而不是这个部分的解决方法,我会更新这个。



Edit: Ok, I dug into this a little more and I think I now understand what is going on. I'll explain with more detailed code samples:

编辑:好的,我深入研究了一点,我想我现在明白发生了什么。我将用更详细的代码示例进行解释:

I am rendering to my bitmap on a per-frame basis by registering to handle the CompositionTarget.Rendering event as described in this MSDN article.Basically, it means that every time the UI is rendered my code will be called so I can update my bitmap. This is essentially equivalent to the rendering that you are doing, it's just that your rendering code gets called behind the scenes depending on how you've set up your visual elements and my rendering code is where I can see it. I override the OnMouseMove event to update some variable depending on the position of the 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);
  }
}

The problem is that, as the rendering takes more time, the MouseMove event (and all mouse events, really) gets called much less frequently. When the rendering code takes 15ms, the MouseMove event gets called every few ms. When the rendering code takes 30ms, the MouseMove event gets called every few hundredmilliseconds. My theory on why this happens is that the rendering is happening on the same thread where the WPF mouse system updates its values and fires mouse events. The WPF loop on this thread must have some conditional logic where if the rendering takes too long during one frame it skips doing the mouse updates. The problem arises when my rendering code takes "too long" on every single frame. Then, instead of the interface appearing to slow down a little bit because the rendering is taking 15 extra ms per frame, the interface stutters greatly because that extra 15ms of render time introduces hundreds of milliseconds of lag between mouse updates.

问题是,随着渲染花费更多时间,MouseMove 事件(以及所有鼠标事件,实际上)的调用频率要低得多。当渲染代码需要 15 毫秒时,每隔几毫秒就会调用一次 MouseMove 事件。当渲染代码需要 30 毫秒时,MouseMove 事件每隔几百调用一次毫秒。我关于为什么会发生这种情况的理论是,渲染发生在 WPF 鼠标系统更新其值并触发鼠标事件的同一线程上。该线程上的 WPF 循环必须有一些条件逻辑,如果渲染在一帧内花费的时间太长,它会跳过鼠标更新。当我的渲染代码在每一帧上花费“太长时间”时,就会出现问题。然后,界面看起来并没有因为渲染每帧多花 15 毫秒而变慢,而是因为额外的 15 毫秒渲染时间在鼠标更新之间引入了数百毫秒的延迟,所以界面会非常卡顿。

The PInvoke workaround I mentioned before essentially bypasses the WPF mouse input system. Every time the rendering happens it goes straight to the source, so starving the WPF mouse input system no longer prevents my bitmap from updating correctly.

我之前提到的 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;
    }
  }
}

This approach didn't fix the rest of my mouse events (MouseDown, MouseWheel, etc), however, and I wasn't keen on taking this PInvoke approach for all of my mouse input, so I decided I better just stop starving the WPF mouse input system. What I ended up doing was only updating the WriteableBitmap when it really needed to be updated. It only needs to be updated when some mouse input has affected it. So the result is that I receive mouse input one frame, update the bitmap on the next frame but do not receive more mouse input on the same frame because the update takes a few milliseconds too long, and then the next frame I'll receive more mouse input because the bitmap didn't need to be updated again. This produces a much more linear (and reasonable) performance degradation as my rendering time increases because the variable length frame times just sort of average out.

这种方法并没有修复我的其余鼠标事件(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);
  }
}

Translating this same knowledge to your own particular situation: for your complex geometries that lead to performance issues I would try some type of caching. For example, if the geometries themselves never change or if they don't change often, try rendering them to a RenderTargetBitmapand then add the RenderTargetBitmap to your visual tree instead of adding the geometries themselves. That way, when WPF is performing it's rendering path, all it needs to do is blit those bitmaps rather than reconstruct the pixel data from the raw geometric data.

将同样的知识转化为您自己的特定情况:对于导致性能问题的复杂几何图形,我会尝试某种类型的缓存。例如,如果几何图形本身从不改变或不经常改变,请尝试将它们渲染到RenderTargetBitmap,然后将 RenderTargetBitmap 添加到您的可视化树中,而不是添加几何图形本身。这样,当 WPF 执行它的渲染路径时,它需要做的就是 blit 这些位图,而不是从原始几何数据重建像素数据。

回答by Mike Strobel

The likely culprit is the fact that you are clearing out and rebuilding your visual tree on each wheel event. According to your own post, that tree includes a "large number" of text elements. For each event that comes in, each of those text elements must be recreated, reformatted, measured, and eventually rendered. That is not the way to accomplish simple text scaling.

可能的罪魁祸首是您正在清除并在每个轮事件上重建您的视觉树。根据您自己的帖子,该树包含“大量”文本元素。对于传入的每个事件,必须重新创建、重新格式化、测量并最终呈现这些文本元素中的每一个。这不是完成简单文本缩放的方法。

Rather than setting a ScaleTransformon each FormattedTextelement, set one on the element containing the text. Depending on your needs, you can set a RenderTransformor LayoutTransform. Then, when you receive wheel events, adjust the Scaleproperty accordingly. Don't rebuild the text on each event.

不是ScaleTransform在每个FormattedText元素上设置一个,而是在包含文本的元素上设置一个。根据您的需要,您可以设置 aRenderTransformLayoutTransform。然后,当您收到滚轮事件时,相应地调整Scale属性。不要在每个事件上重建文本。

I would also do what other have recommended and bind an ItemsControlto the list of columns and generate the text that way. There is no reason you should need to do this by hand.

我也会做其他人推荐的事情并将 an 绑定ItemsControl到列列表并以这种方式生成文本。您没有理由需要手动执行此操作。

回答by Erti-Chris Eelmaa

@Vahid: the WPF system is using [retained graphics]. What you eventually should do, is devise a system where you only send "what has changed compared to previous frame" - nothing more, nothing less, you should not be creating new objects at all. It's not about "creating objects takes zero seconds", it's about how it affects rendering and the time. It's about letting the WPF do it's job using caching.

@Vahid:WPF 系统正在使用[retained graphics]。您最终应该做的是设计一个系统,在该系统中您只发送“与前一帧相比发生的变化”——仅此而已,您根本不应该创建新对象。这不是关于“创建对象需要零秒”,而是关于它如何影响渲染和时间。这是关于让 WPF 使用缓存来完成它的工作。

Sending new objects to the GPU for rendering=slow. Sending only updates to the GPU which tells what objects moved=fast.

将新对象发送到 GPU 进行渲染 =。仅向 GPU 发送更新,告知哪些对象移动 =

Also, it's possible to create Visuals in an arbitrary threadto improve the performance (Multithreaded UI: HostVisual - Dwayne Need). That all said, if your project is pretty complex in 3D wise - there's good chance that WPF won't just cut it. Using DirectX.. directly, is much, much, more performant!

此外,可以在任意线程中创建视觉效果以提高性能(多线程 UI:HostVisual - Dwayne Need)。总而言之,如果您的项目在 3D 方面非常复杂 - WPF 很有可能不会只是削减它。直接使用 DirectX.. 性能更高!

Some of the articles I suggest you to read & understand:

我建议您阅读和理解的一些文章:

[Writing More Efficient ItemsControls - Charles Petzold]- understand the process how one achieves better drawing rate in WPF.

[Writing More Efficient ItemsControls - Charles Petzold]- 了解如何在 WPF 中实现更好的绘制率的过程。

As for why your UI is lagging, Dan answer seems to be spot on. If you are trying to render more than WPF can handle, the input system will suffer.

至于为什么您的用户界面滞后,Dan 的回答似乎恰到好处。如果您尝试渲染的内容超出 WPF 的处理能力,输入系统就会受到影响。