Drawing conclusions

When we have a requirement to draw shapes in our UI, such as in our callout window example in Chapter 8, Creating Visually Appealing User Interfaces, we tend to use the abstract Shape class or, more accurately, one or more of its derived classes.

The Shape class extends the FrameworkElement class, so it can make use of the layout system, be styled, have access to a range of stroke and fill properties, and its properties can be data bound and animated. This makes it easy to use and, generally, the preferred method of drawing in WPF applications.

However, WPF also provides lower-level classes that can achieve the same end results, but more efficiently. The five classes that extend the abstract Drawing class have a much smaller inheritance hierarchy and, as such, have a much smaller memory footprint than their Shape object-based counterparts.

The two most commonly used classes include the GeometryDrawing class, which is used to draw geometrical shapes, and the DrawingGroup class, which is used to combine multiple drawing objects into a single composite drawing.

Additionally, the Drawing class is also extended by the GlyphRunDrawing class, which renders text; the ImageDrawing class, which displays images; and the VideoDrawing class, which enables us to play video files. As the Drawing class extends the Freezable class, further efficiency savings can be made by freezing its instances, that is, if they do not need to be modified afterward.

There is one other, and potentially even more efficient, method of drawing shapes in WPF. The DrawingVisual class does not provide event handling or layout functionality, so its performance is improved compared with other drawing methods. However, this is a code-only solution and there is no XAML-based DrawingVisual option.

Furthermore, its lack of layout abilities means that, in order to display it, we need to create a class that extends a class that provides layout support in the UI, such as the FrameworkElement class. To be even more efficient, though, we could extend the Visual class, as that is the lightest-weight class that can be rendered in the UI, with the fewest properties and no events to handle.

This class would be responsible for maintaining a collection of Visual elements to be rendered, creating one or more DrawingVisual objects to add to the collection, and overriding a property and a method, in order to participate in the rendering process. It could also, optionally, provide event handling and hit-testing capabilities if user interaction was required.

It really depends on what we want to draw. Typically, the more efficient the drawing, the less flexible it is. For example, if we were just drawing some static clipart, background image, or, perhaps, logo, we could take advantage of the more efficient drawing methods. However, if we need our drawing to grow and shrink as the application windows change size, then we'll need to use the less efficient methods that provide more flexibility, or use another class in addition that provides that functionality.

 

Let's explore an example that creates the same graphical image using each of the three different drawing methods. We'll define some smiley face emoticons, starting with the Shape-based method on the left-hand side, the Drawing object-based method in the center, and the DrawingVisual-based method on the right. Let's first look at the visual output:

Now, let's inspect the XAML:

