RenderTargetBitmap + Resource'd VisualBrush = incomplete image
Asked Answered
C

2

9

I've found a new twist on the "Visual to RenderTargetBitmap" question!

I'm rendering previews of WPF stuff for a designer. That means I need to take a WPF visual and render it to a bitmap without that visual ever being displayed. Got a nice little method to do it like to see it here it goes

private static BitmapSource CreateBitmapSource(FrameworkElement visual)
{
    Border b = new Border { Width = visual.Width, Height = visual.Height };
    b.BorderBrush = Brushes.Black;
    b.BorderThickness = new Thickness(1);
    b.Background = Brushes.White;
    b.Child = visual;

    b.Measure(new Size(b.Width, b.Height));
    b.Arrange(new Rect(b.DesiredSize));

    RenderTargetBitmap rtb = new RenderTargetBitmap(
                                (int)b.ActualWidth,
                                (int)b.ActualHeight,
                                96,
                                96,
                                PixelFormats.Pbgra32);

    // intermediate step here to ensure any VisualBrushes are rendered properly
    DrawingVisual dv = new DrawingVisual();
    using (var dc = dv.RenderOpen())
    {
        var vb = new VisualBrush(b);
        dc.DrawRectangle(vb, null, new Rect(new Point(), b.DesiredSize));
    }
    rtb.Render(dv);
    return rtb;
}

Works fine, except for one leeetle thing... if my FrameworkElement has a VisualBrush, that brush doesn't end up in the final rendered bitmap. Something like this:

<UserControl.Resources>
    <VisualBrush
        x:Key="LOLgo">
        <VisualBrush.Visual>
            <!-- blah blah -->
<Grid 
    Background="{StaticResource LOLgo}">
<!-- yadda yadda -->

Everything else renders to the bitmap, but that VisualBrush just won't show. The obvious google solutions have been attempted and have failed. Even the ones that specifically mention VisualBrushes missing from RTB'd bitmaps.

I have a sneaky suspicion this might be caused by the fact that its a Resource, and that lazy resource isn't being inlined. So a possible fix would be to, somehow(???), force resolution of all static resource references before rendering. But I have absolutely no idea how to do that.

Anybody have a fix for this?

Curve answered 17/5, 2010 at 17:24 Comment(0)
A
16

You have two problems:

  1. You didn't set a PresentationSource on your visual so Loaded events won't fire.
  2. You didn't flush the Dispatcher queue. Without flushing the Dispatcher queue, any functionality that uses Dispatcher callbacks won't work.

The immediate cause of your problem is failure to flush the Dispatcher queue, since VisualBrush uses it, but you will probably run into the PresentationSource problem before long so I would fix both of these.

Here is how I do it:

// Create the container
var container = new Border
{
  Child = contentVisual,
  Background = Brushes.White,
  BorderBrush = Brushes.Black,
  BorderThickness = new Thickness(1),
};

// Measure and arrange the container
container.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
container.Arrange(new Rect(container.DesiredSize));

// Temporarily add a PresentationSource if none exists
using(var temporaryPresentationSource = new HwndSource(new HwndSourceParameters()) { RootVisual = (VisualTreeHelper.GetParent(container)==null ? container : null) })
{
  // Flush the dispatcher queue
  Dispatcher.Invoke(DispatcherPriority.SystemIdle, new Action(() => { }));

  // Render to bitmap
  var rtb = new RenderTargetBitmap((int)b.ActualWidth, (int)b.ActualHeight, 96, 96, PixelFormats.Pbgra32);
  rtb.Render(container);

  return rtb;
}

FYI, StaticResource lookup is never delayed under any circumstances: It is processed the moment the XAML is loaded and immediately replaced with the value retrieved from the ResourceDictionary. The only way StaticResource could possibly be related is if it picked up the wrong resource because two resources had the same key. I just thought I should explain this -- it has nothing to do with your actual problem.

Away answered 18/6, 2010 at 22:12 Comment(7)
Well, I did read and try flushing the dispatcher, but that didn't help by itself. I'll try adding the presentation source, which I did not do. Thanks.Curve
Be sure you flush the dispatcher last, after all the other steps are done. Also I noticed you set the height and with of your border explicitly, whereas I measured with PositiveInfinity.Away
@ray yeah, I've been doing that for awhile then realized Measure needs only to know the max, rather than the actual. I've been doing it with +inf since I learned that. Will be trying this soon.Curve
wat this (VisualTreeHelper.GetParent(container)==null ? container : null) if the container is the root return the container otherwise return null? Why not return the root?Curve
My code is designed to work on both independent visuals and visuals that already are being presented by a Window. So in my case, if the visual has a parent it already has a PresentationSource. Only one is allowed. By passing null for RootVisual I was able to use a single code path for both cases with very little cost. The most general solution would be (PresentationSource.FromVisual(container)!=null ? null : FindVisualTreeRoot(container)) with a separate FindVisualTreeRoot method that uses VisualTreeHelper.GetParent to find the root, but my code suffices for most purposes.Away
Holy cow is it possible to give +10 to this answer? This, plus creating a fake Dispatcher, were the missing pieces for getting dynamic rendering of arbitrary WPF controls to a PNG stream on an IIS server.Alon
@Ray I'm trying to do 3D rendering in IIS 7/.NET 4.0. This approach is working for me for 2D, but if I try to send it a Viewport3D the Viewport3D doesn't render. Any ideas?Anglophobia
V
0

Well to inline it, you could just do something like this:

<Grid>
    <Grid.Background>
        <VisualBrush>
            <VisualBrush.Visual>
                <!-- blah blah -->
            </VisualBrush.Visual>
        </VisualBrush>
    </Grid.Background>
</Grid>

If that doesn't work, my guess would be that it must be something specific with the Visual instance you are using (and that will require further code to better diagnose).

Verjuice answered 17/5, 2010 at 18:23 Comment(1)
I don't want to have to explain to people "Hey, I know the thumbnail is incomplete. Why don't you go ahead and not use brush resources kthx."Curve

© 2022 - 2024 — McMap. All rights reserved.