Я сейчас пытаюсь реализовать что-то вроде «Ленивого» 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
или же
б) выполнить работу с совершенно другим подходом?
Спасибо !!!