Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Rendering a WPF Container to Bitmap


:P
On this page:

I'm playing around a little today with WPF again. One thing I like about WPF is the fact that you can take just about anything that you render on the canvas and dump it out as a bitmap file. Since it's fairly easy to create images and shapes in WPF I thought I'd create a quick form to created rounded corner bars, because one can never have enogh of those in Web applications <g>...

Oh ok, so I have tools that do that shit, but I wanted to see what it takes to do this with WPF. A few hours later I got it working, but there were a few hiccups along the way. Here's what the end result looks like:

The idea is you type in a width, height, radius and color and it will generate the appropriate rounded bar for you. It renders in real time as you type so you see immediately what it looks like.

Trivial, but actually kinda useful - there are a few similar combinations I'm thinking would be useful: Creating gradients (easier and more trivial), creating shadow images etc.

So the whole point of this exercise was to see how "easy" it'd be to do this because WPF provides the ability to take any rendered visual and lets render it to a bitmap. Even nicer, WPF properly respects transparency when rendering to disk, something that's a real bitch in GDI+ when needing to save to GIF images.

So, I thought how hard could this be? Well it turns out the concept is very simple, but due to some very unexpected behavior in WPF it too a long time and some help from a friend to figure it out.

The basic set up is this: The middle section of the form above is a Grid that lives inside of a DockPanel. The DockPanel handles the docking of the controls at the top and the save options on the bottom with the grid making up the fill of the rest of the window DockPanel content. So I figured all I'd have to do is stick a Grid inside of the of the fill container (ie. the parent grid) and then render a rectangle into it at the right size and I'm done.

There are some tricky issues to deal with in the shapes - like a Rectangle - aren't controls, but drawings so they don't act like a container. So the idea is to draw the outer container and fill the container with the rectangle. The rectangle then has a Radius X and Radius Y assigned to create the rounder corners.

My first shot looked like this:

        <Grid Name="renderContainer" Background="Azure" ClipToBounds="True">            
            <Grid Name="cvWrapper" Background="Transparent">                
                <Rectangle Name="rectMainShape">                   
                </Rectangle>                
            </Grid>
        </Grid>

with code that essentially does the following:

private void btnSave_Click(object sender, RoutedEventArgs e)
{
    int Height = (int)this.cvWrapper.ActualHeight;
    int Width = (int)this.cvWrapper.ActualWidth;
 
    RenderTargetBitmap bmp = new RenderTargetBitmap(Width, Height, 96, 96, PixelFormats.Pbgra32);
    bmp.Render(this.cvWrapper);
 
    string file = this.txtFilename.Text;
 
    string Extension = System.IO.Path.GetExtension(file).ToLower();
 
    BitmapEncoder encoder;            
    if (Extension == ".gif") 
        encoder = new GifBitmapEncoder();            
    else if (Extension == ".png")
        encoder = new PngBitmapEncoder();            
    else if (Extension == ".jpg")
        encoder = new JpegBitmapEncoder();            
    else
        return;
 
    encoder.Frames.Add(BitmapFrame.Create(bmp));
 
    using (Stream stm = File.Create(file))
    {
        encoder.Save(stm);
    }
}

This code is supposed to look at the above code and render it into a bitmap. The RenderBitmap object is used and it renders the target in system level pixels not WPF device independent pixels - important because if the bitmap is used anywhere but WPF you'll want standard fixed pixel sizes. RenderTargetBitmap handles the appropriate scaling to provide the correct size. The bitmap is then added to a Bitmap Encoder which in turn can write out the bitmap to a file. Simple enough (if you know what to do <s>).

But unfortunately this did not work. In fact, the output generated where the correct size I specified in the form, but the content was - well blank. After some more experimenting I found that the content actually wasn't blank but rather shifted off rather far to the bottom and right. It turns out that it was shifted exactly by the margins of the Parent container or in this case RenderContainer. It looks like WPF is rendering the parent container - but actually it's not. What's happening is that WPF is rendering the visual control I provided but it's applying any margins that are applied against it. The margins are implicit - I never declared them but they are nevertheless implied. So the background of the 'empty' image is transparent, not Azure which would have accounted for the parent container.