<UserControl x:Class="CompanyName.ApplicationName.Views.DrawingView" 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
  xmlns:Controls=
    "clr-namespace:CompanyName.ApplicationName.Views.Controls" 
  xmlns:PresentationOptions=
    "http://schemas.microsoft.com/winfx/2006/xaml/presentation/options" 
  Width="450" Height="150"> 
  <Grid> 
    <Grid.Resources> 
      <RadialGradientBrush x:Key="RadialBrush" RadiusX="0.8" RadiusY="0.8"
        PresentationOptions:Freeze="True"> 
        <GradientStop Color="Orange" Offset="1.0" /> 
        <GradientStop Color="Yellow" /> 
      </RadialGradientBrush> 
    </Grid.Resources> 
    <Grid.ColumnDefinitions> 
      <ColumnDefinition /> 
      <ColumnDefinition /> 
      <ColumnDefinition /> 
    </Grid.ColumnDefinitions> 
    <Grid> 
      <Grid.RowDefinitions> 
        <RowDefinition Height="3*" /> 
        <RowDefinition Height="2*" /> 
        <RowDefinition Height="2*" /> 
        <RowDefinition Height="2*" /> 
        <RowDefinition Height="3*" /> 
      </Grid.RowDefinitions> 
      <Grid.ColumnDefinitions> 
        <ColumnDefinition /> 
        <ColumnDefinition /> 
        <ColumnDefinition /> 
        <ColumnDefinition /> 
        <ColumnDefinition /> 
      </Grid.ColumnDefinitions> 
      <Ellipse Grid.RowSpan="5" Grid.ColumnSpan="5"  
        Fill="{StaticResource RadialBrush}" Stroke="Black"  
        StrokeThickness="5" /> 
      <Ellipse Grid.Row="1" Grid.Column="1" Fill="Black" Width="20"  
        HorizontalAlignment="Center" /> 
      <Ellipse Grid.Row="1" Grid.Column="3" Fill="Black" Width="20"  
        HorizontalAlignment="Center" /> 
      <Path Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="3" Stroke="Black"
        StrokeThickness="10" StrokeStartLineCap="Round"
        StrokeEndLineCap="Round" Data="M0,10 A10,25 0 0 0 12.5,10" 
        Stretch="Fill" HorizontalAlignment="Stretch" /> 
    </Grid> 
    <Canvas Grid.Column="1"> 
      <Canvas.Background> 
        <DrawingBrush PresentationOptions:Freeze="True"> 
          <DrawingBrush.Drawing> 
            <DrawingGroup> 
              <GeometryDrawing Brush="{StaticResource RadialBrush}"> 
                <GeometryDrawing.Geometry> 
                  <EllipseGeometry Center="50,50" RadiusX="50"  
                    RadiusY="50" /> 
                </GeometryDrawing.Geometry> 
                <GeometryDrawing.Pen> 
                  <Pen Thickness="3.5" Brush="Black" /> 
                </GeometryDrawing.Pen> 
              </GeometryDrawing> 
              <GeometryDrawing Brush="Black"> 
                <GeometryDrawing.Geometry> 
                  <EllipseGeometry Center="29.5,33" RadiusX="6.75"  
                    RadiusY="8.5" /> 
                </GeometryDrawing.Geometry> 
              </GeometryDrawing> 
              <GeometryDrawing Brush="Black"> 
                <GeometryDrawing.Geometry> 
                  <EllipseGeometry Center="70.5,33" RadiusX="6.75"  
                    RadiusY="8.5" /> 
                </GeometryDrawing.Geometry> 
              </GeometryDrawing> 
              <GeometryDrawing> 
                <GeometryDrawing.Geometry> 
                  <PathGeometry> 
                    <PathGeometry.Figures> 
                      <PathFigure StartPoint="23,62.5"> 
                        <ArcSegment Point="77,62.5" Size="41 41" /> 
                      </PathFigure> 
                    </PathGeometry.Figures> 
                  </PathGeometry> 
                </GeometryDrawing.Geometry> 
                <GeometryDrawing.Pen> 
                  <Pen Thickness="7" Brush="Black" StartLineCap="Round"
                    EndLineCap="Round" /> 
                </GeometryDrawing.Pen> 
              </GeometryDrawing> 
            </DrawingGroup> 
          </DrawingBrush.Drawing> 
        </DrawingBrush> 
      </Canvas.Background> 
    </Canvas> 
    <Canvas Grid.Column="2"> 
      <Canvas.Background> 
        <VisualBrush> 
          <VisualBrush.Visual> 
            <Controls:SmileyFace /> 
          </VisualBrush.Visual> 
        </VisualBrush> 
      </Canvas.Background> 
    </Canvas> 
  </Grid> 
</UserControl> 

The first thing that we can see straight away from this example is that the Shape object-based method of drawing is far simpler, achieving the same output as the far more verbose Drawing object-based method in far fewer lines of XAML. Let's now investigate the code.

After defining the PresentationOptions XAML namespace, we declare a RadialGradientBrush resource and optimize its efficiency, by freezing it using the Freeze attribute that was discussed earlier in the chapter. Note that if we were planning on using this control multiple times simultaneously, then we could be even more efficient, by declaring all of our Brush and Pen objects in the application resources and referencing them with StaticResource references.

We then declare an outer Grid panel that has two columns. In the left column, we declare another Grid panel, with five rows and five columns. This inner panel is used to position the various Shape elements that make up the first smiley face. Note that we use star sizing on the row definitions of this panel in order to slightly increase the sizes of the top and bottom rows to better position the eyes and mouth of the face.

Inside the panel, we define an Ellipse object to create the overall shape of the face, fill it with our brush from the resources, and add an outline with a black brush. We then use two further Ellipse elements filled with the black brush to draw the eyes and a Path element to draw the smile. Note that we do not fill the Path element, as that would look more like an open mouth than a smile.

Two other important points to note are that we must set the Stretch property to Fill in order to get the Path element to fill the available space that we provide it with, and we must set the StrokeStartLineCap and StrokeEndLineCap properties to Round to produce the nice, rounded ends of the smile.

We specify the shape that the Path element should be using its Data property and the inline mini-language that we used previously. Let's now break this value down into the various mini-language commands:

M0,10 A10,25 0 0 0 12.5,10 

