7

現在、「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) まったく異なるアプローチで仕事を完了する方法を知っている人はいますか?

ありがとう!!!

4

2 に答える 2

4

リフレクションを介して 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
    }

}

次に、Rendering イベントのサポート コード:

//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;
}

とにかく、この方法でビジュアル ツリーの更新を監視しました。必要に応じて、同様のものを使用して監視できると思います。これはベスト プラクティスとはかけ離れていますが、実用的なコードが必要になる場合もあります。注意してください。

于 2011-04-25T04:59:37.137 に答える
1

あなたの解決策はすでにかなり良いと思います。タイマーの代わりに、ApplicationIdle優先度のDispatcherコールバックを使用してそれを実行しようとすると、アプリケーションがビジーでないときにのみ更新が発生するため、更新が効果的に遅延します。また、すでに述べたように、VisualBrushの代わりにBitmapCacheBrushを使用して概要画像を描画し、これが違いを生むかどうかを確認することもできます。

ブラシを再描画するタイミングに関する質問について:

基本的に、既存のサムネイル画像をダーティとしてマークするような方法で状況が変化したときを知りたいと思います。

バックエンド/モデルでこの問題を攻撃してそこにダーティフラグを設定するか、フロントエンドから取得しようとすることができると思います。

バックエンドは明らかにアプリケーションに依存するため、コメントすることはできません。

フロントエンドでは、LayoutUpdatedイベントを実行するのが正しいように見えますが、あなたが言うように、必要以上に頻繁に発生する可能性があります。

これが暗闇の中でのショットです-LayoutUpdatedが内部でどのように機能するかわからないため、LayoutUpdatedと同じ問題が発生する可能性があります。監視するコントロールでArrangeOverrideをオーバーライドできます。ArrangeOverrideが呼び出されるたびに、ディスパッチャを使用して独自のレイアウト更新イベントを発生させ、レイアウトパスの終了後に発生するようにします。(おそらく、数ミリ秒長く待って、その間に新しいArrangeOverrideを呼び出す必要がある場合は、それ以上のイベントをキューに入れないでください)。レイアウトパスは常にMeasureを呼び出し、次にアレンジしてツリーを上に移動するため、これはコントロール内のすべての変更をカバーする必要があります。

于 2011-05-05T05:08:43.947 に答える