我现在正在尝试像“懒惰”的 VisualBrush 那样实现一些东西。有人知道怎么做吗?含义:行为类似于 VisualBrush 但不会在 Visual 中的每次更改时更新,但最多每秒一次(或其他)。
我最好应该提供一些背景,为什么我要这样做,我猜我已经尝试过什么:)
问题:我现在的工作是提高一个相当大的 WPF 应用程序的性能。我追踪了应用程序中使用的一些视觉画笔的主要性能问题(无论如何都是在 UI 级别)。该应用程序由一个带有一些相当复杂的用户控件的“桌面”区域和一个包含缩小版桌面的导航区域组成。导航区域正在使用视觉画笔来完成工作。只要桌面项目或多或少是静态的,一切都很好。但是如果元素经常变化(例如因为它们包含动画),VisualBrushes 就会变得疯狂。它们将随着动画的帧速率而更新。降低帧率当然有帮助,但我正在寻找更通用的解决方案来解决这个问题。而“源” 控件仅渲染受动画影响的小区域视觉画笔容器被完全渲染导致应用程序性能下降。我已经尝试使用 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) 以完全不同的方法完成工作?
谢谢!!!