Title: Save WPF control images in C#
Occasionally I need to save WPF control images with transparent backgrounds for one reason or another. This time I needed to make some icons that displayed numbers. I could have used a Windows Forms application (it would have been a lot easier) but I wanted to take advantage of some WPF features. Besides, I knew I would eventually need to save WPF images at other times.
This example lets you enter a width and height, font size, and text and makes an image of the text at the desired size. When you click the Capture button, the program saves the result in a PNG file.
The following code shows the most interesting part of the program's XAML code.
<Grid Margin="12,127,0,0" Name="grdText"
HorizontalAlignment="Left"
Width="128" Height="128" VerticalAlignment="Top">
<Rectangle Margin="0" RadiusX="20" RadiusY="20"
Stroke="LightGreen">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="Green" Offset="1" />
<GradientStop Color="LightGreen" Offset="0" />
</LinearGradientBrush>
</Rectangle.Fill>
<Rectangle.BitmapEffect>
<BitmapEffectGroup>
<BevelBitmapEffect EdgeProfile="BulgedUp" />
</BitmapEffectGroup>
</Rectangle.BitmapEffect>
</Rectangle>
<TextBlock Foreground="LightGreen" Margin="0"
VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock.BitmapEffect>
<BevelBitmapEffect />
</TextBlock.BitmapEffect>
<Run Name="runText"
FontFamily="Arial Rounded MT" FontStyle="Normal"
FontSize="80">1</Run>
</TextBlock>
</Grid>
After the preliminaries, the code defines the labels and text boxes where you enter values. It then defines the Capture button. It's not obvious from this code unless you really know your XAML but the text boxes resize when the window gets wider or narrower. The button also sticks to the right side of the window.
The more functional part of the interface begins with the Grid named grdText. That control contains a Rectangle with rounded corners and that is filled with a linear gradient brush with the "beveled bulged up" bitmap effect.
The Grid also contains a TextBlock with the beveled bitmap effect. This control contains a Run that determines the text displayed.
Both the Rectangle and the TextBlock fill the Grid so the program only needs to resize the Grid to make the others resize, too.
When you enter new values in the text boxes, the corresponding TextChanged event handlers adjust the display. For example, the following code shows how the program responds when you enter a new width.
// Update the rectangle's width.
private void txtWidth_TextChanged(object sender,
TextChangedEventArgs e)
{
try
{
if (grdText != null)
grdText.Width = double.Parse(txtWidth.Text);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
This code checks to see if the grdText grid has been created yet. If it has, the program parses the width you entered and sets the grid's width to that amount. (The Rectangle and TextBlock inside the Grid automatically resize to match.)
The following code shows how the program responds when you change the font size.
// Change the font size.
private void txtFontSize_TextChanged(object sender,
TextChangedEventArgs e)
{
try
{
if (runText != null)
runText.FontSize = double.Parse(txtFontSize.Text);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
The key here is that the code sets the FontSize property of the runText Run object defined by the XAML. (That's why the XAML gave the Run object a name.)
So far that's all fairly straightforward. Unfortunately saving an image of the Grid when you click the Capture button is a bit trickier. (This is one of my problems with WPF. This is the sort of operation that has been used for many years in Windows Forms programming. In early versions of .NET this was harder but over the years tools were added to make it easier. Then WPF started all over again, ignoring the lessons we learned over many years of .NET programming.)
Anyway, the WPF code that captures the image is fairly confusing and includes one really weird issue that confuses a lot of people. That issue is that a straightforward attempt to render the control's image will position the image relative to the parent's origin. For example, suppose the control is located at position (100, 200) relative to its parent's origin. Then if you just try to render the control, the image is shifted 100 pixels right and 200 pixels down in the result.
If the control is big enough, then the bitmap where you're trying to render it may be big enough to overlap the area where the control is drawn, but often people simply get a black bitmap and they wonder what went wrong. What happened was the control was rendered but it was shifted so far that it didn't appear on the bitmap.
There are a couple of ways you can work around this. One is to move the control to its parent's origin, render its image, and then move the control back.
A simpler approach (if you can apply the word "simpler" to anything dealing with WPF and rendering) is to create a VisualBrush that contains an image of the control. Then you can fill the target bitmap with that brush so it displays the control's image. This example takes that approach.
A second annoying issue is that the control renders itself as it appears on the screen. If it is covered by another control, that control appears in the rendering. If the control is chopped off because it sticks out past the edge of the window, then its rendering is also chopped off.
The following code executes when you click the Capture button.
// Save the image.
private void btnCapture_Click(object sender, RoutedEventArgs e)
{
try
{
// Make sure the window is big enough.
this.SizeToContent = SizeToContent.WidthAndHeight;
// Save the file.
string filename = txtText.Text + ".png";
SaveControlImage(grdText, filename);
MessageBox.Show("Done");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
This code first sets the window's SizeToContent property to WidthAndHeight to ensure that the entire control is visible. It then calls the following SaveControlImage method to do the real work.
// Save a control's image.
private void SaveControlImage(FrameworkElement control,
string filename)
{
// Get the size of the Visual and its descendants.
Rect rect = VisualTreeHelper.GetDescendantBounds(control);
// Make a DrawingVisual to make a screen
// representation of the control.
DrawingVisual dv = new DrawingVisual();
// Fill a rectangle the same size as the control
// with a brush containing images of the control.
using (DrawingContext ctx = dv.RenderOpen())
{
VisualBrush brush = new VisualBrush(control);
ctx.DrawRectangle(brush, null, new Rect(rect.Size));
}
// Make a bitmap and draw on it.
int width = (int)control.ActualWidth;
int height = (int)control.ActualHeight;
RenderTargetBitmap rtb = new RenderTargetBitmap(
width, height, 96, 96, PixelFormats.Pbgra32);
rtb.Render(dv);
// Make a PNG encoder.
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(rtb));
// Save the file.
using (FileStream fs = new FileStream(filename,
FileMode.Create, FileAccess.Write, FileShare.None))
{
encoder.Save(fs);
}
}
This method first uses VisualTreeHelper.GetDescendantBounds to get the area covered by the control and its descendants. It then makes a DrawingVisual to represent a pixel-oriented graphical object (in this case it will be a bitmap).
The code then creates a DrawingContext associated with the DrawingVisual. It makes a VisualBrush that contains an image of the control. It then uses the brush to fill a rectangle the same size as the control on the DrawingContext.
Next the code makes a RenderTargetBitmap sized to fit the control's actual size. You can use the size of rect but this seems to produce a better result. The code then makes the DrawingVisual render itself onto the bitmap.
At this point the method has a RenderTargetBitmap holding the control's image and it just needs to save the bitmap into the file. To do that the code creates a PngBitmapEncoder and adds the bitmap to its Frames list.
Finally the method writes the encoder into a file stream to create the PNG file.
Simple, isn't it?
You could make lots of improvements to this program. For example, you could add more text boxes to let the user specify the colors and corner radius used by the Rectangle, the font name used by the Run, the bevel width used by the Rectangle's BitmapEffect, and so forth. However, if you only need to do this occasionally, you can just change the XAML code.
Download the example to experiment with it and to see additional details.
|