現在、「Lazy」VisualBrush のようなものを実装しようとしています。誰かがその方法を知っていますか? 意味: VisualBrush のように動作しますが、Visual のすべての変更で更新されるのではなく、最大で 1 秒に 1 回 (または何でも) 更新されるもの。
なぜ私がこれを行っているのか、そして私がすでに試したことについて、いくつかの背景を説明する必要があります:)
問題: 現在の私の仕事は、かなり大きな WPF アプリケーションのパフォーマンスを改善することです。主なパフォーマンスの問題 (とにかく UI レベルで) を、アプリケーションで使用されているいくつかのビジュアル ブラシに突き止めました。アプリケーションは、いくつかのかなり複雑な UserControls を含む「デスクトップ」領域と、デスクトップの縮小バージョンを含むナビゲーション領域で構成されます。ナビゲーション エリアでは、ビジュアル ブラシを使用して作業を完了しています。デスクトップ項目が多かれ少なかれ静的である限り、すべて問題ありません。ただし、要素が頻繁に変更される場合 (たとえば、アニメーションが含まれているため)、VisualBrushes は暴走します。アニメーションのフレームレートとともに更新されます。フレームレートを下げることはもちろん役に立ちますが、この問題に対するより一般的な解決策を探しています。「ソース」でありながら コントロールは、アニメーションの影響を受ける小さな領域のみをレンダリングします。ビジュアル ブラシ コンテナが完全にレンダリングされると、アプリケーションのパフォーマンスが大幅に低下します。代わりに 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 を使用すると、ビジュアル自体がまったく変更されていなくても、レンダリングが常にトリガーされます (おそらく、アプリケーションの他の部分のアニメーションなどのため)。LayoutUpdated は頻繁に発生します。実際のところ、トリガーをスキップして、トリガーなしでタイマーを使用してコントロールを更新するだけで済みます。それは問題ではありません。また、Visual で OnRender をオーバーライドし、カスタム イベントを発生させて更新をトリガーしようとしました。VisualTree の奥深くで何かが変更されたときに OnRender が呼び出されないため、どちらも機能しません。これが今の私のベストショットです。元の VisualBrush ソリューションよりもはるかにうまく機能しています (少なくともパフォーマンスの観点から)。しかし、私はまださらに良い解決策を探しています。
a) nessasarry の場合にのみ更新をトリガーする方法、または b) まったく異なるアプローチで仕事を完了する方法を知っている人はいますか?
ありがとう!!!