As with the previous example, we start with the Move command, specified by M and the following coordinate pair, which dictates the start point for the line. The remainder is taken up with the Elliptical Arc command, which is specified by A and the following five figures.

In order, the five figures of the Elliptical Arc command relate to the size of the arc, or its x and y radii, its rotation angle, a bit field to specify whether the angle of the arc should be greater than 180 degrees or not, another bit field to specify whether the arc should be drawn in a clockwise or an anti-clockwise direction, and, finally, the end point of the arc.

Full details of this path mini-language syntax can be found on the Microsoft website. Note that we could change the bit field of the drawing direction to a 1 in order to draw a frown instead:

M0,10 A10,25 0 0 1 12.5,10 

Now, let's move onto the second column of the outer Grid panel now. In this column, we recreate the same smiley face but using the more efficient Drawing object-based objects. As they cannot render themselves like the Shape classes and we need to utilize other elements to do that job for us, we define them inside a DrawingBrush element and use that to paint the background of a Canvas object.

There are two important things to note here. The first is that we could have used the DrawingBrush element to paint any class that extends the FrameworkElement class, such as a Rectangle element, or another type of panel.

The second is that as we have frozen the DrawingBrush element using the Freeze attribute, all of the inner elements that extend the Freezable type will also be frozen. In this case, that includes the GeometryDrawing objects, the EllipseGeometry and PathGeometry objects, and even the Brush and Pen elements that were used to paint them.

When using a DrawingBrush object to render our drawings, we must define them using the Drawing property. As we want to build up our image from multiple Drawing-based objects, we need to wrap them all in a DrawingGroup object.

In order to recreate the overall shape of the face, we start with a GeometryDrawing element and specify an EllipseGeometry object as its Geometry property value. With this GeometryDrawing element, we paint the background by setting a reference of our RadialGradientBrush resource to its Brush property, and define a new Pen instance in its Pen property to specify a stroke for it.

As with all Geometry objects, we specify its dimensions so that they are in scale with each other, rather than using exact pixel sizes. For example, our View is 150 pixels high; however, instead of setting the Center property of this EllipseGeometry object to 75, which is half of the height, we have set it to 50.

As the two radii properties are also set to 50, they remain in scale with the position of the center and the resulting image is scaled to fit the container that it is rendered in. The scale that we use is up to our preference. For example, we could divide or multiply all of the coordinates, radii, and brush and pen thicknesses in our drawing example by the same amount and we would end up with the same face visual.

Next, we add another GeometryDrawing element with an EllipseGeometry object specified in its Drawing property for each of the two eyes on the face. These have no stroke and so have nothing assigned to the Pen property and are colored only using a black Brush set to their Brush properties. The final GeometryDrawing element hosts a PathGeometry object that draws the smile on the face.

Note that defining a PathGeometry object in XAML is far more verbose than using the path mini-language syntax. In it, we need to specify each PathFigure element in the PathFigures collection property, although actually declaring the surrounding collection in XAML is optional. In the case of our smile, we just need to define a single PathFigure element containing an ArcSegment object.

The StartPoint property of the PathFigure element dictates where the arc should start, the Size property of the ArcSegment object relates to the size of the arc, or its x and y radii, while its Point property specifies the end point of the arc.

In order to define round ends for the smile, as we did with the previous smiley face, the Pen element that we specify for this PathGeometry object must have its StartLineCap and EndLineCap properties set to the Round member of the PenLineCap enumeration. This completes the second method of drawing a smiley face.

The third method uses DrawingVisual objects in code internally and results in a Visual object. As the items in the Children collection of the Grid panel are of the UIElement type, we cannot add our Visual control to it directly. Instead, we can set it to the Visual property of a VisualBrush element and paint the background of an efficient container, such as a Canvas control, with it.

Let's now take a look at the code in this SmileyFace class:

using System; 
using System.Collections.Generic; 
using System.Windows; 
using System.Windows.Media; 
 
namespace CompanyName.ApplicationName.Views.Controls 
{ 
  public class SmileyFace : Visual 
  { 
    private VisualCollection visuals; 
 
    public SmileyFace() 
    { 
      visuals = new VisualCollection(this); 
      visuals.Add(GetFaceDrawingVisual()); 
    } 
 
