views:

243

answers:

2

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?

A: 

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).

Charlie
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."
Will
+2  A: 

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.

Ray Burns
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.
Will
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.
Ray Burns
@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.
Will
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?
Will
Worked. Thanks much!
Will
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.
Ray Burns