Skip to main content

Using WPF to Render Bitmaps

There are a few posts around the net about this, but a lot of them focus on taking XAML controls or pages, and turning parts of them into bitmaps.

This is fine, but I want to be able to use WPF entirely off-screen in order to create rich graphical content.

What I'm going to present here is a way of doing just that, in a console application just to press home the point.

This is the image that you're going to produce - it's a little bit, well, crap - but it does the job.

wpfinabitmap

So, it's an Ellipse with a RadialGradientFill (off-centre, white-to-blue gradient stops), and a TextBlock in Arial Black/Bold font of around 40px size.

The edges around the ellipse are going to be output as transparent (open up this file in Photoshop or Paint.Net and notice the checkerboard patterns around the edge - useful for web images!

Step 1: Create the project

Open a new Visual Studio, and create a new Console Application Project.  Make sure it targets .Net 3.5.

Add references to:

  • PresentationCore
  • PresentationFramework
  • WindowsBase

These are the core WPF libraries that are going to be required.

Warning - if you're going to be repeating this code in a Windows Forms application, you should be careful - because many of the classes in WPF clash with those in the standard windows forms framework.

Step 2: Basic setup

Now, a quick google or MSDN search will show that the main class you're going to need is called RenderTargetBitmap.  This class has a method called Render, to which you pass a Visual.  Anything that can be displayed in a WPF window is a visual (in fact the window itself is) which means, in theory, that anything you can do in WPF to screen can be done to a bitmap.

To produce the image file you're going to use one of the BitmapEncoder-implementing classes (there are classes for creating PNGs, Gifs, JPGs and more).

Open up Program.cs file and add the following usings:

using System.Windows.Media.Imaging;

using System.Windows.Media;

using System.Windows.Controls;

using System.Windows.Shapes;

using System.Windows;

using System.IO;

Modify the Main() method in Program.cs so it looks like this:

  1: [STAThread]

  2: static void Main(string[] args)

  3: {

  4: 	int height, width;

  5: 

  6: 	height = 300;

  7: 	width = 400;

  8: 

  9: 	//going to place all our stuff in here - same as on a standard WPF form.

 10: 	Grid mainContainer = new Grid();

 11: 	mainContainer.HorizontalAlignment = HorizontalAlignment.Stretch;

 12: 	mainContainer.VerticalAlignment = VerticalAlignment.Stretch;

 13: 

 14: 	Ellipse e = new Ellipse();

 15: 	e.Stroke = new SolidColorBrush(Color.FromArgb(255, 0, 0, 255));

 16: 	//setup an off-centre gradient fill for a 3D effect

 17: 	//Of course - we could instead use a Viewport3D with Visual3D elements :)

 18: 	RadialGradientBrush rFill = new RadialGradientBrush();

 19: 	GradientStopCollection gradientStops = new GradientStopCollection(new GradientStop[] { 

 20: 		new GradientStop(Color.FromArgb(255, 255, 255, 255), 0.0),

 21: 		new GradientStop(Color.FromArgb(255, 60, 90, 255), 1.0)

 22: 	});

 23: 	rFill.GradientStops = gradientStops;

 24: 	rFill.GradientOrigin = new Point(0.65, 0.25);

 25: 	rFill.Center = new Point(0.5,0.5);

 26: 	rFill.RadiusX = 0.5;

 27: 	rFill.RadiusY = 0.5;

 28: 	e.Fill = rFill;

 29: 

 30: 	e.HorizontalAlignment = HorizontalAlignment.Stretch;

 31: 	e.VerticalAlignment = VerticalAlignment.Stretch;

 32: 

 33: 	mainContainer.Children.Add(e);

 34: 

 35: 	TextBlock message = new TextBlock();

 36: 	message.Text = "WPF In a Bitmap!";

 37: 	message.HorizontalAlignment = HorizontalAlignment.Stretch;

 38: 	message.TextAlignment = TextAlignment.Center;

 39: 	message.VerticalAlignment = VerticalAlignment.Stretch;

 40: 	//use margin for placement

 41: 	//could use canvas instead - but don't like the static 'Canvas.SetLeft/SetTop'

 42: 	//methods.

 43: 	message.FontFamily = new FontFamily("Arial Black");

 44:    	message.FontSize = 40.0;

 45: 	message.FontWeight = FontWeights.Bold;

 46: 	message.Margin = new Thickness(0.0, 120, 0.0, 0.0);

 47: 	mainContainer.Children.Add(message);

 48: 

 49: 	PngBitmapEncoder encoder = new PngBitmapEncoder();

 50: 	RenderTargetBitmap render = new RenderTargetBitmap(

 51: 		width, 

 52: 		height, 

 53: 		96, 

 54: 		96, 

 55: 		PixelFormats.Pbgra32);

 56: 	render.Render(mainContainer);

 57: 	encoder.Frames.Add(BitmapFrame.Create(render));

 58: 	using (Stream s = File.Open("outputfile.png", FileMode.Create))

 59: 	{

 60: 		encoder.Save(s);

 61: 	}

 62: }

Notice a few things:


1) [STAThread] is applied to the Main method as WPF requires this.  If you omit it, an exception is raised telling you as much.


2) Transparency is enabled in the output image by using a pixel format that includes an alpha channel.  Line 55 specifies 32-bit rgba - same as most people's desktop.


3) Horizontal/Vertical Alignment being set to stretch in most places - has the same effect as it would on a XAML form.