    private DrawingVisual GetFaceDrawingVisual() 
    { 
      RadialGradientBrush radialGradientBrush =  
        new RadialGradientBrush(Colors.Yellow, Colors.Orange); 
      radialGradientBrush.RadiusX = 0.8; 
      radialGradientBrush.RadiusY = 0.8; 
      radialGradientBrush.Freeze(); 
      Pen outerPen = new Pen(Brushes.Black, 5.25); 
      outerPen.Freeze(); 
      DrawingVisual drawingVisual = new DrawingVisual(); 
      DrawingContext drawingContext = drawingVisual.RenderOpen(); 
      drawingContext.DrawEllipse(radialGradientBrush, outerPen,  
        new Point(75, 75), 72.375, 72.375);  
      drawingContext.DrawEllipse(Brushes.Black, null,  
        new Point(44.25, 49.5), 10.125, 12.75); 
      drawingContext.DrawEllipse(Brushes.Black, null,  
        new Point(105.75, 49.5), 10.125, 12.75); 
      ArcSegment arcSegment = 
        new ArcSegment(new Point(115.5, 93.75), new Size(61.5, 61.5), 0,
        false, SweepDirection.Counterclockwise, true); 
      PathFigure pathFigure = new PathFigure(new Point(34.5, 93.75),  
        new List<PathSegment>() { arcSegment }, false); 
      PathGeometry pathGeometry =  
        new PathGeometry(new List<PathFigure>() { pathFigure }); 
      pathGeometry.Freeze(); 
      Pen smilePen = new Pen(Brushes.Black, 10.5); 
      smilePen.StartLineCap = PenLineCap.Round; 
      smilePen.EndLineCap = PenLineCap.Round; 
      smilePen.Freeze(); 
      drawingContext.DrawGeometry(null, smilePen, pathGeometry); 
      drawingContext.Close(); 
      return drawingVisual; 
    } 
 
    protected override int VisualChildrenCount 
    { 
      get { return visuals.Count; } 
    } 
 
    protected override Visual GetVisualChild(int index) 
    { 
      if (index < 0 || index >= visuals.Count)  
        throw new ArgumentOutOfRangeException(); 
      return visuals[index]; 
    } 
  } 
} 

There are several classes that we could have extended our SmileyFace class from, in order to display it in the UI. As we saw in Chapter 5, Using the Right Controls for the Job, most UI controls have a rich inheritance hierarchy, with each extended class offering some particular functionality.

In order to make the most efficient container for our DrawingVisual, we want to extend a class that enables it to take part in the layout process, but adds as little additional overhead via unused properties and unrequired event handling as possible. As such, we have chosen the Visual class, which cannot be used as a UI element directly in the XAML, but it can be displayed as the visual of a VisualBrush element and used to paint a surface with.

To generate one or more DrawingVisual elements in our SmileyFace class, we need to declare and maintain a VisualCollection instance that will hold the Visual elements that we want to display. In the constructor, we initialize this collection and add the single DrawingVisual element that we want to render to it in this example, via the GetFaceDrawingVisual method.

In the GetFaceDrawingVisual method, we first declare a new version of our RadialBrush resource using the RadialGradientBrush class and a Pen element and freeze them using their Freeze methods. Next, we initialize a single DrawingVisual element and access a DrawingContext object from its RenderOpen method, with which to draw our shape.

We use the DrawingContext object to draw the ellipse that serves as the background for the face first. It is colored using the frozen Brush and pen elements. Note that, as the Visual class has no Stretch property or concept of size, the dimensions that we use here are exact device-independent pixel dimensions, rather than relative values, as were used in the previous drawing methods.

In this example, our smiley faces are 150 pixels wide by 150 pixels tall, so the center position will be half of that. Therefore, these exact pixel values can be calculated by multiplying the relative values from the previous Drawing-based example by 1.5.

However, we also need to consider the fact that the outline will be drawn half inside the drawing and half outside. As such, we need to adjust the two radii of this ellipse, reducing them by half of the outline size. As the pen used for this ellipse has a thickness of 5.25 device-independent pixels, we need to reduce each radius by 2.625.

Next, we call the DrawEllipse method again to draw each of the eyes, passing in a black brush and no Pen element, along with their newly calculated positions and sizes. For the smile, we first need to create an ArcSegment element and add that to a collection of the PathSegment type, while initializing a PathFigure object.

We then add the PathFigure object to a collection and pass that to the constructor of the PathGeometry object to initialize it. Next, we define the Pen object that will be used to draw the smile, ensuring that we set its StartLineCap and EndLineCap properties to the Round member of the PenLineCap enumeration, as in the previous examples.

We then freeze this Pen object and pass it, along with the PathGeometry object, to the DrawGeometry method of the DrawingContext object to draw it. Finally, we close the drawing context using its Close method and return the single DrawingVisual element that we just created.