So after a lot of back and forth to figure this out (and some help from Mark Miller of DevExpress) I finally figured out that the margins where being applied and applied a simple solution: Wrap the inner Grid into another container without any margins. So the margins of the Grid, which by default centers all content (which is what I want so the image is always centered in the view) are the issue here. By wrapping the inside Grid in a Canvas of the same size the rendering started to work properly:

        <Grid Name="renderContainer" Background="Azure" ClipToBounds="True">            
            <Canvas Name="stOuter">
                <Canvas Name="cvWrapper" Background="Transparent" ClipToBounds="True">                
                    <Rectangle Name="rectMainShape">                   
                    </Rectangle>                
                </Canvas>
            </Canvas>
        </Grid>

The code now has to size the canvas, the inner grid and the rectangle all to the same size before rendering. Here's what the actual display Image code looks like:

public void DisplayImage()
{
    try
    {
        float Height = float.Parse(this.txtHeight.Text);
        float OriginalHeight = Height;
        float Width = float.Parse(this.txtWidth.Text);
        float Radius = float.Parse(this.txtRadius.Text);
 
        this.cvWrapper.Width = Width;
        this.cvWrapper.Height = Height;
        this.stOuter.Width = Width;
        this.stOuter.Height = Height;
 
        this.rectMainShape.RenderTransform = null;
 
        this.rectMainShape.ClipToBounds = false;
        if (this.radTop.IsChecked.Value || this.radBottom.IsChecked.Value)
        {
            Height += Radius;
            this.rectMainShape.ClipToBounds = true;
        }
 
        this.rectMainShape.Width = Width;
        this.rectMainShape.Height = Height;
 
        this.rectMainShape.RadiusX = Radius;
        this.rectMainShape.RadiusY = Radius;
 
        this.rectMainShape.Fill = new BrushConverter().ConvertFrom(this.txtColor.Text) as Brush;
 
        // *** for bottom shape we'll use TranslateTransform
        if (this.radBottom.IsChecked.Value)
            this.rectMainShape.RenderTransform = new TranslateTransform(0, Radius * -1);
    }
    catch
    {}
}

And this works correctly.

Transforms on the Rectangle

Another issue I ran into was related to the rendering of the rectangle with top and bottom corners only. Since I'm lazy I decided to just use a Rectangle for this because it already has all the logic to render rounded corners. So the idea is to render a rectangle with a Radius applied for rendering both top and bottom edges and simply clipping the rectangle for top and bottom corners.

This actually works fine for the top corners. All I do is make the size of the image larger than the display size and then clip the bottom off. Easy.

But I couldn't figure out a way to move rectangle to a negative offset AND clip the region. It seems that this should have worked:

if (this.radBottom.IsChecked.Value)
    this.rectMainShape.SetValue(Canvas.TopProperty, Radius * -1);

but it doesn't. Oddly if I explicitly add Canvas.Top="-15" into the XAML markup it DOES properly shift the offset to a negative position, but for some reason this behavior is not working in code any which way I tried.

In the end I did this with a RenderTransform using a TranslateTransform and ensuring that everything is clipped. TranslateTransforms can't be used with LayoutTransforms which seems a bit odd - that threw me for a loop for a while. In fact this solution came to me very late after I had fucked around with this for nearly an hour, trying everything from RotateTransforms to every possible container.

It is extremely difficult to get a consistent feel for WPF containers. Should I use a Grid or Canvas or StackPanel for these types of empty containers? Canvas is usually easiest to deal with if there's no content other than drawings. But it took a while to figure that out - I used Grid and Grid would NOT work with the RenderTransform and instead showed the transform at a new position not respecting the clip region. <sigh>

The tools are painful!

