我现在正在尝试实现一些像“懒惰”VisualBrush这样的东西。有没有人知道该怎么做?含义:行为类似于VisualBrush的东西,但不会随着视觉中的每个变化而更新,而是最多每秒更新一次(或者其他什么)。
我最好给出一些我为什么要这样做的背景知识,以及我已经尝试过的东西,我想:)
问题:我现在的工作是提高一个相当大的WPF应用程序的性能。我追踪到了应用程序中使用的一些可视化笔刷的主要性能问题(无论如何是在UI级别)。这个应用程序由一个带有一些相当复杂的UserControls的“桌面”区域和一个包含桌面缩小版本的导航区组成。导航区使用视觉画笔来完成工作。只要桌面项目或多或少是静态的,一切都很好。但是,如果元素频繁变化(例如,因为它们包含动画),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)使用完全不同的方法完成工作?
谢谢!
发布于 2011-04-25 12:59:37
我已经通过反射使用WPF的内部监控了控件的可视状态。因此,我编写的代码挂钩到CompositionTarget.Rendering事件中,遍历树,并查找子树中的任何更改。我写这段代码是为了拦截推送到MilCore的数据,然后将其用于我自己的目的,所以请将这段代码看作是一种黑客行为,仅此而已。如果对你有帮助,那就太好了。我在.NET 4上使用了这个。
首先,遍历树的代码读取状态标志:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Media;
using System.Reflection;
namespace MilSnatch.Utils
{
public static class VisualTreeHelperPlus
{
public static IEnumerable<DependencyObject> WalkTree(DependencyObject root)
{
yield return root;
int count = VisualTreeHelper.GetChildrenCount(root);
for (int i = 0; i < count; i++)
{
foreach (var descendant in WalkTree(VisualTreeHelper.GetChild(root, i)))
yield return descendant;
}
}
public static CoreFlags ReadFlags(UIElement element)
{
var fieldInfo = typeof(UIElement).GetField("_flags", BindingFlags.Instance | BindingFlags.NonPublic);
return (CoreFlags)fieldInfo.GetValue(element);
}
public static bool FlagsIndicateUpdate(UIElement element)
{
return (ReadFlags(element) &
(
CoreFlags.ArrangeDirty |
CoreFlags.MeasureDirty |
CoreFlags.RenderingInvalidated
)) != CoreFlags.None;
}
}
[Flags]
public enum CoreFlags : uint
{
AreTransformsClean = 0x800000,
ArrangeDirty = 8,
ArrangeInProgress = 0x20,
ClipToBoundsCache = 2,
ExistsEventHandlersStore = 0x2000000,
HasAutomationPeer = 0x100000,
IsCollapsed = 0x200,
IsKeyboardFocusWithinCache = 0x400,
IsKeyboardFocusWithinChanged = 0x800,
IsMouseCaptureWithinCache = 0x4000,
IsMouseCaptureWithinChanged = 0x8000,
IsMouseOverCache = 0x1000,
IsMouseOverChanged = 0x2000,
IsOpacitySuppressed = 0x1000000,
IsStylusCaptureWithinCache = 0x40000,
IsStylusCaptureWithinChanged = 0x80000,
IsStylusOverCache = 0x10000,
IsStylusOverChanged = 0x20000,
IsVisibleCache = 0x400000,
MeasureDirty = 4,
MeasureDuringArrange = 0x100,
MeasureInProgress = 0x10,
NeverArranged = 0x80,
NeverMeasured = 0x40,
None = 0,
RenderingInvalidated = 0x200000,
SnapsToDevicePixelsCache = 1,
TouchEnterCache = 0x80000000,
TouchesCapturedWithinCache = 0x10000000,
TouchesCapturedWithinChanged = 0x20000000,
TouchesOverCache = 0x4000000,
TouchesOverChanged = 0x8000000,
TouchLeaveCache = 0x40000000
}
}接下来,支持渲染事件的代码:
//don't worry about RenderDataWrapper. Just use some sort of WeakReference wrapper for each UIElement
void CompositionTarget_Rendering(object sender, EventArgs e)
{
//Thread.Sleep(250);
Dictionary<int, RenderDataWrapper> newCache = new Dictionary<int, RenderDataWrapper>();
foreach (var rawItem in VisualTreeHelperPlus.WalkTree(m_Root))
{
var item = rawItem as FrameworkElement;
if (item == null)
{
Console.WriteLine("Encountered non-FrameworkElement: " + rawItem.GetType());
continue;
}
int hash = item.GetHashCode();
RenderDataWrapper cacheEntry;
if (!m_Cache.TryGetValue(hash, out cacheEntry))
{
cacheEntry = new RenderDataWrapper();
cacheEntry.SetControl(item);
newCache.Add(hash, cacheEntry);
}
else
{
m_Cache.Remove(hash);
newCache.Add(hash, cacheEntry);
}
//check the visual for updates - something like the following...
if(VisualTreeHelperPlus.FlagsIndicateUpdate(item as UIElement))
{
//flag for new snapshot.
}
}
m_Cache = newCache;
}无论如何,通过这种方式,我监视了可视化树的更新,我认为如果您愿意,可以使用类似的东西来监视它们。这远不是最佳实践,但有时实用的代码必须是最佳实践。当心。
发布于 2011-05-05 13:08:44
我认为你的解决方案已经相当不错了。您可以尝试使用具有ApplicationIdle优先级的Dispatcher回调来代替计时器,这将有效地使更新变得懒惰,因为只有在应用程序不忙时才会发生更新。此外,正如您已经说过的,您可以尝试使用BitmapCacheBrush而不是VisualBrush来绘制概览图像,看看这是否有什么不同。
关于你关于何时重新绘制画笔的问题:
基本上,你想知道什么时候事情发生了变化,会将你现有的缩略图标记为脏的。
我认为你可以在后端/模型中解决这个问题,并在那里设置一个脏标志,或者尝试从前端获取它。
后端显然依赖于你的应用程序,所以我不能评论。
在前端,LayoutUpdated事件似乎是应该做的事情,但正如您所说的,它可能会频繁触发。
这里有一个试金石--我不知道LayoutUpdated是如何在内部工作的,所以它可能会有和LayoutUpdated一样的问题:你可以在你想观察的控件中覆盖ArrangeOverride。每当调用ArrangeOverride时,您都会使用调度程序触发您自己的布局更新事件,以便在布局传递完成后触发它。(甚至可以多等待几毫秒,如果在此期间应该调用新的ArrangeOverride,则不要对更多的事件进行排队)。因为布局传递总是调用Measure,然后在树上排列和遍历,所以这应该涵盖控件内任何地方的任何更改。
https://stackoverflow.com/questions/5764382
复制相似问题