While we have now taken care of the code that draws our smiley face, we will not be able to see anything in the UI yet. In order to participate in the rendering process, we need to override a couple of members from the Visual class, the VisualChildrenCount property, and the GetVisualChild method.

When overriding these members, we need to inform the Visual class of the visuals that we want it to render for us. As such, we simply return the number of items in our internal VisualCollection object from the VisualChildrenCount property and return the item in the collection that relates to the specified index input parameter from the GetVisualChild method.

In this example, we have added a check for invalid values from the index input parameter, although this shouldn't ever occur if we output the correct number of items from the VisualChildrenCount property in the first place.

So, now we have seen three different drawing methods for creating the same visual output, with each being more efficient than the previous one. However, apart from the efficiency differences, we should also be aware of the differences in these drawing methods when it comes to the manipulation and versatility of the elements.

As an example, let's adjust the Width of our DrawingView class, set its ClipToBounds property to true, and view its new output:

  Width="225" Height="150" ClipToBounds="True"> 

Let's now run the application again and see the output:

As you can see from the preceding screenshot, these drawing methods behave differently when resized. The first method is redrawn at the current size and the thickness of each drawn line remains the same, even though the width of this face has been narrowed by the space provided to it from the parent Grid panel.

However, the second and third smiley faces actually look like squashed images, where the thickness of each line is no longer static; the more vertical the line is, the thinner it now becomes. The overall widths of these faces have also been adjusted by the parent Grid panel.

The third face, however, has only been scaled by the VisualBrush object that is used to display it. If instead of extending the Visual class, we had wanted to derive from the UIElement class to utilize some of its functionality, or perhaps to enable us to display our SmileyFace control directly in the XAML, then we would see a different output. Let's make a slight adjustment to our class declaration:

public class SmileyFace : UIElement 

Let's also display it directly in the XAML now, replacing the Canvas and VisualBrush objects that previously displayed it:

<Controls:SmileyFace Grid.Column="2" /> 

Now, if we run the application again and see the output, it will look very different:

Because we specified exact values for our drawing, our SmileyFace control does not extend any class that would enable resizing or scaling, and we no longer have the VisualBrush object to resize it. That is, the drawing remains exactly as it would be at full size, except that it now no longer fits into the space provided to it from the parent Grid panel.

In order to build the ability to draw the shape at different sizes into our class, we'll need to derive it from a class that provides us with additional properties and functionality. The FrameworkElement class supplies us with both dimension properties that we can use to draw our shape at the required size and a Loaded event that we can use to delay the construction of our shape until the relevant size has been calculated by the layout system.

Let's examine the changes that we'd need to make to achieve this:

public class SmileyFace : FrameworkElement 
{ 
  ...
 
  public SmileyFace() 
  { 
    visuals = new VisualCollection(this); 
    Loaded += SmileyFace_Loaded; 
  } 
 
  private void SmileyFace_Loaded(object sender, RoutedEventArgs e) 
  { 
    visuals.Add(GetFaceDrawingVisual()); 
  } 
 
  private DrawingVisual GetFaceDrawingVisual() 
  { 
    ... 
    DrawingVisual drawingVisual = new DrawingVisual(); 
    DrawingContext drawingContext = drawingVisual.RenderOpen(); 
    drawingContext.DrawEllipse(radialGradientBrush, outerPen,  
      new Point(ActualWidth / 2, ActualHeight / 2), (ActualWidth -  
      outerPen.Thickness) / 2, (ActualHeight - outerPen.Thickness) / 2); 
    drawingContext.DrawEllipse(Brushes.Black, null, new Point(
      ActualWidth / 3.3898305084745761, ActualHeight / 3.0303030303030303),
      ActualWidth / 14.814814814814815, ActualHeight / 11.764705882352942); 
    drawingContext.DrawEllipse(Brushes.Black, null, new Point(
      ActualWidth / 1.4184397163120568, ActualHeight / 3.0303030303030303),
      ActualWidth / 14.814814814814815, ActualHeight / 11.764705882352942); 
    ArcSegment arcSegment = new ArcSegment(new Point(ActualWidth /  
      1.2987012987012987, ActualHeight / 1.6), new Size(ActualWidth /
      2.4390243902439024, ActualHeight /  2.4390243902439024), 0, false,
      SweepDirection.Counterclockwise, true); 
    PathFigure pathFigure = new PathFigure(new Point(ActualWidth /  
      4.3478260869565215, ActualHeight / 1.6), new List<PathSegment>() {
      arcSegment }, false); 
    PathGeometry pathGeometry =
      new PathGeometry(new List<PathFigure>() { pathFigure }); 
    ... 
    return drawingVisual; 
  } 
 
