WPF“惰性”视觉画笔
我现在正在尝试实现像“Lazy”VisualBrush 这样的东西。有人知道如何做到这一点吗?含义:行为类似于 VisualBrush 的东西,但不会针对视觉中的每次更改进行更新,而是最多每秒更新一次(或其他)。
我最好应该提供一些背景知识,说明为什么我要这样做,以及我猜想我已经尝试过什么:)
问题:我现在的工作是提高一个相当大的 WPF 应用程序的性能。我跟踪了应用程序中使用的一些视觉画笔的主要性能问题(无论如何在 UI 级别)。该应用程序由一个带有一些相当复杂的用户控件的“桌面”区域和一个包含桌面缩小版本的导航区域组成。导航区域使用视觉画笔来完成工作。只要桌面项目或多或少是静态的,一切都很好。但是,如果元素频繁更改(例如,因为它们包含动画),VisualBrush 就会变得疯狂。它们将随着动画的帧速率一起更新。降低帧速率当然有帮助,但我正在寻找解决此问题的更通用的解决方案。虽然“源”控件仅渲染受动画影响的小区域,但视觉画笔容器会完全渲染,导致应用程序性能下降。我已经尝试使用 BitmapCacheBrush 来代替。不幸的是没有帮助。动画位于控件内部。所以刷子无论如何都要刷新。
可能的解决方案:我创建了一个行为或多或少类似于 VisualBrush 的控件。它需要一些视觉效果(如 VisualBrush),但使用 DiapatcherTimer 和 RenderTargetBitmap 来完成这项工作。现在,我正在订阅控件的 LayoutUpdated 事件,每当它发生更改时,都会安排“渲染”(使用 RenderTargetBitmap)。然后实际的渲染由 DispatcherTimer 触发。这样,控件将以 DispatcherTimer 的最大频率重新绘制自身。
这是代码:
public sealed class VisualCopy : Border
{
#region private fields
private const int mc_mMaxRenderRate = 500;
private static DispatcherTimer ms_mTimer;
private static readonly Queue<VisualCopy> ms_renderingQueue = new Queue<VisualCopy>();
private static readonly object ms_mQueueLock = new object();
private VisualBrush m_brush;
private DrawingVisual m_visual;
private Rect m_rect;
private bool m_isDirty;
private readonly Image m_content = new Image();
#endregion
#region constructor
public VisualCopy()
{
m_content.Stretch = Stretch.Fill;
Child = m_content;
}
#endregion
#region dependency properties
public FrameworkElement Visual
{
get { return (FrameworkElement)GetValue(VisualProperty); }
set { SetValue(VisualProperty, value); }
}
// Using a DependencyProperty as the backing store for Visual. This enables animation, styling, binding, etc...
public static readonly DependencyProperty VisualProperty =
DependencyProperty.Register("Visual", typeof(FrameworkElement), typeof(VisualCopy), new UIPropertyMetadata(null, OnVisualChanged));
#endregion
#region callbacks
private static void OnVisualChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var copy = obj as VisualCopy;
if (copy != null)
{
var oldElement = args.OldValue as FrameworkElement;
var newelement = args.NewValue as FrameworkElement;
if (oldElement != null)
{
copy.UnhookVisual(oldElement);
}
if (newelement != null)
{
copy.HookupVisual(newelement);
}
}
}
private void OnVisualLayoutUpdated(object sender, EventArgs e)
{
if (!m_isDirty)
{
m_isDirty = true;
EnqueuInPipeline(this);
}
}
private void OnVisualSizeChanged(object sender, SizeChangedEventArgs e)
{
DeleteBuffer();
PrepareBuffer();
}
private static void OnTimer(object sender, EventArgs e)
{
lock (ms_mQueueLock)
{
try
{
if (ms_renderingQueue.Count > 0)
{
var toRender = ms_renderingQueue.Dequeue();
toRender.UpdateBuffer();
toRender.m_isDirty = false;
}
else
{
DestroyTimer();
}
}
catch (Exception ex)
{
}
}
}
#endregion
#region private methods
private void HookupVisual(FrameworkElement visual)
{
visual.LayoutUpdated += OnVisualLayoutUpdated;
visual.SizeChanged += OnVisualSizeChanged;
PrepareBuffer();
}
private void UnhookVisual(FrameworkElement visual)
{
visual.LayoutUpdated -= OnVisualLayoutUpdated;
visual.SizeChanged -= OnVisualSizeChanged;
DeleteBuffer();
}
private static void EnqueuInPipeline(VisualCopy toRender)
{
lock (ms_mQueueLock)
{
ms_renderingQueue.Enqueue(toRender);
if (ms_mTimer == null)
{
CreateTimer();
}
}
}
private static void CreateTimer()
{
if (ms_mTimer != null)
{
DestroyTimer();
}
ms_mTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(mc_mMaxRenderRate) };
ms_mTimer.Tick += OnTimer;
ms_mTimer.Start();
}
private static void DestroyTimer()
{
if (ms_mTimer != null)
{
ms_mTimer.Tick -= OnTimer;
ms_mTimer.Stop();
ms_mTimer = null;
}
}
private RenderTargetBitmap m_targetBitmap;
private void PrepareBuffer()
{
if (Visual.ActualWidth > 0 && Visual.ActualHeight > 0)
{
const double topLeft = 0;
const double topRight = 0;
var width = (int)Visual.ActualWidth;
var height = (int)Visual.ActualHeight;
m_brush = new VisualBrush(Visual);
m_visual = new DrawingVisual();
m_rect = new Rect(topLeft, topRight, width, height);
m_targetBitmap = new RenderTargetBitmap((int)m_rect.Width, (int)m_rect.Height, 96, 96, PixelFormats.Pbgra32);
m_content.Source = m_targetBitmap;
}
}
private void DeleteBuffer()
{
if (m_brush != null)
{
m_brush.Visual = null;
}
m_brush = null;
m_visual = null;
m_targetBitmap = null;
}
private void UpdateBuffer()
{
if (m_brush != null)
{
var dc = m_visual.RenderOpen();
dc.DrawRectangle(m_brush, null, m_rect);
dc.Close();
m_targetBitmap.Render(m_visual);
}
}
#endregion
}
到目前为止,效果很好。唯一的问题是触发器。当我使用 LayoutUpdated 时,即使视觉本身根本没有改变(可能是因为应用程序其他部分的动画或其他原因),渲染也会不断触发。 LayoutUpdated 经常被触发。事实上,我可以跳过触发器,只使用计时器更新控件,而无需任何触发器。没关系。我还尝试重写 Visual 中的 OnRender 并引发自定义事件来触发更新。也不起作用,因为当 VisualTree 深处的某些内容发生变化时,不会调用 OnRender。这是我现在最好的一击。它的工作效果已经比原来的 VisualBrush 解决方案好得多(至少从性能的角度来看)。但我仍在寻找更好的解决方案。
有谁知道如何 a) 仅当 nessasarry 时触发更新 或者 b) 用完全不同的方法完成工作?
谢谢!!!
I'm trying to implement somesting like an "Lazy" VisualBrush right now. Does anybody have an idea how to to that? Meaning: Something that behaves like an VisualBrush but does not update on every change in the Visual but at max once a second (or whatever).
I better should give some background why I'm doing this and what I alreay tried I guess :)
Problem: My job right now is to improve the performance of an rather big WPF application. I tracked down the main performance issue (at the UI level anyway) to some visual brushes used in the application. The application consists of an "Desktop" area with some rather complex UserControls and an Navigation area containing a scaled down version of the Desktop. The navigation area is using visual brushes to get the job done. Everything is fine as long as the Desktop items are more or less static. But if the elements are changing frequently (because they contain an animation for example) the VisualBrushes go wild. They will update along with the framerate of the animations. Lowering the framerate helps of course, but I'm looking for an more general solution to this problem. While the "source" control only renders the small area affected by the animation the visual brush container is rendered completly causing the application performance to go to hell. I already tried to use BitmapCacheBrush instead. Doesn't help unfortunately. The animation is inside the control. So the brush have to be refreshed anyway.
Possible solution: I created a Control behaving more or less like an VisualBrush. It takes some visual (as the VisualBrush) but is using a DiapatcherTimer and RenderTargetBitmap to do the job. Right now I'm subscribing to the LayoutUpdated event of the control and whenever it changes it will be scheduled for "rendering" (using RenderTargetBitmap). The actual rendering then is triggered by the DispatcherTimer. This way the control will repaint itself at maximum in the frequency of the DispatcherTimer.
Here is the code:
public sealed class VisualCopy : Border
{
#region private fields
private const int mc_mMaxRenderRate = 500;
private static DispatcherTimer ms_mTimer;
private static readonly Queue<VisualCopy> ms_renderingQueue = new Queue<VisualCopy>();
private static readonly object ms_mQueueLock = new object();
private VisualBrush m_brush;
private DrawingVisual m_visual;
private Rect m_rect;
private bool m_isDirty;
private readonly Image m_content = new Image();
#endregion
#region constructor
public VisualCopy()
{
m_content.Stretch = Stretch.Fill;
Child = m_content;
}
#endregion
#region dependency properties
public FrameworkElement Visual
{
get { return (FrameworkElement)GetValue(VisualProperty); }
set { SetValue(VisualProperty, value); }
}
// Using a DependencyProperty as the backing store for Visual. This enables animation, styling, binding, etc...
public static readonly DependencyProperty VisualProperty =
DependencyProperty.Register("Visual", typeof(FrameworkElement), typeof(VisualCopy), new UIPropertyMetadata(null, OnVisualChanged));
#endregion
#region callbacks
private static void OnVisualChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var copy = obj as VisualCopy;
if (copy != null)
{
var oldElement = args.OldValue as FrameworkElement;
var newelement = args.NewValue as FrameworkElement;
if (oldElement != null)
{
copy.UnhookVisual(oldElement);
}
if (newelement != null)
{
copy.HookupVisual(newelement);
}
}
}
private void OnVisualLayoutUpdated(object sender, EventArgs e)
{
if (!m_isDirty)
{
m_isDirty = true;
EnqueuInPipeline(this);
}
}
private void OnVisualSizeChanged(object sender, SizeChangedEventArgs e)
{
DeleteBuffer();
PrepareBuffer();
}
private static void OnTimer(object sender, EventArgs e)
{
lock (ms_mQueueLock)
{
try
{
if (ms_renderingQueue.Count > 0)
{
var toRender = ms_renderingQueue.Dequeue();
toRender.UpdateBuffer();
toRender.m_isDirty = false;
}
else
{
DestroyTimer();
}
}
catch (Exception ex)
{
}
}
}
#endregion
#region private methods
private void HookupVisual(FrameworkElement visual)
{
visual.LayoutUpdated += OnVisualLayoutUpdated;
visual.SizeChanged += OnVisualSizeChanged;
PrepareBuffer();
}
private void UnhookVisual(FrameworkElement visual)
{
visual.LayoutUpdated -= OnVisualLayoutUpdated;
visual.SizeChanged -= OnVisualSizeChanged;
DeleteBuffer();
}
private static void EnqueuInPipeline(VisualCopy toRender)
{
lock (ms_mQueueLock)
{
ms_renderingQueue.Enqueue(toRender);
if (ms_mTimer == null)
{
CreateTimer();
}
}
}
private static void CreateTimer()
{
if (ms_mTimer != null)
{
DestroyTimer();
}
ms_mTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(mc_mMaxRenderRate) };
ms_mTimer.Tick += OnTimer;
ms_mTimer.Start();
}
private static void DestroyTimer()
{
if (ms_mTimer != null)
{
ms_mTimer.Tick -= OnTimer;
ms_mTimer.Stop();
ms_mTimer = null;
}
}
private RenderTargetBitmap m_targetBitmap;
private void PrepareBuffer()
{
if (Visual.ActualWidth > 0 && Visual.ActualHeight > 0)
{
const double topLeft = 0;
const double topRight = 0;
var width = (int)Visual.ActualWidth;
var height = (int)Visual.ActualHeight;
m_brush = new VisualBrush(Visual);
m_visual = new DrawingVisual();
m_rect = new Rect(topLeft, topRight, width, height);
m_targetBitmap = new RenderTargetBitmap((int)m_rect.Width, (int)m_rect.Height, 96, 96, PixelFormats.Pbgra32);
m_content.Source = m_targetBitmap;
}
}
private void DeleteBuffer()
{
if (m_brush != null)
{
m_brush.Visual = null;
}
m_brush = null;
m_visual = null;
m_targetBitmap = null;
}
private void UpdateBuffer()
{
if (m_brush != null)
{
var dc = m_visual.RenderOpen();
dc.DrawRectangle(m_brush, null, m_rect);
dc.Close();
m_targetBitmap.Render(m_visual);
}
}
#endregion
}
This works pretty good so far. Only problem is the trigger. When I use LayoutUpdated then the Rendering is triggered constantly even if the Visual itself is not changed at all (propably because of animations in other parts of the application or whatever). LayoutUpdated is just fired way to often. As a matter of fact I could just skip the trigger and just update the control using the timer without any trigger. It doesn't matter. I also tried to override OnRender in the Visual and raise an custom event to trigger the update. Doesn't work either because OnRender is not called when something deep inside the VisualTree changes. This is my best shot right now. It's working much better then the original VisualBrush solution already (from the performance point of view at least). But I'm, still looking for an even better solution.
Does anyone have an idea how to
a) trigger the update only when nessasarry
or
b) get the job done with an altogether differen approach?
Thanks!!!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
我通过反射使用 WPF 的内部结构来监视控件的视觉状态。因此,我编写的代码挂钩 CompositionTarget.Rendering 事件,遍历树,并查找子树中的任何更改。我编写它是为了拦截推送到 MilCore 的数据,然后将其用于我自己的目的,因此将此代码视为黑客攻击,仅此而已。如果它对你有帮助,那就太好了。我在.NET 4上使用这个。
首先,遍历树的代码读取状态标志:
接下来,渲染事件的支持代码:
无论如何,通过这种方式我监视了可视化树的更新,我认为你可以监视它们如果您愿意,可以使用类似的东西。这远非最佳实践,但有时务实的代码必须如此。提防。
I've monitored the visual status of controls using the internals of WPF via reflection. So the code I've written hooks into the CompositionTarget.Rendering event, walks the tree, and looks for any changes in the subtree. I was writing it to intercept data being pushed to MilCore and then use it for my own purposes, so take this code as a hack and nothing more. If it helps you, great. I was using this on .NET 4.
First, the code to walk the tree read the status flags:
Next, supporting code for Rendering event:
Anyways, in this way I monitored the visual tree for updates, and I think you can monitor them using something similar if you'd like. This is far from best practices, but sometimes pragmatic code has to be. Beware.
我认为你的解决方案已经很好了。您可以尝试使用具有 ApplicationIdle 优先级的 Dispatcher 回调来代替计时器,这将有效地使更新变得懒惰,因为它只会在应用程序不忙时发生。另外,正如您已经说过的,您可能会尝试使用 BitmapCacheBrush 而不是 VisualBrush 来绘制概览图像并
看看这是否有什么不同。
关于您关于何时重画画笔的问题:
基本上您想知道事情何时发生变化,从而将现有的缩略图标记为脏。
我认为您可以在后端/模型中解决这个问题并在那里设置脏标志,或者尝试从前端获取它。
后端显然取决于您的应用程序,所以我无法发表评论。
在前端, LayoutUpdated 事件似乎是正确的做法,但正如您所说,它可能会比必要的更频繁地触发。
这是一个黑暗中的镜头 - 我不知道 LayoutUpdated 内部是如何工作的,所以它可能有与 LayoutUpdated 相同的问题:
您可以在要观察的控件中重写 ArrangeOverride。每当调用 ArrangeOverride 时,您都会使用调度程序触发自己的布局更新事件,以便在布局传递完成后触发该事件。 (如果同时调用新的 ArrangeOverride,甚至可能再等待几毫秒,并且不要对更多事件进行排队)。由于布局过程将始终调用“测量”,然后“排列”并沿树向上移动,因此这应该覆盖控件内任何位置的任何更改。
I think your solution is pretty good already. Instead of a timer you could try to do it with a Dispatcher callback with a ApplicationIdle priority, this would effectively make the updates lazy since it will only occur when the application isn't busy. Also, as you have already stated you might try to use the BitmapCacheBrush instead of the VisualBrush to draw your overview image and
see if this makes any difference.
Regarding your question on WHEN to redraw the brush:
Basically you want to know when things changed in a way that would mark your existing thumbnail image as dirty.
I think you could either attack this problem in the backend/model and have a dirty flag there or try to get it from the front end.
Backend obviously depends on your application so I can't comment.
In the front end the LayoutUpdated event seems the right thing to do but as you say it could fire more often than necessary.
Here is a shot in the dark - I don't know how LayoutUpdated works internally so it might have the same problem as LayoutUpdated:
You could override ArrangeOverride in the control you want to observe. Whenever ArrangeOverride is called you fire your own layout updated event using a dispatcher so that it is fired after the layout pass finishes. (maybe even wait for a couple of milliseconds longer and don't queue more events if a new ArrangeOverride should be called in the meanwhile). Since a layout pass will always call Measure and then Arrange and travel up the tree this should cover any changes anywhere inside the control.