WPF «Ленивая» VisualBrush

Я пытаюсь реализовать что-то вроде "Lazy" VisualBrush прямо сейчас. Кто-нибудь знает, как это сделать? Смысл: Что-то, что ведет себя как VisualBrush, но не обновляется при каждом изменении в Visual, но не чаще одного раза в секунду (или что-то еще).

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

Проблема: Моя работа прямо сейчас - улучшить производительность довольно большого приложения WPF. Я отследил основную проблему производительности (во всяком случае, на уровне пользовательского интерфейса) в некоторых визуальных кистях, используемых в приложении. Приложение состоит из области «Рабочий стол» с некоторыми довольно сложными пользовательскими элементами управления и области навигации, содержащей уменьшенную версию рабочего стола. Область навигации использует визуальные кисти для выполнения работы. Все в порядке, пока элементы рабочего стола более или менее статичны. Но если элементы часто меняются (например, потому что они содержат анимацию), VisualBrushes сходят с ума. Они будут обновляться вместе с частотой кадров анимации. Понижение частоты кадров, конечно, помогает, но я ищу более общее решение этой проблемы. В то время как «исходный» элемент управления отображает только небольшую область, на которую влияет анимация, контейнер визуальной кисти визуализируется полностью, в результате чего производительность приложения падает. Я уже пробовал использовать вместо этого BitmapCacheBrush. К сожалению, не помогает. Анимация находится внутри элемента управления. Так что кисть все равно придется обновлять.

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

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

Спасибо !!!

7
задан harri 23 April 2011 в 13:19
поделиться