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
Other Posts you might also like