Run the app and open up outputfile.png in the output folder.


Oops...


What's that?  The image is empty?


What's happened is that, although the grid, ellipse and textblock are all present and, in theory, ready to be rendered, they haven't actually been told to lay themselves out according to their visual container.  Of course, they don't actually have one, since they are just objects in memory, so what do we do?


We have to take the place of the layout engine that is usually wired up automatically by a real WPF form; which it achieves through the use of all the events that WPF elements expose (i.e. for when controls get added or updated etc).


On line 48 (or at least between 47 and 49) you need to add this line of code:

mainContainer.Arrange(new Rect(0, 0, width, height));

Thankfully, this is recursive.


Save and run again, and this time the image should be as above.


Animation


If you're going to try and create pre-rendered animations using WPF, there are a couple of gotchas - and one outright brick wall if animated gifs are your thing.


The brick wall is that GifBitmapEncoder is not capable of producing correct animatable gifs.  While you can successively call the AddFrame method of it's Frames collection property, and create a single gif file containing all the individual frames, you can't (out of the box) add the necessary timing information and looping information that most browsers/image viewers require.  You'll get a non-looping default-speed animation in IE7, for example, but in other browsers (apparently IE8 is one of them) it just won't animate at all.


It is possible to add this functionality into the Gif encoder - but it's not a job I want to take on (see this MSDN forum post and it's answer).  In the meantime, it's still possible to use .Net and WPF to generate the intermediate gif frames on disk, and then either manually stitch the animation together with timing in a dedicated tool.


The gotchas for animating in this way are:



  1. Once a BitmapEncoder has saved a file, it cannot be used to save another; so you'll need to create a new encoder for each frame.
  2. You can re-use the RenderBitmapTarget between each frame, but...
  3. In the same way that we had to manually call the 'Arrange' method of the Grid (or whichever root container we're going to be using), we also have to manually Invalidate any controls whose visual state changes between frames.  You can achieve this, by sticking this line before the next call to the Render(Visual) method of the RenderBitmapTarget object:
mainContainer.InvalidateVisual();

Now, in theory it might be possible to use the story-boarding animation features of WPF, but since they rely on animation timers (which will not be present since there's no actual WPF app or window here) I'm not sure how effective it'll be.  My guess is that you would manually visit the timeline, explicitly setting the current position from 0 -> 1, then invalidate anything that might be affected, and then do the Invalidate/Render code.


If that were possible, then it would certainly make animating rotations etc a lot easier.


In any case - the ability to render stills using WPFs frankly rather swish rasterisation layer is, in itself, a real boon if looking to create dynamic images on a web page, for example, or even for any other scenario.  Automatic anti-aliasing and transparency, for example, being two of the biggest boons.

Comments

Popular posts from this blog

Asp.Net 2 and 4 default application pool generates CS0016 IIS7.5

Before I start – if you’ve found a bunch of other articles about this around the net, tried the fixes that are mentioned and still not getting any joy – then read on – you might find this solves your problem. Earlier today I discovered that when I run any ASP.Net 2 or 4 application through IIS7.5 using the default application pools (which use ApplicationPoolIdentity) on Windows 2008 R2 x64 I get an error message similar to this: Server Error in '/MvcApplication31' Application. Compilation ErrorDescription: An error occurred during the compilation of a resource required to service this request. Please review the following specific error details and modify your source code appropriately.
Compiler Error Message: CS0016: Could not write to output file 'c:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\mvcapplication31\222b4fe6\4e80a86\App_global.asax.clb4bsnc.dll' -- 'The directory name is invalid. '
Source Error:[No relevant source lines]
Sou…

Serializing to attributes in WCF with DataContractSerializer

It’s a common problem – you want to return an object from a WCF service as XML, but you either want, or need, to deliver some or all of the property values as XML Attributes instead of XML Elements; but you can’t because the DataContractSerializer doesn’t support attributes (you’re most likely to have seen this StackOverflow QA if you’ve done a web search).  Most likely you’ve then migrated all your WCF service code to using the XmlSerializer (with all the XmlElement/XmlAttribute/XmlType attributes et al) – and you’ve cursed loudly.Well, I’m here to rescue you, because it is possible – and the answer to the problem is actually inferred from the MSDN article entitled ‘Types supported by the Data Contract Serializer’.The example I’m going to give is purely for illustration purposes only.  I don’t have a lot of time, so work with me!Create a new Asp.Net WCF service application, you can use Cassini as your web server (probably easier – otherwise you might have to enable Asp.Net compatibil…

Adding ‘Deny’ functionality to AuthorizeAttribute in Asp.Net Web API

For the web service project I’m working on at the moment I need to be able to treat authorization differently based on the hostname of the URL that requests are made through.To state more clearly – these web services will have a ‘sandbox’ mode in addition to the real mode, and the mode a request will operate under is determined as part of the controller-selection phase early in the Web API request lifecycle.  So, say that my web services will be hosted on services.acme.com; the sandbox will simply be sandbox.services.acme.com.Please note – a discussion of how this is implemented is entirely outside the scope of this article; but I’ll just say that I’ve developed an in-house multi-tenancy layer for both MVC 4 and Web API that allows us to define ‘brands’ and, under those, you can then redefine content, controllers, and even the DI container that is used.These services are going to require caller-level authentication for most operations via SCRAM Authentication (RFC 5802), and as such m…