  ...
} 

The first change is that we need to move the call to generate the shape from the constructor to the SmileyFace_Loaded handling method. If we had not moved this, our shape would have no size, because the ActualWidth and ActualHeight properties that are used to define its size would not have been set by the layout system at that time.

Next, in the GetFaceDrawingVisual method, we need to replace the hardcoded values with divisions of the control's dimensions. The ellipse that draws the whole face is simple to calculate, with a position of half the width and height of the control and radii of half of the width and height of the control minus half of the thickness of the Pen element that draws its outline.

However, if you were wondering where all of the remaining long decimal divisor values came from, the answer is basic mathematics. The original drawing was 150 pixels wide by 150 pixels tall, so we can divide this by the various positions and sizes of the drawn lines from the previous example.

For example, the ellipse that draws the first eye was previously centered with an X position of 44.25. So, to calculate our required width divisor, we simply divide 150 by 44.25, which equals 3.3898305084745761. Therefore, when the control is provided with 150 pixels of space, it will draw the left eye at an X position of 44.25 and it will now scale correctly at all of the other sizes.

The divisors for each position and size of the drawn shapes were all calculated using this method, to ensure that they would be sized appropriately for the space provided to our control. Note that we could have altered the brush and pen thicknesses likewise, but we have opted not to do so in this example for brevity.

When running this example now, we again have a slightly different output:

Now, the first and third faces look more similar, with the thicknesses of their drawn lines being static and unchanging along their length, unlike the second face. So, we can see that we have many options when it comes to creating custom drawings, and we need to balance the need for efficiency with the ease of use of the drawing method and also take the use of the resulting image into consideration.

Before moving onto the next topic in this chapter, there are a few further efficiency savings that we can make when drawing complex shapes. If our code uses a large number of PathGeometry objects, then we can replace them by using a StreamGeometry object instead.

The StreamGeometry class is specifically optimized to handle multiple path geometries and shows better performance than can be attained from using multiple PathGeometry instances. In fact, we have already been using the StreamGeometry class inadvertently, as that is what is used internally when the binding path mini-language syntax is parsed by the XAML reader.

It can be thought of in a similar way to the StringBuilder class, in that it is more efficient at drawing complex shapes than using multiple instances of the PathGeometry class, but it also has some overhead and so only benefits us when replacing a fair number of them.

Finally, rather than display our DrawingVisual using a VisualBrush, which is refreshed during each layout pass, if our drawings are never to be manipulated in the UI, it is even more efficient to create actual images from them and display those instead.

The RenderTargetBitmap class provides a simple way for us to create images from Visual instances, using its Render method. Let's explore an example of this:

using System.IO; 
using System.Windows.Media; 
using System.Windows.Media.Imaging;

...

RenderTargetBitmap renderTargetBitmap = new RenderTargetBitmap( 
  (int)ActualWidth, (int)ActualHeight, 96, 96, PixelFormats.Pbgra32); 
renderTargetBitmap.Render(drawingVisual); 
renderTargetBitmap.Freeze();  
PngBitmapEncoder image = new PngBitmapEncoder(); 
image.Frames.Add(BitmapFrame.Create(renderTargetBitmap)); 
using (Stream stream = File.Create(filePath)) 
{ 
  image.Save(stream); 
} 

We start by initializing a RenderTargetBitmap object with the required dimensions, resolution, and pixel format of the image to create. Note that the Pbgra32 member of the static PixelFormats class specifies a pixel format that follows the sRGB format, using 32 bits per pixel, with each of the four alpha, red, green, and blue channels receiving 8 bits each per pixel.

Next, we pass our DrawingVisual element, or any other element that extends the Visual class, to the Render method of the RenderTargetBitmap class to render it. To make the operation more efficient still, we then call its Freeze method to freeze the object.

In order to save a PNG image file, we first initialize a PngBitmapEncoder object and add the renderTargetBitmap variable to its Frames collection via the Create method of the BitmapFrame class. Finally, we initialize a Stream object using the File.Create method, passing in the desired file name and path, and call its Save method to save the file to the computer's hard drive. Alternatively, the JpegBitmapEncoder class can be used to create a JPG image file.

Let's now move on to find ways of using images more efficiently.