WPF “懒加载” VisualBrush

7
我现在正在尝试实现类似于“懒惰”的VisualBrush。有人知道如何实现吗?我的意思是:它的行为类似于VisualBrush,但不会在每次Visual更改时更新,而是最多每秒一次(或其他频率)。
我应该先解释一下我为什么要这样做以及我已经尝试过什么了 :)
问题:我现在的工作是提高一个相当大的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)用完全不同的方法完成任务吗?
谢谢!!!

1
你能解释一下为什么你需要VisualBrush吗?如果它降低了性能,那么值得寻找一种摆脱(实例)VisualBrush的方法。恐怕你当前的解决方案正在慢慢演变成更昂贵的Brush,因为跟踪可能的更改可能比标准VisualBrush更加昂贵。(在开发/维护和最终运行时都是昂贵的) - Emond
1
@Erno在他的问题中详细描述了VisualBrush的使用。他所述的问题并不罕见。想象一下显示比显示器大的地图的“概览地图”,其中包含一个矩形来选择当前显示的视口。这是一个好问题,他的解决方案也很好,现在只需要进行一些微调,这将是未来的一个不错的参考。想象一下可能性,您可以拥有一个VisualBrush/VisualCopy元素,通过属性,您可以打开/关闭即时/异步更新。 - Markus Hütter
实际上,解决方案非常简单。只需采用VisualBrush实现并添加一些代码以将触发器与实际渲染分离即可。不幸的是,ViualBruh的实现方式不允许我这样做。如果您查看此处的实现[http://reflector.webtropy.com/default.aspx/4@0/4@0/DEVDIV_TFS/Dev10/Releases/RTMRel/wpf/src/Core/CSharp/System/Windows/Media/VisualBrush@cs/1305600/VisualBrush@cs],所有相关内容都是内部的。要做到这一点的关键是MediaContext类。由于这是内部的,所以无法钩入渲染:( - harri
@Markus,看来你知道我在说什么。你有类似的问题吗?你是怎么解决的?实际上,我的“解决方案”现在已经足够好用了,但是我无法让 .Net 做我真正想要的事情,这让我很疯狂。这应该是可行的,对吧?@Erno:在发布之前,我应该先读完整个内容 : )。你是对的。我的解决方案比实际的 VisualBrush 要昂贵得多。但是当你的 Visual 快速更新时,应用程序的性能仍然要好得多。想象一下一个动画。默认情况下,该动画将导致 Brush 每秒更新 60 次。 - harri
@Harri,你的代码有一些问题,但核心问题是:首先,layoutupdated不是正确的事件(你已经发现了),因为它在每次更新可视树中的某个布局时都会被触发(所以UpdateBuffer会触发另一个LayoutUpdated)。这本身并不是很糟糕,但第二个问题是:当可视化中有动画时,LayoutUpdated不会被触发。所以最终问题就是这个。 - Markus Hütter
显示剩余8条评论
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;
}

无论如何,在这种方式下,我监控了可视树的更新,如果您愿意,我认为你也可以使用类似的方法来监控它们。这远非最佳实践,但有时务实的代码必须要这样做。请谨慎。

当然,这只是一些示例代码。我需要尽可能多的调试来从milcore推送缓冲区中提取原始结构体。 :D - J Trana
提示:下一次使用 Debug.WriteLine。 - Emond
非常不错。但是如果我在生产代码中使用反射来访问 .Net 内部,我可能会被杀掉 :)。此外,由于我需要监视的控件非常复杂,我不确定在每个渲染事件(如果我记得正确的话,这些事件会非常频繁地发生)上遍历 VisualTree 的性能影响如何。但是聪明的做法肯定值得一赞! - harri

1

我认为你的解决方案已经很好了。你可以尝试使用一个应用程序空闲优先级的Dispatcher回调来代替计时器,这样会使更新变得懒惰,因为它只会在应用程序不忙碌时发生。此外,正如你已经提到的,你可以尝试使用BitmapCacheBrush而不是VisualBrush来绘制概览图像,并查看是否有任何区别。

关于你关于何时重新绘制画刷的问题:

基本上,你想知道什么时候事情发生了改变,以一种标记现有缩略图像为脏的方式。

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

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

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

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


好主意!我刚刚尝试了一下。但由于应用程序在正常操作期间没有太多操作,所以这并没有太大的区别。而且视觉更新每500ms才执行一次。但是,我真正想知道的是如何确定何时重新绘制画笔。主要问题是我没有事件可以属性更新视觉界面。 - harri
虽然我认为你的计时器或ApplicationIdle事件已经足够实现这种功能,但我添加了一些更多的建议。 - Patrick Klug
我很愿意被证明是错的,但我不相信如果一个控件的子子控件拥有布局/渲染流程,ArrangeOverride会被调用。如果那能够运行的话就太好了。 - Markus Hütter
刚试过使用ArrangeOverride。有时它有效,但可悲的是不太可靠。似乎这取决于控件中的哪些更改以及控件的布局方式。在“后端”也无法完成。如果我的控件中有(例如)一个ScrollViewer,则视觉更新根本不会触发或反映在ViewModel中。但还是感谢您的建议。 - harri

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接