Using the RenderTargetBitmap in Windows Store Apps with XAML and C#
Introduction
In Windows 8.1 the WinRT (Windows Runtime) contains the new RenderTargetBitmap-class. It allows you to render an arbitrary UIElement into a bitmap. This article describes in the first section how to use the RenderTargetBitmap in Windows 8.1. In the second section I want to show you a different sizing behavior of the RenderTargetBitmap-class in WinRT compared to WPFs RenderTargetBitmap-class. This could be an issue for your app. But fortunately this article comes up with a workaround so that you can achieve the same results in your Windows Store App as in WPF. :-) Ok, let's go!
How to use the RenderTargetBitmap in Windows 8.1
In Windows Store Apps, you can use the RenderTargetBitmap-class for two scenarios:
- use it as ImageSource (to display it with an Image-Element or to use it with an ImageBrush)
- use its pixels (e.g. to save the bitmap to disk or to share it with other apps)
In this section I want to show you the basics. Just the basics, as the MSDN-documentation contains fantastic information on this topic:
- RenderTargetBitmap-class (Scroll down on that site to read the informative Remarks )
- XAML render to bitmap sample (great sample that shows both scenarios: ImageSource and Pixels)
OK, let's look at a simple sample here and start with a UIElement we want to render
The UIElement to render
In the next sections we use the Grid defined in the snippet below as input for a RenderTargetBitmap. The Grid contains an Image-Element and a TextBlock. As the Grid neither defines any Row- nor ColumnDefinitions, the TextBlock is drawn on top of the Image-Element.
<Grid x:Name="elementToRender" Width="500" Height="500">
<Image Source="turtle.jpg" Stretch="UniformToFill"/>
<TextBlock Text="This text is on top of the image" FontSize="40" TextWrapping="Wrap" Margin="50"/>
</Grid>
The output in the application looks like this:
Notice that the Grid-element in the snippet above has the name "elementToRender", so we can access it in C# with that name. Now let's fill a RenderTargetBitmap with it.
Fill a RenderTargetBitmap with a UIElement
To fill a RenderTargetBitmap with a UIElement, you just call it's RenderAsync-method. Pass in the UIElement you want to render into the RenderTargetBitmap. In the example here we're using the Grid with the name "elementToRender":
var bitmap = new RenderTargetBitmap();
await bitmap.RenderAsync(elementToRender);
Ok, with those two lines you're rendering a UIElement into a RenderTargetBitmap. Now as mentioned at the beginning of this section, you can use this RenderTargetBitmap as an ImageSource or access its Pixels.
Use RenderTargetBitmap as ImageSource
As the RenderTargetBitmap-class directly inherits from ImageSource, you can just assign in e.g. to the Source-Property of any Image-Element defined in your UI. Let's assume you've an Image-Element named image:
<Image x:Name="image" Width="200" Height="200" .../>
Now you can fill that Image-Element by assigning your RenderTargetBitmap to its Source-Property. The snippet below shows a Click-Event Handler that does exactly this:
private async void ButtonRender_Click(object sender, RoutedEventArgs e)
{
var bitmap = new RenderTargetBitmap();
await bitmap.RenderAsync(elementToRender);
image.Source = bitmap;
}
Use RenderTargetBitmap's Pixels
If you want to access the pixels of a RenderTargetBitmap, just call its GetPixelsAsync-Method after you've rendered a UIElement into it via its RenderAsync-Method as shown before. The GetPixelsAsync-Method returns an IBuffer with the binary data of the bitmap. This IBuffer can be converted to a byte[] with BGRA8-format.
The code below shows how you could grab the byte[] from the RenderTargetBitmap. Notice that the ToArray-Method that is called on the IBuffer-instance is an extension method. To use it, you need a using-directive in your codefile for the System.Runtime.InteropServices.WindowsRuntime-namespace
var bitmap = new RenderTargetBitmap();
await bitmap.RenderAsync(elementToRender);
// Get the pixels
IBuffer pixelBuffer = await bitmap.GetPixelsAsync();
byte[] pixels = pixelBuffer.ToArray();
With the pixels available, you can do several things. E.g. you could save the contents of your RenderTargetBitmap to disk, or you could use the pixels as input for a WriteableBitmap and then modify that one, or you can share your bitmap with other apps using the Share-Contract. The snippet below shows you a small sample for a share-contract-implementation. In the DataRequested-Event Handler the pixels are grabbed, written to a InMemoryRandomAccessStream and then shared to other apps:
public sealed partial class MainPage : Page
{
...
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
DataTransferManager.GetForCurrentView().DataRequested+= MainPage_DataRequested;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
DataTransferManager.GetForCurrentView().DataRequested -= MainPage_DataRequested;
}
private async void MainPage_DataRequested(DataTransferManager sender, DataRequestedEventArgs args)
{
var deferral = args.Request.GetDeferral();
var bitmap = new RenderTargetBitmap();
await bitmap.RenderAsync(elementToRender);
// 1. Get the pixels
IBuffer pixelBuffer = await bitmap.GetPixelsAsync();
byte[] pixels = pixelBuffer.ToArray();
// 2. Write the pixels to a InMemoryRandomAccessStream
var stream = new InMemoryRandomAccessStream();
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, stream);
encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Straight, (uint)bitmap.PixelWidth, (uint)bitmap.PixelHeight, 96, 96,
pixels);
await encoder.FlushAsync();
stream.Seek(0);
// 3. Share it
args.Request.Data.SetBitmap(RandomAccessStreamReference.CreateFromStream(stream));
args.Request.Data.Properties.Description = "RenderTargetBitmap-Sample";
args.Request.Data.Properties.Title = "Wow, here the shared Bitmap :-)";
deferral.Complete();
}
...
}
Please note that calling GetPixelsAsync takes additional costs, as the pixels are copied out of the video memory. If you're only calling RenderAsync and use the RenderTargetBitmap as ImageSource, the pixels are staying in video memory and no copy is required behind the scenes.
Limitations
As mentioned earlier, you can render an arbitrary UIElement to a Bitmap by using the RenderTargetBitmap-class. This is not totally true. There are some limitations. E.g. the video content displayed by a MediaElement cannot be captured by a RenderTargetBitmap. Content that is in the Visual Tree but offscreen also cannot be captured etc. Please find a full list of limitations on MSDN here.
The Sizing Issue and a workaround
In this section you'll learn about an issue or maybe an unexpected behavior of the RenderTargetBitmap if you're used to WPFs RenderTargetBitmap-class. I first want to describe the issue and then show you a workaround.
The issue
When you render an element that has a child-element crossing its borders, your RenderTargetBitmap gets bigger. This is the behavior in Windows Store Apps. In WPF your RenderTargetBitmap will still have the same size as the element to render, and the child-element crossing the borders is just clipped. Let's look at an example.
Let’s say we’ve the following element to render with a transformed element inside:
<Grid x:Name="elementToRender" Height="100" Width="500" Background="Red">
<Rectangle Fill="Yellow" Width="100" Height="100">
<Rectangle.RenderTransform>
<TranslateTransform Y="50"/>
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
The displayed element looks like below. Notice that the yellow rectangle draws out of the bounds of the red Grid:
Now when rendering the red Grid with the name "elementToRender" in a Windows Store App, the RenderTargetBitmap has a size of 500x150. That means that the output is the one below. I've marked the additional area gray. It's 500x150, even if the "elementToRender" is explicitly defined 500x100:
If I render the same Grid in WPF, the RenderTargetBitmap has a size of 500x100 (exactly the Grids size) and the yellow rectangle is clipped. The output looks like this:
The thought workaround using the Clip-Property
To get the WPF-behavior in my Windows Store App, my first idea was to use the Clip-Property and just clip the element I want to render. The following snippet shows this with the Grid from the previous section:
<Grid x:Name="elementToRender" Height="100" Width="500" Background="Red">
<Grid.Clip>
<RectangleGeometry Rect="0 0 500 100"/>
</Grid.Clip>
<Rectangle Fill="Yellow" Width="100" Height="100">
<Rectangle.RenderTransform>
<TranslateTransform Y="50"/>
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
The Grid now looks like that on the screen. The yellow rectangle is clipped. That's exactly what I want to have in my RenderTargetBitmap.
But if you now render that Grid with a RenderTargetBitmap in a Windows Store App, the RenderTargetBitmap still doesn't have the Grid-size of 500x100. Instead the RenderTargetBitmap still has a size of 500x150 and the output is the one below. The additional area that is part of the RenderTargetBitmap is marked gray:
That means that the total size of the data rendered into the RenderTargetBitmap still seems to take the total height (including the bottom of the transformed rectangle) into account. So the content of the RenderTargetBitmap has exactly the same size as without clipping: 500x150 instead of the displayed and expected 500x100.
The real workaround
As you've seen in the previous section, the thought workaround using the Clip-Property didn't work. The RenderTargetBitmap is still bigger as the clipped element to render (the Grid). But the Clip-Property is the first step to a solution. The second step is to add an additional element to the visual tree that contains your clipped element. Then render that additional element, and you're fine. Let's walk through this.
Ok, the first step we already did in the previous section. We clipped our Grid by using its Clip-Property (defined in UIElement)
<Grid x:Name="elementToRender" Height="100" Width="500" Background="Red">
<Grid.Clip>
<RectangleGeometry Rect="0 0 500 100"/>
</Grid.Clip>
<Rectangle Fill="Yellow" Width="100" Height="100">
<Rectangle.RenderTransform>
<TranslateTransform Y="50"/>
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
The second step is to place an additional element in the Visual Tree. The snippet below shows this. I've wrapped the whole clipped Grid (oldElementToRender) inside of a new Grid. And that new Grid is now the new element to render.
<Grid x:Name="elementToRender" Height="100" Width="500">
<Grid x:Name="oldElementToRender" Height="100" Width="500" Background="Red">
<Grid.Clip>
<RectangleGeometry Rect="0 0 500 100"/>
</Grid.Clip>
<Rectangle Fill="Yellow" Width="100" Height="100">
<Rectangle.RenderTransform>
<TranslateTransform Y="50"/>
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
</Grid>
If you render the new/outer Grid, the RenderTargetBitmap will now have a size of 500x100, and nomore 500x150. Yes, that's it. Now you're fine and you're able to produce the same output as in WPF. The content of the RenderTargetBitmap now looks like this:
Please find a similar scenario for this workaround on MSDN-Forums in my answer to this post.
I hope this article helps you to work with RenderTargetBitmap in Windows Store Apps.
Keep on codin', :-)
Thomas
if you want, follow me on twitter: @thomasclaudiush
or just visit my homepage: http://www.thomasclaudiushuber.com