C# · 12月 27, 2021

c# – WPF中性能低下的原因

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

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

那么可能是什么问题呢?很久以前,我想我读到某个地方,实际上是渲染,花费时间,而不是创建和添加视觉效果.

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

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; }解决方法 我正在处理可能是同样的问题,我发现了一些非常意想不到的事情.我正在渲染到一个WriteableBitmap,并允许用户滚动(缩放)和平移来更改所渲染的内容.这个动作对于缩放和平移来说似乎都是不稳定的,所以我自然认为渲染花费的时间太长.经过一些测试,我验证了我以30-60 fps渲染.渲染时间没有增加,无论用户如何缩放或平移,所以choppiness必须来自其他地方.

我看着OnMouseMove事件处理程序.虽然WriteableBitmap每秒更新30-60次,但是MouseMove事件每秒只能触发1-2次.如果我减小WriteableBitmap的大小,MouseMove事件更频繁地触发,并且平移操作看起来更平滑.所以,chopiness实际上是由于MouseMove事件被破坏的结果,而不是渲染(例如WriteableBitmap渲染7-10帧,看起来相同,MouseMove事件触发,然后WriteableBitmap渲染新的图像的7-10帧,等等).

每当WriteableBitmap使用Mouse.GetPosition(this)更新时,通过轮询鼠标位置,我尝试跟踪平移操作.然而,这样做的结果相同,因为在更改为新值之前,返回的鼠标位置将与7-10帧相同.

然后我尝试使用PInvoke服务GetCursorPos like in this SO answer轮询鼠标位置,例如:

[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每次调用时都会返回一个新的位置(当鼠标移动时),所以每个帧在用户平移的同时呈现在稍微不同的位置.同样的choppiness似乎影响了MouseWheel事件,我不知道如何解决这个问题.

所以,尽管上述关于有效维护视觉树的建议是很好的做法,但我怀疑你的性能问题可能是某些干扰鼠标事件频率的结果.在我的情况下,似乎由于某些原因,渲染导致鼠标事件更新并且比平常更慢.如果我找到一个真正的解决方案,而不是这个部分解决方法,我会更新这个.

编辑:好的,我再多挖一点,我想我现在明白了.我将用更详细的代码示例来解释:

我按照每帧渲染到我的位图,通过注册来处理如this MSDN article.所述的CompositionTarget.Rendering事件.基本上,这意味着每次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事件(和所有的鼠标事件真的)被调用的频率要低得多.当渲染代码需要15ms时,MouseMove事件将每隔几ms调用一次.当渲染代码需要30ms时,MouseMove事件将每几百毫秒被调用.我的理论为什么会发生这样的情况:渲染发生在同一个线程上,WPF鼠标系统更新其值并触发鼠标事件.该线程上的WPF循环必须具有一些条件逻辑,如果在一帧中渲染需要太长时间,它将跳过执行鼠标更新.当我的渲染代码在每一帧都需要“太长”时,会出现问题.然后,由于渲染每帧需要15个额外的ms,因此界面看起来慢了一点点,因此界面很大程度上是因为额外的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执行它的渲染路径时,它所需要做的就是将这些位图blit,而不是从原始几何数据重构像素数据.