question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Reliable way of exporting complex UIElements to a bitmap

See original GitHub issue

One topic that has come up in the previous years a lot from customers of our WPF-based library is that image exports can be unreliable when using XAML-based controls inside. The exhibited problems vary between:

  • missing visuals
  • misplaced visuals
  • bindings not applied
  • triggers not run
  • values from converters not applied

Last year I’ve fixed those, or so I thought, by changing how we actually export our images. The previous code path looked basically as follows:

// Export visual
var visual = ourControl.ExportEverythingAsVisual();
// Place in container
// container is a custom class, but basically a UIElement that can contain both Visuals and other UIElements
// and passes Measure/Arrange calls to its children, as well as implement our own layout logic, which uses a custom AttachedProperty on our control
container.Add(visual);
// Perform a single layout pass (or so we thought)
container.Measure(new Size(imageWidth, imageHeight));
container.Arrange(new Rect(0, 0, imageWidth, imageHeight));
// write to RenderTargetBitmap
bitmap.Render(container);
// encode and save to file; not relevant here, I guess

Upon reading the ContextLayoutManager source code a bit I noticed that there are some things that won’t ever work correctly with explicit calls to Measure/Arrange since they defer part of the work into a second layout pass, which only the actual layout manager can resolve and perform. I think things like event handlers on SizeChanged can cause this, or SharedSizeGroups for grids.

So I then set out to try to fix our export with the knowledge I had at that time. I even finally had a test case by a customer that was simple enough to unit-test to verify that things worked correctly after the fix, whereas they were broken before. The new code basically looked as follows:

// Perform a single layout pass - doesn't help much, but shouldn't hurt much either, except performance
container.Measure(new Size(imageWidth, imageHeight));
container.Arrange(new Rect(0, 0, imageWidth, imageHeight));
// Wait for WPF's layout to finish by scheduling at a priority _after_ layout
ourControl.Dispatcher.Invoke(DispatcherPriority.Loaded, new Action(() => {
    // write to RenderTargetBitmap
    bitmap.Render(container);
    // encode and save to file; not relevant here, I guess
}));

Turns out, this works most of the time, but apparently not always. One customer performed image exports of different diagrams in a loop and found random elements to be missing from the export. From reading the source code I remembered that the layout manager may defer remaining elements to a later frame if layout takes too long. What I didn’t realize at the time was that those deferred updated are posted at Background priority, so waiting for Loaded will get all updates from the first 153 elements + 153×2 ms, but would not get the results of the later layouts that are happening at a lower priority.

My current guess would be that things might work when changing Loaded above to ContextIdle. But it still nags at me that I cannot figure out the »correct« way of exporting UIElements so that all layout has happened at export time. VisualBrush somehow manages to wait for all that before painting and so do printing or XPS export, but those have the benefit of being able to call internal APIs.

Is there an official correct way of waiting for layout completion before rendering to a bitmap? Am I on a completely dangerous and wrong path? Almost correct but missing an important detail?

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:4
  • Comments:7 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
SamBentcommented, Oct 29, 2020

Try adding the grid to a convenient place in the visual tree, but setting Visibility=“Hidden” to keep it from displaying. (Convenient means some place where its layout space won’t affect anything else.)

If your container isn’t getting added to the measure queue, it won’t get all the benefits of the real layout algorithm. For example, it will see the Measure you call explicitly, but not any subsequent re-measures that may arise due to changes in its subtree (such as a binding providing a new value that changes the size of a TextBlock).

1reaction
SamBentcommented, Oct 26, 2020

First, don’t call Measure/Arrange directly; instead just call container.InvalidateMeasure() (after ensuring its Height and Width are set to the desired values). This marks your container as “dirty” and causes it to participate in the next layout pass.

Second, there is no guaranteed way to tell when layout is complete. Each layout pass calls Measure and Arrange on a set of dirty elements, then raises events about the changes that happened. Any of these calls can introduce new elements or make existing elements dirty, requiring another layout pass. The “move layout to Background” logic you mentioned is WPF’s way of coping with this potentially never-ending process; it keeps complicated layouts from starving the render thread or the reaction to user input.

In practice, layouts are rarely that complicated; waiting for Loaded priority usually works, and ContextIdle should work except in cases that explicitly ask for infinite layout (which, like infinite loops in programs, are impossible to detect in all cases). Another alternative is to listen for the LayoutUpdated event, which is raised near the end of each layout pass.

Third, you can query the IsMeasureValid and IsArrangeValid properties on your container to see whether its layout is current. If not, simply call BeginInvoke to try again later.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Any way to save an UIElement created programmatically in ...
I'm using the class RenderTargetBitmap using the method RenderAsync() but it only works with UI elements created in the XAML code.
Read more >
9 slice scaling of bitmaps - Adobe XD Feedback - UserVoice
Without 9-slice scaling, there's no way to properly scale a png graphic within XD. I have to create all the different sizes I...
Read more >
How to export images from TouchDesigner like a Pro
This is perhaps the most common method for image exporting within TouchDesigner. It consists of using the Movie File Out TOP and its...
Read more >
A month with Sketch 3
What it basically means is that something created once will be directly exportable in any given DPIs without the need for the designer...
Read more >
What is the best way to render 1000000 bricks? (Is there a ...
I've tried the following. 1) Game object for each brick (lags out at about 5,000 game objects). 2) All bricks combined in one...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found