我使用DrawText
在WPF中创建大量文本,然后将其添加到单个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;
}
答案 0 :(得分:16)
我目前正在处理可能存在同样问题的事情,而且我发现了一些非常意外的事情。我正在渲染到WriteableBitmap并允许用户滚动(缩放)和平移以更改渲染的内容。对于缩放和平移,这个动作看起来都很不稳定,所以我自然认为渲染时间太长了。经过一些仪器测试后,我确认我的渲染速度为30-60 fps。无论用户如何缩放或平移,渲染时间都不会增加,因此波动性必须来自其他地方。
我看了OnMouseMove事件处理程序。当WriteableBitmap每秒更新30-60次时,MouseMove事件每秒仅触发1-2次。如果我减小WriteableBitmap的大小,MouseMove事件会更频繁地触发,并且平移操作显得更平滑。因此,紊乱实际上是由于MouseMove事件不稳定而不是渲染(例如,WriteableBitmap渲染7-10帧看起来相同,MouseMove事件触发,然后WriteableBitmap渲染7-10帧的新渲染图像等等。)
我尝试通过使用Mouse.GetPosition(this)每次WriteableBitmap更新时轮询鼠标位置来跟踪平移操作。但是,结果相同,因为在更改为新值之前返回的鼠标位置对于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每次调用时都会返回一个新位置(当鼠标移动时),因此每个帧在用户平移时呈现的位置略有不同。同样的不稳定似乎正在影响MouseWheel事件,我不知道如何解决这个问题。
因此,尽管有关有效维护视觉树的所有上述建议都是很好的做法,但我怀疑您的性能问题可能是由于某些因素干扰鼠标事件频率造成的。在我的情况下,似乎由于某种原因,渲染导致鼠标事件更新并且比平常更慢。如果我找到一个真正的解决方案而不是这个部分解决方案,我会更新这个。
编辑:好的,我进一步挖掘了这一点,我想我现在明白了发生了什么。我将使用更详细的代码示例进行解释:
我通过注册处理CompositionTarget.Rendering事件来逐帧渲染我的位图,如this MSDN article.中所述。基本上,这意味着每次渲染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事件。当渲染代码需要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,而不是从原始几何数据重建像素数据。
答案 1 :(得分:4)
可能的罪魁祸首是你在每个车轮事件中清理和重建你的可视树。根据你自己的帖子,那棵树包括一个大数字"文本元素。对于每个进入的事件,必须重新创建,重新格式化,测量和最终呈现每个文本元素。这不是完成简单文本缩放的方法。
不是在每个ScaleTransform
元素上设置FormattedText
,而是在包含文本的元素上设置一个RenderTransform
。根据您的需要,您可以设置LayoutTransform
或Scale
。然后,当您收到滚轮事件时,请相应地调整ItemsControl
属性。不要在每个事件上重建文本。
我也会做其他人推荐的事情,并将{{1}}绑定到列列表并以这种方式生成文本。没有理由你需要手工完成这个。
答案 2 :(得分:4)
@Vahid:WPF系统正在使用[retained graphics]。你最终应该做的是设计一个系统,你只发送"与之前的框架相比发生了什么变化" - 仅此而已,您根本不应该创建新对象。它不是关于"创建对象需要零秒"它关于它如何影响渲染和时间。它是关于让WPF使用缓存来完成它的工作。
将新对象发送到GPU以进行渲染= 慢。仅向GPU发送更新,告知移动的对象= 快速。
此外,可以在任意线程中创建Visuals 以提高性能(Multithreaded UI: HostVisual - Dwayne Need)。大家都这么说,如果你的项目在3D方面非常复杂 - 那么WPF很有可能只是削减它。直接使用DirectX ..,性能更高,更好!
我建议你阅读的一些文章&理解:
[Writing More Efficient ItemsControls - Charles Petzold] - 了解如何在WPF中实现更好的绘图速率的过程。
至于为什么你的用户界面滞后,Dan的回答似乎就在眼前。如果您尝试渲染超过WPF可以处理的内容,输入系统将受到影响。