我现在正在尝试实现类似于“懒惰”的VisualBrush。有人知道如何实现吗?我的意思是:它的行为类似于VisualBrush,但不会在每次Visual更改时更新,而是最多每秒一次(或其他频率)。
我应该先解释一下我为什么要这样做以及我已经尝试过什么了 :)
问题:我现在的工作是提高一个相当大的WPF应用程序的性能。我在应用程序中跟踪到了主要的性能问题(至少在UI层面上),那就是应用程序中使用的一些视觉画刷。应用程序由一个包含一些相当复杂的UserControls的“桌面”区域和一个包含缩小版本桌面的导航区域组成。导航区域使用视觉画刷来完成任务。只要桌面项目更或多或少是静态的,一切都很好。但是,如果元素频繁更改(因为它们包含动画,例如),则VisualBrushes会变得非常混乱。它们将随着动画的帧速率进行更新。降低帧速率当然有所帮助,但我正在寻找更一般的解决方案来解决这个问题。虽然“源”控件仅呈现动画所影响的小区域,但视觉画刷容器会完全呈现,导致应用程序性能降至极低。我已经尝试使用BitmapCacheBrush,但不幸的是没有帮助。动画在控件内部。因此,画刷必须被刷新。
可能的解决方案:我创建了一个行为与VisualBrush类似的控件。它获取一些可视化内容(如VisualBrush),但使用DiapatcherTimer和RenderTargetBitmap来完成工作。现在,我订阅控件的LayoutUpdated事件,每当它更改时,它就会被安排进行“渲染”(使用RenderTargetBitmap)。实际渲染由DispatcherTimer触发。这样,控件最多每秒渲染一次。
以下是代码:
这个方案到目前为止运作得很好。唯一的问题是触发器。当我使用LayoutUpdated时,即使可视化本身没有任何更改(可能是因为应用程序中其他部分的动画或其他原因),渲染也会不断触发。事实上,LayoutUpdated被频繁触发。其实我可以跳过触发器,只使用定时器更新控件。这并不重要。我还尝试在Visual中覆盖OnRender并引发自定义事件来触发更新。这也行不通,因为当VisualTree内部的某些内容发生更改时,OnRender不会被调用。这是我目前最好的方法。从性能角度来看,它已经比原始的VisualBrush解决方案好多了。但我仍在寻找更好的解决方案。
有人有想法如何 a)仅在必要时触发更新 或 b)用完全不同的方法完成任务吗?
谢谢!!!
我应该先解释一下我为什么要这样做以及我已经尝试过什么了 :)
问题:我现在的工作是提高一个相当大的WPF应用程序的性能。我在应用程序中跟踪到了主要的性能问题(至少在UI层面上),那就是应用程序中使用的一些视觉画刷。应用程序由一个包含一些相当复杂的UserControls的“桌面”区域和一个包含缩小版本桌面的导航区域组成。导航区域使用视觉画刷来完成任务。只要桌面项目更或多或少是静态的,一切都很好。但是,如果元素频繁更改(因为它们包含动画,例如),则VisualBrushes会变得非常混乱。它们将随着动画的帧速率进行更新。降低帧速率当然有所帮助,但我正在寻找更一般的解决方案来解决这个问题。虽然“源”控件仅呈现动画所影响的小区域,但视觉画刷容器会完全呈现,导致应用程序性能降至极低。我已经尝试使用BitmapCacheBrush,但不幸的是没有帮助。动画在控件内部。因此,画刷必须被刷新。
可能的解决方案:我创建了一个行为与VisualBrush类似的控件。它获取一些可视化内容(如VisualBrush),但使用DiapatcherTimer和RenderTargetBitmap来完成工作。现在,我订阅控件的LayoutUpdated事件,每当它更改时,它就会被安排进行“渲染”(使用RenderTargetBitmap)。实际渲染由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)仅在必要时触发更新 或 b)用完全不同的方法完成任务吗?
谢谢!!!
UpdateBuffer
会触发另一个LayoutUpdated)。这本身并不是很糟糕,但第二个问题是:当可视化中有动画时,LayoutUpdated不会被触发。所以最终问题就是这个。 - Markus Hütter