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