WPF "Ленивый" VisualBrush - PullRequest
       33

WPF "Ленивый" VisualBrush

7 голосов
/ 23 апреля 2011

Я сейчас пытаюсь реализовать что-то вроде «Ленивого» VisualBrush. У кого-нибудь есть идеи, как это сделать? Значение: что-то, что ведет себя как VisualBrush, но не обновляется при каждом изменении в Visual, но не чаще, чем раз в секунду (или что-то в этом роде).

Мне лучше дать некоторую предысторию, почему я делаю это и что я уже пробовал, я думаю:)

Проблема: моя задача сейчас - повысить производительность довольно большого WPF-приложения. Я отследил основную проблему производительности (в любом случае на уровне пользовательского интерфейса) с некоторыми визуальными кистями, используемыми в приложении. Приложение состоит из области «Рабочий стол» с некоторыми довольно сложными пользовательскими элементами управления и областью навигации, содержащей уменьшенную версию рабочего стола. В области навигации используются визуальные кисти, чтобы выполнить работу. Все хорошо, если элементы рабочего стола более или менее статичны. Но если элементы часто меняются (потому что они содержат анимацию, например), VisualBrushes сходит с ума. Они будут обновляться вместе с частотой кадров анимации. Понятно, что снижение частоты кадров помогает, но я ищу более общее решение этой проблемы. В то время как элемент управления «source» отображает только небольшую область, затронутую анимацией, контейнер визуальных кистей визуализируется полностью, в результате чего производительность приложения падает в ад. Я уже пытался использовать 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, рендеринг запускается постоянно, даже если сам Visual не изменяется вообще (вероятно, из-за анимации в других частях приложения или чего-либо еще). LayoutUpdated часто используется. На самом деле я мог бы просто пропустить триггер и просто обновить элемент управления, используя таймер без какого-либо триггера. Это не важно Я также попытался переопределить OnRender в Visual и вызвать пользовательское событие для запуска обновления. Также не работает, потому что OnRender не вызывается, когда что-то глубоко внутри VisualTree меняется. Это мой лучший снимок прямо сейчас. Он работает намного лучше, чем оригинальное решение VisualBrush (по крайней мере, с точки зрения производительности). Но я все еще ищу еще лучшее решение.

У кого-нибудь есть идеи, как а) запускать обновление только тогда, когда nessasarry или же б) выполнить работу с совершенно другим подходом?

Спасибо !!!

Ответы [ 2 ]

4 голосов
/ 25 апреля 2011

Я наблюдал за визуальным состоянием элементов управления, используя внутреннюю часть 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;
}

В любом случае, таким образом я контролировал визуальное дерево обновлений, и я думаю, что вы можете отслеживать их, используя что-то подобное, если хотите.Это далеко от лучших практик, но иногда должен быть прагматичный код.Осторожно.

1 голос
/ 05 мая 2011

Я думаю, что ваше решение уже довольно хорошее.Вместо таймера вы можете попытаться сделать это с обратным вызовом Dispatcher с приоритетом ApplicationIdle, это фактически сделает обновления ленивыми, поскольку это произойдет только тогда, когда приложение не занято.Кроме того, как вы уже заявили, вы можете попытаться использовать BitmapCacheBrush вместо VisualBrush для рисования вашего обзорного изображения и посмотреть, имеет ли это какое-либо значение.

По поводу вашего вопроса КОГДА перерисовать кисть:

В основном вы хотите знать, когда что-то изменилось таким образом, чтобы пометить существующее эскизное изображение как грязное.

Я думаю, что вы могли бы либо решить эту проблему в бэкэнде / модели и установить там флаг "грязный", либопопробуйте получить его из внешнего интерфейса.

Очевидно, что бэкэнд зависит от вашего приложения, поэтому я не могу комментировать.

В внешнем интерфейсе событие LayoutUpdated кажется правильным, но как выскажем, он может срабатывать чаще, чем необходимо.

Вот выстрел в темноте - я не знаю, как LayoutUpdated работает внутри, поэтому у него может быть та же проблема, что и у LayoutUpdated: вы можете переопределить ArrangeOverride в элементе управления, который вы используете.хочу наблюдать.Всякий раз, когда вызывается ArrangeOverride, вы запускаете свое собственное событие обновления макета с помощью диспетчера, чтобы оно запускалось после завершения прохода макета.(возможно, даже подождите пару миллисекунд дольше и не ставьте в очередь больше событий, если в это время должен быть вызван новый ArrangeOverride).Поскольку проход макета всегда будет вызывать Measure, а затем Arrange и перемещаться вверх по дереву, это должно охватывать любые изменения в любом месте внутри элемента управления.

...