Well, live and learn. This was an interesting exercise and I learned quite a bit more about containers. But frankly I get the feeling I won't ever quite get WPF and be productive with it. It's just too strange of an environment and horribly complex. Yes it's flexible but how does one keep all these properties, and depency properties straight? Working with both Visual Studio and Blend side by side works but it's painfully slow. Blend looks nice but everything is 50 mouseclicks away. I found myself working mostly in the XAML code in Visual Studio to get shit done cutting and pasting XML code. Visual Studio is OK for XAML editing but man do I wish the designer could be turned off. You get split design, XAML view, except the designer is very, very slow and crashes frequently.You can open the document as XML but then you don't get Intellisense or the property sheet and I can't do XAML at this point without it.

There's a sore need for a *developer WPF tool*. Blend might work for designers who want to feed their cartoon egos, but it sucks if you need to layout a simple form.

Download Code

Posted in Visual Studio  WPF  

The Voices of Reason


 

Mike Wolf
September 10, 2007

# re: Rendering a WPF Container to Bitmap

As always rick yours posts are great...

not so much a comment on the post content, but on the wpf frustration as a whole

1) I agree the tools need some maturing. Why cant blend do intelesence, why is cider in orcas still poop... why cant blend deal w/ usercontrols so well.

But also , on a more general level, I have found if your code focused like myself, blend takes some getting used to , simply because code focused people tend to just crank it out... Think of html/css... i never use a design view in html, because after so long I felt I knew it in and out, and could always do it faster than any tool could.... the difference is xaml is not html... and is too damn verbose... long and short of this rant is... its partly the tools needing to mature and partly us code people needing to admit somethings are faster and more design accurate using the tools. Essentially global layout (especially canvas based), go w/ blend, tweaking / hacking (especially grids) go w/ orcas.

2) WPF is complicated and kind of confusing, but is it also that us as developers have become old dogs a bit? WPF is kind of a radical shift, inline w/ web dev, but still a radical shift w/ new tools, libs, concepts, events, life cycles etc.... is it that much more complicated or is it that we have had our minds in the web ui mindset for so long that adjusting is difficult?

my 2 cents

Josh Stodola
September 10, 2007

# re: Rendering a WPF Container to Bitmap

I agree that WPF is far too complex at this point. It is almost overwhelming for somebody who is just giving it a shot (me). I have thrown in the towel for now, eagerly awaiting that *developer WPF tool*....

Rick Strahl
September 10, 2007

# re: Rendering a WPF Container to Bitmap

@Mike - re: old dogs. It's possible. But I've been mucking with WPF for quite a while admittedly off and on because I've gotten very frustrated with how long even the simplest things take. It's true for example, if I look at the code and markup for the above form there's very, very little of it. It would have taken quite a bit more code to do the same with GDI+ for example. But there are so many different ways to accomplish things and the subleties are killing me. And if I look at the discussions on some of the WPF forums where the hardcore guys hang out and talk about picture tubes and color hues make me realize that this is a world I truely know nothing about.

The whole environment is set up for those types of people <s>. I'm not afraid of learning something new in fact I'd love to get a better grip on this, but frankly I don't even know where do I start? Game design? Graphics design? Design 101 at the local community college? <s>...

WPF right now feels kind of like Windows 1.0 where everybody's drooling and only a few people really understand how to make it work. It's like there needs to be a more high level framework on top of this base API to provide for the more mundane developer experience.

Compared to WPF learning Web development originally was a walk in the park...

mike wolf
September 10, 2007

# re: Rendering a WPF Container to Bitmap

true... but when we firstlearned web dev.... the web was a simpler place to learn. Not sure picking up all the frameworks and ajax etc would be as simple as it was me learing basic html + basic perl + basic vi..

just saying... web dev and desktop dev aint so easy to pick up anymore.... especially in this richer and richer world... but i do hear yah.

Rick Strahl
September 10, 2007

# re: Rendering a WPF Container to Bitmap

@Mike, I think the difference is you can still build a basic Web application pretty easily. IOW, the Web is incremental technology: you make it as complicated as you want it to be. WPF is in your face with the complexity right from the start, there's no way getting around it.

