7

我现在正在尝试像“懒惰”的 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) 以完全不同的方法完成工作?

谢谢!!!

4

2 回答 2

4

我已经通过反射使用 WPF 的内部监视控件的视觉状态。因此,我编写的代码挂钩到 CompositionTarget.Rendering 事件,遍历树,并查找子树中的任何更改。我正在编写它来拦截推送到 MilCore 的数据,然后将其用于我自己的目的,所以将此代码视为 hack,仅此而已。如果它对你有帮助,那就太好了。我在 .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-04-25T04:59:37.137 回答
1

我认为您的解决方案已经很好了。您可以尝试使用具有 ApplicationIdle 优先级的 Dispatcher 回调来代替计时器,这将有效地使更新变得懒惰,因为它只会在应用程序不忙时发生。此外,正如您已经说过的,您可能会尝试使用 BitmapCacheBrush 而不是 VisualBrush 来绘制概览图像,看看这是否有任何区别。

关于何时重绘画笔的问题:

基本上,您想知道事情何时以将现有缩略图标记为脏的方式发生变化。

我认为你可以在后端/模型中攻击这个问题并在那里有一个脏标志,或者尝试从前端获取它。

后端显然取决于您的应用程序,所以我无法评论。

在前端, LayoutUpdated 事件似乎是正确的做法,但正如您所说,它可能会比必要的更频繁地触发。

这是黑暗中的一个镜头 - 我不知道 LayoutUpdated 在内部是如何工作的,因此它可能与 LayoutUpdated 存在相同的问题:您可以在要观察的控件中覆盖 ArrangeOverride。每当调用 ArrangeOverride 时,您都会使用调度程序触发您自己的布局更新事件,以便在布局传递完成后触发它。(如果同时应该调用新的 ArrangeOverride,甚至可能再等待几毫秒,并且不要排队更多事件)。由于布局传递将始终调用 Measure 然后 Arrange 并沿树向上移动,这应该涵盖控件内任何位置的任何更改。

于 2011-05-05T05:08:43.947 回答