WPF "Lazy" VisualBrush

I am trying to implement somesting as a "Lazy" VisualBrush right now. Does anyone have an idea how to do this? Meaning: something that behaves like a VisualBrush, but not updated every time it changes in Visual, but at max once per second (or something else).

I better give some prerequisites why I am doing this, and that I, as I try, I think :)

Problem: Now my job is to improve the performance of a fairly large WPF application. I tracked the main performance issues (at the user interface level) in any case with some of the visual brushes used in the application. The application consists of a “Desktop” area with some rather complicated UserControls and a navigation area containing a smaller version of the desktop. In the navigation area, visual brushes are used to complete the task. Everything is fine if the desktop elements are more or less static. But if elements change frequently (because they contain animation, for example), VisualBrushes goes into the wild. They will be updated along with the frame rate of the animation. Of course, lowering the frame rate helps, but I'm looking for a more general solution to this problem. Although the “source” control only displays a small area subject to animation, the visual container of the brush is fully rendered, causing the application's performance to go to hell. I already tried using BitmapCacheBrush. Unfortunately, it doesn’t help. The animation is inside the control. So the brush needs to be updated anyway.

Possible solution: I created a control more or less similar to VisualBrush. This requires some visual (like VisualBrush), but uses DiapatcherTimer and RenderTargetBitmap to do the job. Right now, I am subscribing to the LayoutUpdated event of the control, and whenever it changes, it will be assigned to "render" (using RenderTargetBitmap). The actual rendering is then triggered by DispatcherTimer. Thus, the control will redraw as much as possible at the DispatcherTimer frequency.

Here is the code:

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 } 

It works very well so far. The only problem is the trigger. When I use LayoutUpdated, rendering starts constantly, even if the visual itself does not change at all (possibly due to animation in other parts of the application or something else). LayoutUpdated just starts often. In fact, I could just skip the trigger and just update the control with a timer without any trigger. It does not matter. I also tried to override OnRender in Visual and create a custom event to trigger the update. It does not work either because OnRender is not called when something deep inside VisualTree changes. This is my best shot right now. It works much better than the original VisualBrush solution already (at least in terms of performance). But I'm still looking for an even better solution.

Does anyone have an idea how to a) initiate an update only when nessasarry or b) perform a completely differentiated approach?

Thanks!!!

+6
source share
2 answers

I tracked the visual status of controls using WPF internals through reflection. Thus, the code I wrote catches the CompositionTarget.Rendering event, scans the tree and looks for any changes in the subtree. I wrote this to intercept the data that was transferred to MilCore, and then use it for my own purposes, so take this code as a hack and nothing more. If that helps you, great. I used this on .NET 4.

First, the code for navigating to the tree reads the status flags:

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

Next, the supporting code for the Rendering event:

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

In any case, I was tracking the visual tree for updates, and I think you can control them using something like that if you want. These are far from best practices, but sometimes pragmatic code should be. Caution.

+4
source

I think your decision is already good. Instead of a timer, you can try to do this using the Dispatcher callback with ApplicationIdle priority, this will actually make the updates lazy, as this will only happen when the application is not busy. Also, as you said, you can try using BitmapCacheBrush instead of VisualBrush to draw your overall image and if that matters.

Regarding your question about WHEN to redraw the brush:

Basically, you want to know when something has changed so that your existing thumbnail is dirty.

I think you could either attack this problem in the backend / model, or have a dirty flag, or try to get it from the front.

The backend obviously depends on your application, so I cannot comment.

At the front end, the LayoutUpdated event seems correct, but as you say, it can fire more often than necessary.

Here's a shot in the dark - I don't know how LayoutUpdated works inside, so it might have the same problem as LayoutUpdated: You can override ArrangeOverride in the control you want to watch. Whenever you call ArrangeOverride, you start your own updated layout using the dispatcher so that it starts after the layout is complete. (maybe even wait a few milliseconds longer and do not queue more events if you need to call a new ArrangeOverride at this time). Since going through the layout always calls “Measure” and then “Arrange” and navigate the tree, this should cover any changes anywhere inside the control.

+1
source

Source: https://habr.com/ru/post/886529/


All Articles