One general thought is this: WPF makes the complex things easy, and simple things hard... I suppose that's promising: At least it means that this can probably be fixed in the future with more abstraction.

Michael Stuart
September 11, 2007

# re: Rendering a WPF Container to Bitmap

Man, I feel your pain! I struggled with this same issue with RenderTargetBitmap about a week ago! It made no sense why my element would not seem to get rendered. After a lot of frustration, I figured it out as well. So far, I completely agree with you in your "The tools are painful!" part. I have been using Blend for the xaml, then using vs.net for the code (because the 2008 xaml editor crashes). Blend and xaml took awhile to get used to, even for making basic forms (I haven't even tried learning the animation tools in it yet). I end up hardly even using the design view, and manually typing my xaml...I might as well be using Notepad. If it only had intellisense in the xaml view, it would be so much easier to use. How hard could it have been to add that...it feels unfinished.

Joel Chretien
October 01, 2007

# re: Rendering a WPF Container to Bitmap

You can set only the top corners of the RoundedCorners by setting the property in terms of the four corners. "6,6,0,0" I believe that would set the top to rounded corners.

Rick Strahl
October 01, 2007

# re: Rendering a WPF Container to Bitmap

Sorry Joel? What do you mean? It'd be great if I could render a rectangle directly with the rounded corners applied only to top and bottom without having to clip.

Joel Chretien
October 05, 2007

# re: Rendering a WPF Container to Bitmap

When you add a border, you can specify the corners individually.

<Border BorderThickness="1"
            BorderBrush="Black"
            CornerRadius="20, 20,0,0" />


This specifies the top has a corner radius, but the bottom doesn't.

Eric Newhuis
October 19, 2007

# re: Rendering a WPF Container to Bitmap

I tried your approach with Visuals built from straight C# and still had trouble.

I discovered that the following works without screwing around with clipping et al.

Canvas wrapper = new Canvas ();

Ellipse shape = new Ellipse {
Width = 90,
Height = 90,
Fill = new SolidColorBrush (Colors.Beige),
Stroke = new SolidColorBrush (Colors.Purple)
};

wrapper.Children.Add (shape);
wrapper.Arrange (new Rect (0, 0, 90, 90));

RenderTargetBitmap bitmap = new RenderTargetBitmap ((int)wrapper.ActualWidth, (int)wrapper.ActualHeight, 96, 96, PixelFormats.Pbgra32);
bitmap.Render (wrapper);

PngBitmapEncoder encoder = new PngBitmapEncoder ();
encoder.Frames.Add (BitmapFrame.Create (bitmap));

using (Stream stream = File.Create ("ellipse.png")) {
encoder.Save (stream);
}

Chris Cavanagh
February 17, 2008

# re: Rendering a WPF Container to Bitmap

Hi Rick,

Great post! - I stumbled on it 'late' after I'd already implemented something similar (http://chriscavanagh.wordpress.com/2008/02/15/wpf-skinning-your-websites/).

Any ideas how I can get the absolute position of a visual in a RenderTargetBitmap? (never rendered to the screen). I could use PointToScreen, but my elements don't have a PresentationSource attached; maybe I could create and attach a custom one... Basically I want to grab a visual and use its position as an alternative means of specifying the snapshot bounds.

Dan Vestergaard
October 05, 2008

# re: Rendering a WPF Container to Bitmap

Hi Rick.

Updating the layout works fine for me:

RenderTargetBitmap bmp = new RenderTargetBitmap(Width, Height, 96, 96, PixelFormats.Pbgra32);

Size visualSize = new Size(this.cvWrapper.ActualWidth, this.cvWrapper.ActualHeight);

this.cvWrapper.Measure(visualSize);
this.cvWrapper.Arrange(new Rect(visualSize));

bmp.Render(this.cvWrapper);

Chris Curry
December 15, 2008

# re: Rendering a WPF Container to Bitmap

THANKS a lot, I was trying to capture a thumbnail from a media element within my custom control and wondered what was going on. This helped me alot.

However instead of wrapping it in a canvas I ended up just using Translate point to create a bigger bitmap to accomodate the offset and then crop it using a CroppedBitMap. Like this:

 //translate point to get the position of the media element relative to the parent control
 var p = myMedia.TranslatePoint(new Point(0, 0), this);

RenderTargetBitmap rtbmp = new RenderTargetBitmap((int)(myMedia.ActualWidth + p.X) , (int)(myMedia.ActualHeight + p.Y) , 96, 96, System.Windows.Media.PixelFormats.Pbgra32);

rtbmp.Render(myMedia);

CroppedBitmap cbm = new CroppedBitmap(rtbmp, new Int32Rect((int)p.X,(int)p.Y,(int)myMedia.ActualWidth,(int)myMedia.ActualHeight));

C Coffey
January 28, 2009

# re: Rendering a WPF Container to Bitmap

Anyone tried this for a hi-res bitmap, say 600 dpi (for printing)? I'm seeing serious memory issues when creating several bitmaps of this size and displaying them in a DocumentViewer.

double resolutionScale = 600 / 96;

RenderTargetBitmap bitmap = new RenderTargetBitmap((int)(resolutionScale * (r.Width + 1)),
(int)(resolutionScale * (r.Height + 1)),
resolutionScale * 96,
resolutionScale * 96, PixelFormats.Default);

Bac Nong Tien
April 22, 2009

# re: Rendering a WPF Container to Bitmap

Hi, I wanna ask you one question: Could we do this in Silverlight 2? Thanks and regards!~!!!

Andreas
June 05, 2009

# re: Rendering a WPF Container to Bitmap

Hi,

another way to render the rectangle at the right position would be using a VisualBrush
        private void btnSave_Click(object sender, RoutedEventArgs e)
        {
            int height = (int)this.cvWrapper.ActualHeight;
            int width = (int)this.cvWrapper.ActualWidth;
            DrawingVisual dv = new DrawingVisual();
            using (DrawingContext ctx = dv.RenderOpen())
            {
                VisualBrush br = new VisualBrush();
                br.AutoLayoutContent = true;
                br.Visual = cvWrapper;
                ctx.DrawRectangle(br, null, new Rect(0, 0, width, height));
            }
            RenderTargetBitmap bmp = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
            bmp.Render(dv);

            string file = "d:\\ temp\\ file.png";
            string extension = System.IO.Path.GetExtension(file).ToLower();

            BitmapEncoder encoder;
            if (extension == ".gif")
                encoder = new GifBitmapEncoder();
            else if (extension == ".png")
                encoder = new PngBitmapEncoder();
            else if (extension == ".jpg")
                encoder = new JpegBitmapEncoder();
            else
                return;

            encoder.Frames.Add(BitmapFrame.Create(bmp));
            using (Stream stm = File.Create(file))
            {
                encoder.Save(stm);
            }
        }

Nick
August 03, 2009

# re: Rendering a WPF Container to Bitmap

This is 2 years too late, but you can turn off the design view. There is a little button between the two views that says "Collapse Pane". This will show only one of the two views (XAML or design). You can then use the tabs at the bottom of the screen to switch between the views.

madhav
October 12, 2009

# re: Rendering a WPF Container to Bitmap

Hi Rick,

Adding Tiff Images to DrawingGroup and using RenderTargetBitmap method to save the DrawingGroup image collection results in loss of Actual Pixelformat which cmyk.
as below:
RenderTargetBitmap rtb = new RenderTargetBitmap((int)ImgSource.Width, (int)ImgSource.Height, 96, 96, PixelFormats.Pbgra32 );

Evenif we reformat the pixel as:

FormatConvertedBitmap fcb = new FormatConvertedBitmap();
fcb.BeginInit();
fcb.Source = cachedSource;
fcb.DestinationFormat = PixelFormats.Cmyk32 ;
fcb.EndInit();

the actual pixel ration will be lost do you have any suggesion resolve issue.
Or do we have any similar objects like DrawingGroup to arrange images and Save it with exact pixelformat ?


...madhav

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024