13

我正在使用 WPF 中创建大量文本,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;
    }
4

3 回答 3

19

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

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

每次使用 Mouse.GetPosition(this) 更新 WriteableBitmap 时,我都尝试通过轮询鼠标位置来跟踪平移操作。但是,这具有相同的结果,因为在更改为新值之前,返回的鼠标位置对于 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 事件,我不知道如何解决这个问题。

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


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

我通过注册处理 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);
  }
}

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

我之前提到的 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 执行它的渲染路径时,它所要做的就是对这些位图进行 blit,而不是从原始几何数据中重建像素数据。

于 2014-06-24T17:29:02.533 回答
5

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

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

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

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

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

至于为什么您的 UI 滞后,Dan 的回答似乎是正确的。如果您尝试渲染的内容超出了 WPF 的处理能力,那么输入系统将会受到影响。

于 2014-12-28T13:09:12.087 回答
4

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

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

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

于 2014-06-09T14:36:27.787 回答