Chapter 13. Graphics

WPF makes it easy to build visually stunning applications. It offers a rich array of drawing capabilities, and it is built to exploit the full power of modern graphics cards. This enables designers to create intricate designs and use animation to bring the UI to life much more easily than before.

WPF’s graphics architecture is not just for designers. The key aspect of the graphics system is its deep integration with the rest of the programming model. It is easy to add graphical elements to any part of your application without the disconcerting change in programming techniques required by many user interface technologies.

Because WPF is a presentation technology, graphics are an important and substantial part of the framework. It would be possible to fill a whole book on WPF’s graphical capabilities alone, so we can only really scratch the surface here. In this chapter, we will look at the fundamental concepts behind using graphics in WPF applications. In later chapters we will look at animation, media, and 3D support.

Graphics Fundamentals

WPF makes it easy to use graphics in your application, and to exploit the power of your graphics hardware. Many aspects of the graphics architecture contribute to this goal. The most important of these is integration.

Integration

Graphical elements can be integrated into any part of your user interface. Many GUI technologies split graphics into a separate world. This requires a “gearshift” when moving from a world of buttons, text boxes, and other controls into a world of shapes and images, because in many systems, these two worlds have different programming models.

For example, Windows Forms and Mac OS X’s Cocoa both provide the ability to arrange controls within a window and build a program that interacts through those controls. They also both provide APIs offering advanced, fully scalable, two-dimensional drawing facilities (GDI+ in the case of Windows Forms, and Quartz 2D on OS X). But these drawing APIs are distinct from the control APIs. Drawing primitives are very different from controls in these systems—you cannot mix the two freely.

WPF, on the other hand, offers shape elements that can participate in the UI tree like any other. So we are free to mix them in with any other kind of element. Example 13-1 shows various examples of this.

Example 13-1. Mixing graphics with other elements
<DockPanel>
  <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
    <TextBlock Text="Mix text, " />
    <Ellipse Fill="Blue" Width="40" />
    <TextBlock Text=" and " />
    <Button>Controls</Button>
  </StackPanel>
  <Ellipse DockPanel.Dock="Left" Fill="Green" Width="100" />
  <Button DockPanel.Dock="Left">Foo</Button>
  <TextBlock FontSize="24" TextWrapping="Wrap">
    And of course you can put graphics into
    your text: <Ellipse Fill="Cyan" Width="50" Height="20" />
  </TextBlock>
</DockPanel>

As you can see, you can mix graphical elements seamlessly with other elements in the markup. Layout works with graphics exactly as it does for any other element. You can see the results in Figure 13-1.

Tip

Although this example is in XAML, you can also use code to create elements. Most of the examples in this chapter use XAML because the structure of the markup directly reflects the structure of the objects being created. However, whether you use markup or code will depend on what you are doing. If you are creating drawings, you will most likely use a design program to create the XAML for these drawings. But if you are building up graphics from data, it might make more sense to do everything from code.

You can use most of the techniques in this chapter in either code or markup. See Appendix A for more information on the relationship between XAML and code.

Mixed content
Figure 13-1. Mixed content

Not only can graphics and the other content live side by side in the markup, but they can even be intermingled. Notice how in Figure 13-1 the ellipse on the righthand side has been arranged within the flow of the containing TextBlock. If you want to achieve this sort of effect in Windows Forms, it is not possible with its Label control—you would have to write a whole new control from scratch that draws both the text and the ellipse. This mixing goes both ways—not only can you mix controls into your graphics, but you can also use graphical elements inside controls. For example, Figure 13-2 shows a button with mixed text and graphics as its caption.

Button with graphical content
Figure 13-2. Button with graphical content

Traditionally in Windows, you would get this effect by relying on the button’s ability to display a bitmap. But bitmaps are just a block of fixed graphics—you can’t easily make parts of a bitmap interactive, or animate selected pieces in response to user input. So, in WPF putting graphics in buttons works a little differently, as you can see in Example 13-2.

Example 13-2. Adding graphics to a Button
<Button>
  <StackPanel Orientation="Horizontal">
    <Canvas Width="20" Height="18" VerticalAlignment="Center">
      <Ellipse Canvas.Left="1" Canvas.Top="1" Width="16" Height="16"
               Fill="Yellow" Stroke="Black" />
      <Ellipse Canvas.Left="4.5" Canvas.Top="5" Width="2.5" Height="3"
               Fill="Black" />
      <Ellipse Canvas.Left="11" Canvas.Top="5" Width="2.5" Height="3"
               Fill="Black" />
      <Path Data="M 5,10 A 3,3 0 0 0 13,10" Stroke="Black" />
    </Canvas>
    <TextBlock VerticalAlignment="Center">Click!</TextBlock>
  </StackPanel>
</Button>

Of course, buttons with images are not a new idea. For example, the Windows Forms Button has an Image property, and in Cocoa, NSButton has a setImage method. But this is pretty inflexible—these controls allow a single caption and a single image to be set. Compare this to Example 13-2, which uses a StackPanel to lay out the interior of the button and just adds the content it requires. You can use any layout panel inside the Button, with any kind of content. Example 13-3 uses a Grid to arrange text and some ellipses within a Button. Figure 13-3 shows the results.

Example 13-3. Layout within a Button
<Button HorizontalAlignment="Center" VerticalAlignment="Center">
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition />
      <ColumnDefinition />
      <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
      <RowDefinition />
      <RowDefinition />
      <RowDefinition />
    </Grid.RowDefinitions>

    <Ellipse Grid.Column="0" Grid.Row="0" Fill="Blue" Width="10" Height="10" />
    <Ellipse Grid.Column="2" Grid.Row="0" Fill="Blue" Width="10" Height="10" />
    <Ellipse Grid.Column="0" Grid.Row="2" Fill="Blue" Width="10" Height="10" />
    <Ellipse Grid.Column="2" Grid.Row="2" Fill="Blue" Width="10" Height="10" />

    <Ellipse Grid.ColumnSpan="3" Grid.RowSpan="3" Stroke="LightGreen"
             StrokeThickness="3" />

    <TextBlock Grid.Column="1" Grid.Row="1" VerticalAlignment="Center"
               Text="Click!" />
  </Grid>
</Button>
Button with Grid content
Figure 13-3. Button with Grid content

In WPF, there is rarely any need for controls to provide properties, such as Text or Image. If it makes sense for a control to present nested content, it’ll do just that by offering a content model—it will present whatever mixture of elements you choose to provide.

If you are familiar with two-dimensional drawing technologies such as Quartz 2D, GDI+, and GDI32, you may have been struck by another difference in the way drawing is done. We no longer need to write a function to respond to redraw requests—WPF can keep the screen repainted for us. This is because WPF lets us represent drawings as objects.

Drawing Object Model

With many GUI technologies, applications that want customized visuals are required to be able to re-create their appearance from scratch. The usual technique for showing a custom appearance is to write code that performs a series of drawing operations in order to construct the display. This code runs when the relevant graphics first need to be displayed. In some systems, the OS does not retain a copy of what the application draws, so this method ends up running anytime an area needs repainting—for example, if a window was obscured and then uncovered.

Updating individual elements is often problematic in systems that use this on-demand rendering style. Even where the OS does retain a copy of the drawing, it is often retained as a bitmap. This means that if you want to change one part of the drawing, you often need to repaint everything in the area that has changed.

WPF offers a different approach: you can add objects representing graphical shapes to the tree of user interface elements. Shape elements are objects in the UI tree like any other, so your code can modify them at any time. If you change some property that has a visual impact—such as the size, location, or color—WPF will automatically update the display.

To illustrate this technique, Example 13-4 shows a simple window containing several ellipses. Each is represented by an Ellipse object, which we will use from the code-behind file to update the display.

Example 13-4. Changing graphical elements
<Window x:Class="ChangeItem.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Change Item">

  <Canvas x:Name="mainCanvas">
    <Ellipse Canvas.Left="10" Canvas.Top="30" Fill="Indigo"
             Width="40" Height="20" />
    <Ellipse Canvas.Left="20" Canvas.Top="40" Fill="Blue"
             Width="40" Height="20" />
    <Ellipse Canvas.Left="30" Canvas.Top="50" Fill="Cyan"
             Width="40" Height="20" />
    <Ellipse Canvas.Left="40" Canvas.Top="60" Fill="LightGreen"
             Width="40" Height="20" />
    <Ellipse Canvas.Left="50" Canvas.Top="70" Fill="Yellow"
             Width="40" Height="20" />
  </Canvas>
</Window>

Example 13-5 shows the code-behind file for this window. It attaches a handler to the main canvas’s MouseLeftButtonDown event. Thanks to event bubbling, this OnClick handler method will be called whenever any of the ellipses is clicked. This method simply increases the Width property of whichever Ellipse raised the event. The result is that clicking on any ellipse will make it wider.

Example 13-5. Changing a shape at runtime
using System.Windows;
using System.Windows.Shapes;

namespace ChangeItem {
    public partial class MainWindow : Window {
        public MainWindow() : base(  ) {
            InitializeComponent(  );
            mainCanvas.MouseLeftButtonDown += OnClick;
        }

        private void OnClick(object sender, RoutedEventArgs e) {
            Ellipse r = e.Source as Ellipse;
            if (r != null) {
                r.Width += 10;
            }
        }
    }
}

If we were using the old approach of drawing everything in a single rendering function, this code would not be sufficient to update the display. It would normally be necessary to tell the OS that the screen is no longer valid, causing it to raise a repaint request. But in WPF, this is not necessary—when you set a property on an Ellipse object, it ensures that the screen is updated appropriately. Moreover, WPF is aware that the items overlap, as shown in Figure 13-4, so it will also redraw the items beneath and above as necessary to get the right results. All you have to do is adjust the properties of the object.

Changing overlapping ellipses
Figure 13-4. Changing overlapping ellipses

Tip

Even though computer memory capacities have increased by orders of magnitude since GUIs first started to appear, in some situations this object model approach for drawing still might be too expensive. In particular, for applications dealing with vast data sets such as maps, having a complete set of objects in the UI tree mirroring the structure of the underlying data could use too much memory. Also, for certain kinds of graphics or data, it may be more convenient to use the old style of rendering code.

Because of this, WPF also supports some lighter weight modes of operation. The "Visual Layer Programming" section, later in this chapter, describes the on-demand rendering mechanisms. The "DrawingBrush" section, also later in this chapter, describes a third technique that is somewhere in between the two, trading off a little flexibility in exchange for better performance—it offers many of the benefits of a retained model, but without the overhead of a full WPF framework element.

You may have noticed that all of the drawing we’ve done so far has been with shapes and not bitmaps. WPF supports bitmaps, of course, but there is a good reason to use shapes—you can scale and rotate geometric shapes without losing image quality. This ability to perform high-quality transforms is an important feature of drawing in WPF.

Resolution Independence

Not only have graphics cards improved dramatically since the first GUIs appeared, but so have screens. For a long time, the only mainstream display technology was the CRT (cathode-ray tube). Color CRTs offer fairly low resolution—they struggle to display images with higher definition than about 100 pixels per inch. However, flat panel displays, which now outsell CRTs, can exceed this by a large margin.

One of the authors’ laptops has a display with a resolution of 150 pixels per inch. Displays are available with more than 200 pixels per inch. It is technically possible to create even higher pixel densities. However, there is a potential problem with using these screens: either everything ends up being so small that it becomes unusable or, if the OS is able to scale things up, it may only be able to do so imperfectly, introducing blurring or other problems. This is because of a pixel-based development culture—the vast majority of applications measure their user interfaces in pixels.

This is not entirely the result of technical limitations. From the very first version of Windows NT, Win32 has made it possible to draw things in a resolution-independent way, because the drawing API—GDI32—allows you to apply transformations to all of your drawings. GDI+, introduced in 2001, offers the same facility. But just because a feature is available doesn’t mean applications will use it—most applications don’t fully exploit this scalability.

Unfortunately, the split between graphics and other UI elements in Win32 means that even if an application does exploit the scalability of the drawing APIs, the rest of the UI won’t automatically follow. Figure 13-5 shows a Windows Forms application that uses GDI+ to draw text and graphics scaled to an arbitrary size.

Incomplete UI scaling in Windows Forms
Figure 13-5. Incomplete UI scaling in Windows Forms

Notice in Figure 13-5 that although the star and the “Hello, world!” text have been scaled, the track bar and label controls have not. This is because drawing transformations affect only what you draw with GDI+—they do not affect the entire UI. And although Windows Forms offers some features to help with scaling the rest of the UI, it’s not completely automatic; you have to take deliberate and nontrivial steps to build a resolution-independent UI in Windows Forms.

Scaling and rotation

WPF solves this problem by supporting transformations at a fundamental level. Instead of providing scalability just at the 2D drawing level, it is built into the underlying composition engine. The result is that everything in the UI can be transformed, not just the user-drawn graphics. Going back to our smiley face button in Figure 13-2, we can exploit this scalability with a simple addition just after the first line:

<Button>
  <Button.LayoutTransform>
    <ScaleTransform ScaleX="3" ScaleY="3" />
  </Button.LayoutTransform>
  ... as before ...
</Button>

The LayoutTransform property is available on all user interface elements in WPF, so you can scale the contents of an entire window just as easily as a single button. Many kinds of transformations are available, and we will discuss them in more detail later. For now, we are simply asking to enlarge the button by a factor of three in both x and y dimensions.

Figure 13-6 shows the enlarged button. When compared to the original Figure 13-2, it is larger, obviously. More significantly, the details have become crisper. The rounded edges of the button are easier to see than in the small version. The shapes of the letters are much better defined. And, our graphic is clearer. We get this clarity because WPF has rendered the button to look as good as it can at the specified size. Compare this with the examples in Figure 13-7.

Enlarged button with graphics
Figure 13-6. Enlarged button with graphics
Enlarged bitmaps
Figure 13-7. Enlarged bitmaps

Figure 13-7 shows what happens if you simply enlarge a bitmap of the original small button. There are several different ways of enlarging bitmaps. The example on the left uses the simplest algorithm, known as nearest neighbor or, sometimes, pixel doubling. To make the image larger, pixels have been repeated. This lends a very square feel to the image. The example on the right uses a more sophisticated interpolation algorithm. It has done a better job of keeping rounded edges looking round, and doesn’t suffer from the chunky pixel effect, but it ends up looking very blurred. Clearly, neither of these comes close to Figure 13-6.

Resolution, coordinates, and “pixels”

This support for scaling graphics means that there is no fixed relationship between the coordinates your application uses and the pixels on-screen. This is true even if you do not use scaling transforms yourself—a transform may be applied automatically to your whole application if it is running on a high-DPI display.

What are the default units of measurement in a WPF application if not physical pixels? The answer is, somewhat confusingly, pixels! To be more precise, the real answer is device-independent pixels.

WPF defines a device-independent pixel as 1/96th of an inch. If you specify the width of a shape as 96 pixels, this means that it should be exactly 1 inch wide. WPF will use as many physical pixels as are required to fill 1 inch. For example, high-resolution laptop screens typically have a resolution of 150 pixels per inch. So, if you make a shape’s width 96 “pixels,” WPF will render it 150 physical pixels wide.

Tip

WPF discovers the physical pixel size from the system-wide display settings, so these need to be set accurately in order for elements to be displayed at the correct size. However, very few systems have this configured correctly, so the physical dimensions are often arbitrary in practice. But it’s easy enough to configure your system correctly if you know the pixel density.

On Windows Vista, you can change this setting by right-clicking on the desktop, selecting Personalize, and then choosing “Adjust font size (DPI)” from the list of options that appears on the left. In the DPI scaling window that appears, click the Custom DPI button. Or in Windows XP, right-click on your desktop and select Properties to display the applet, and then go to the Settings tab. Click on the Advanced button, and in the dialog that opens, select the General tab. This lets you tell Windows your screen resolution. If you set the number to match the physical characteristics of your screen, WPF will render content at the correct physical size.

You might be wondering why WPF uses the somewhat curious choice of 1/96th of an inch, and why it calls this a “pixel.” The reason is that 96 dpi is the default display DPI in Windows when it is running with Small Fonts, so this has long been considered the “normal” size for a pixel. This means that on screens with a normal pixel density, a device-independent pixel will correspond to a physical pixel. On screens with a high pixel density, if the system DPI is correctly configured, WPF will scale your drawings for you so that they remain at the correct physical size, so a device-independent pixel may not correspond to an exact number of physical pixels.

WPF’s capability to optimize its rendering of graphical features for any scale means it is ideally placed to take advantage of increasing screen resolutions. For the first time, on-screen text and graphics will be able to compete with the crisp clarity we have come to expect from laser printers. Of course, for all of this to work in practice, we need a comprehensive suite of scalable drawing primitives.

Shapes, Brushes, and Pens

Most of the classes in WPF’s drawing toolkit fall into one of three categories: shapes, brushes, and pens. There are many variations on these themes, and we will examine them in detail later. However, to get anywhere at all with graphics, we need a basic understanding.

Shapes are objects in the user interface tree that provide the basic building blocks for drawing. The Ellipse, Path, and Rectangle elements we have seen already are all examples of shape objects. There is also support for lines, both single- and multisegment, using Line and Polyline, respectively. Polygon creates closed shapes whose edges are all straight. The Path class supports both open and closed shapes with any mixture of straight and curved edges. Figure 13-8 shows each of these shapes in action.

Rectangle, Ellipse, Line, Polyline, Polygon, and Path
Figure 13-8. Rectangle, Ellipse, Line, Polyline, Polygon, and Path

Regardless of which shape you choose, you’ll need to decide how it should be colored in. For this, you use a brush. Many brush types are available. The simplest is the single-color SolidColorBrush. You can achieve more interesting visual effects using the LinearGradientBrush or RadialGradientBrush. These allow the color to change over the surface of a shape, which can be a great way of providing an impression of depth. You can also create brushes based on images—the ImageBrush uses a bitmap, and the DrawingBrush uses a scalable drawing. Finally, the VisualBrush lets you take any visual tree—any chunk of user interface you like—and use that as a brush to paint some other shape. This makes it easy to achieve effects such as reflections of whole sections of your user interface, or wrapping a user interface around a 3D model.

Finally, pens are used to draw the outline of a shape. A pen is really just an augmented brush. When you create a Pen object, you give it a Brush to tell it how it should paint onto the screen. The Pen class just adds information like line thickness, dash patterns, and end cap details. Figure 13-9 shows a few of the effects available using brushes and pens.

Brushes and pens
Figure 13-9. Brushes and pens

Composition

The final key feature of the graphics architecture is composition. In computer graphics, the term composition refers to the process of combining multiple shapes or images together to form the final output. WPF’s composition model is very different from how Windows has traditionally worked, and it is crucial to enabling the creation of high-quality visuals.

In the classic Win32 model, each user interface element (each HWND) has exclusive ownership of some region of the application’s window. Within each top-level window, any given pixel in that window is controlled completely by exactly one element. This prevents elements from being partially transparent. It also precludes the use of anti-aliasing around the edges of elements, a technique which is particularly important when combining nonrectangular elements. Although various hacks have been devised to provide the illusion of transparency in Win32, they all have limitations, and can be somewhat inconvenient to work with.

WPF’s composition model supports elements of any shape, and allows them to overlap. It also allows elements to have any mixture of partially and completely transparent areas. This means that any given pixel on-screen may have multiple contributing visible elements. Moreover, WPF uses anti-aliasing around the edges of all shapes. This reduces the jagged appearance that simpler drawing techniques can produce on-screen, resulting in a smooth-looking image. Finally, the composition engine allows any element to have a transformation applied before composition.

WPF’s composition engine makes use of the capabilities of modern graphics cards to accelerate the drawing process. Internally, it is implemented on top of Direct3D. This may seem odd because the majority of WPF’s drawing functionality is two-dimensional, but most of the 3D-oriented functionality on a modern graphics card can also be used to draw 2D shapes. For example, WPF exploits the same ultra-fast polygon-drawing facilities used by 3D games to render primitive shapes.

Now that we’ve seen the core concepts underpinning the WPF graphics system, let’s take a closer look at the details.

Shapes

The System.Windows.Shapes namespace defines drawing primitives that act as elements in the user interface tree. WPF supports a variety of different shapes, and provides element types for each of them, which are shown in Table 13-1. These integrate with framework-level functionality such as layout, styling, and data binding. These services are not without their costs, so it’s useful to be aware that the shape classes provide a layer of abstraction on top of a lower-level set of services. See the "Shape Objects Versus Geometries" sidebar for details.

Table 13-1. Shapes

Shape Type

Usage

Ellipse

An ellipse

Line

A single straight line

Path

A shape using any mixture of straight lines and curves

Polygon

A closed shape made from straight lines

Polyline

An open shape made from straight lines

Rectangle

A rectangle, optionally with rounded corners

Base Shape Class

All of the elements described in this section derive from a common abstract base class, Shape. Shape defines a common set of features that you can use on all shapes. These common properties are mainly concerned with the way in which the interior and outline of the shape are painted.

The Fill property specifies the Brush that will be used to paint the interior. (The Line class doesn’t have an interior, so it ignores this property. This was simpler than complicating the inheritance hierarchy by having separate Shape and FilledShape base classes.) The Stroke property specifies the Brush that will be used to paint the outline of the shape.

Warning

If you do not specify either a Fill or a Stroke for your shape, it will be invisible, because both of these properties are null by default.

It may seem peculiar that the Stroke property is of type Brush. As we saw earlier, WPF defines a Pen class for specifying a line’s thickness, dash patterns, and the like, so it would make more sense if the Stroke property were of type Pen. WPF does in fact use a Pen internally to draw the outline of a shape. The Stroke property is of type Brush mainly for convenience—all of the Pen features are exposed through separate properties on Shape, as shown in Table 13-2. This simplifies the markup in scenarios where you’re happy to use the default pen settings—you don’t need to provide a full Pen definition just to set the outline color.

Table 13-2. Shape Stroke properties and Pen equivalents

Shape property

Equivalent Pen property

Stroke

Brush

StrokeThickness

Thickness

StrokeLineJoin

LineJoin

StrokeMiterLimit

MiterLimit

StrokeDashArray

DashArray

StrokeDashCap

DashCap

StrokeDashOffset

DashOffset

StrokeStartLineCap

StartLineCap

StrokeEndLineCap

EndLineCap

The "Brushes and Pens" section, later in this chapter, describes brushes and pens in detail.

The Shape class also defines a Stretch property, which determines how a shape will be adjusted if the available space doesn’t match its preferred size. None means that the shape will simply be whatever size and shape you ask for. If you set this to Fill, the shape will be adjusted to fill the available space. Fill allows the shape to be distorted if necessary in order to fit exactly. Uniform and UniformToFill scale equally, in both directions. The former scales until the shape is large enough in at least one dimension to fill the available space, but will leave spare space on the other dimension if necessary so as to avoid cropping. You can see this on the lefthand side of Figure 13-10. The latter scales the shape until it’s large enough to completely fill the space in both dimensions, even if this means cropping in one, as shown on the right of Figure 13-10.

Uniform (left) and UniformToFill (right)
Figure 13-10. Uniform (left) and UniformToFill (right)

Rectangle and Ellipse default to a Stretch of Fill, whereas the other shapes all default to None.

The classes that derive from Shape all add properties specific to the kind of shape they represent. So, we will now look at each of these types, starting with Rectangle.

Rectangle

Rectangle does what its name suggests. As with any shape, it can be drawn either filled in, as an outline, or both filled in and outlined. As well as drawing a normal rectangle, it can also draw one with rounded corners.

Rectangle doesn’t provide any properties for setting its location. It relies on the same layout mechanisms as any other UI element. The location is determined by the containing panel. The width and height can either be set automatically by the parent, or they can be set explicitly using the standard layout properties, Width and Height.

Example 13-6 shows a Rectangle on a Canvas panel. Here the Width and Height have been set explicitly, and the location has been specified using the attached Canvas.Left and Canvas.Top properties.

Example 13-6. Rectangle with explicit size and position
<Canvas>
  <Rectangle Fill="Yellow" Stroke="Black"
             Canvas.Left="30" Canvas.Top="10"
             Width="100" Height="20" />
</Canvas>

Example 13-7 shows the other approach; none of the rectangles has its location or size set explicitly. They are relying on the containing Grid to do this. Figure 13-11 shows the result.

Example 13-7. Rectangles with size and position controlled by parent
<Grid>
  <Grid.ColumnDefinitions>
    <ColumnDefinition />
    <ColumnDefinition />
  </Grid.ColumnDefinitions>

  <Grid.RowDefinitions>
    <RowDefinition />
    <RowDefinition />
  </Grid.RowDefinitions>

  <Rectangle Grid.Column="0" Grid.Row="0" Fill="LightGray" />
  <Rectangle Grid.Column="1" Grid.Row="0" Fill="Black" />
  <Rectangle Grid.Column="0" Grid.Row="1" Fill="DarkGray" />
  <Rectangle Grid.Column="1" Grid.Row="1" Fill="White" />
</Grid>
Rectangles arranged by a Grid
Figure 13-11. Rectangles arranged by a Grid

A Rectangle will usually be aligned with the coordinate system of its parent panel. This means that its edges will normally be horizontal and vertical, although if the parent panel has been rotated, Rectangle will of course be rotated along with it. If you want to rotate a Rectangle relative to its containing panel, you can use the RenderTransform property available on all user interface elements, as Example 13-8 shows.

Example 13-8. Rotating rectangles
<Canvas>
  <Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
             Fill="Indigo" />
  <Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
             Fill="Violet">
    <Rectangle.RenderTransform>
      <RotateTransform Angle="45" />
    </Rectangle.RenderTransform>
  </Rectangle>
  <Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
             Fill="Blue">
    <Rectangle.RenderTransform>
      <RotateTransform Angle="90" />
    </Rectangle.RenderTransform>
  </Rectangle>
  <Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
             Fill="Cyan">
    <Rectangle.RenderTransform>
      <RotateTransform Angle="135" />
    </Rectangle.RenderTransform>
  </Rectangle>
  <Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
             Fill="Green">
    <Rectangle.RenderTransform>
      <RotateTransform Angle="180" />
    </Rectangle.RenderTransform>
  </Rectangle>
  <Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
             Fill="Yellow">
    <Rectangle.RenderTransform>
    <RotateTransform Angle="225" />
   </Rectangle.RenderTransform>
  </Rectangle>
  <Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
             Fill="Orange">
    <Rectangle.RenderTransform>
    <RotateTransform Angle="270" />
   </Rectangle.RenderTransform>
  </Rectangle>
  <Rectangle Canvas.Left="50" Canvas.Top="50" Width="40" Height="10"
             Fill="Red">
    <Rectangle.RenderTransform>
    <RotateTransform Angle="315" />
   </Rectangle.RenderTransform>
  </Rectangle>
</Canvas>

This uses RenderTransform to rotate a series of rectangles. Figure 13-12 shows the result.

Rotated rectangles
Figure 13-12. Rotated rectangles

To draw a rectangle with rounded corners, use the RadiusX and RadiusY properties, as Example 13-9 illustrates.

Example 13-9. Rounded rectangle
<Rectangle Width="100" Height="50" Fill="Black" RadiusX="30" RadiusY="20" />

Figure 13-13 shows the result.

Rectangle with rounded corners
Figure 13-13. Rectangle with rounded corners

Ellipse

Ellipse is similar to Rectangle. Obviously it draws an ellipse rather than a rectangle, but the size, location, rotation, fill, and stroke of an Ellipse are controlled in exactly the same way as for a Rectangle, as Example 13-10 shows.

Example 13-10. Ellipse
<Ellipse Width="100" Height="50" Fill="Yellow" Stroke="Black" />

Figure 13-14 shows the result.

Ellipse
Figure 13-14. Ellipse

Line

The Line element draws a straight line from one point to another. It has four properties controlling the location: X1 and Y1 define the start point, and X2 and Y2 determine the end point. These coordinates are relative to wherever the parent panel chooses to locate the Line. Consider Example 13-11.

Example 13-11. Two Line elements in a StackPanel
<StackPanel Orientation="Vertical">
  <TextBlock Background="LightGray">Foo</TextBlock>
  <Line Stroke="Green" X1="20" Y1="10" X2="100" Y2="40" />
  <TextBlock Background="LightGray">Bar</TextBlock>
  <Line Stroke="Green" X1="0" Y1="10" X2="100" Y2="0" />
</StackPanel>

This uses a vertical StackPanel to arrange an alternating sequence of TextBlock and Line elements. The TextBlock elements have gray backgrounds to make it easier to see the vertical extent of each element (see Figure 13-15).

Two Line elements in a StackPanel
Figure 13-15. Two Line elements in a StackPanel

As you can see from Figure 13-15, the Line elements have been placed in the stack just like any other element. The StackPanel has allocated enough height to hold the line. The first of the lines is interesting in that there is some space between the TextBlock above it, and the start of the line. This is because the line’s Y1 property has been set to 10, indicating that the line should start slightly below the top of the location allocated for the Line element. (In WPF, positive Y means down, unlike with a typical mathematical graph.) The second Line element goes all the way to the top because its Y2 property is set to 0, again illustrating that the coordinate system of the line end points is relative to the area allocated to the Line by the containing panel.

You can use the Stretch property to make the Line resize automatically with your layout. The Line in Example 13-12 has start and end points of 0,0 and 1,0. However, because its Stretch is set to Fill, the points will automatically be adjusted to fill the available width.

Example 13-12. Auto-sizing line
<Line Stroke="Black" X1="0" X2="1" Stretch="Fill" />

Polyline

A Polyline lets you draw a connected series of line segments. Instead of having properties for start and end points, Polyline has a Points property, containing a list of coordinate pairs, as Example 13-13 illustrates.

Example 13-13. Polyline
<Polyline Stroke="Blue"
   Points="0,30 10,30 15,0 18,60 23,30 35,30 40,0 43,60 48,30 160,30" />

WPF simply draws a line that goes through each point in turn, as shown in Figure 13-16.

A Polyline
Figure 13-16. A Polyline

As with the Line class, the point coordinates in a Polyline are relative to wherever the containing panel chooses to locate the Polyline.

Polygon

Polygon is very similar to Polyline. It has a Points property that works in exactly the same way as Polyline’s. The only difference is that whereas Polyline always draws an open shape, Polygon always draws a closed shape. To illustrate the difference, Example 13-14 contains a Polyline and a Polygon. They have all of the same properties set.

Example 13-14. A Polyline and a Polygon
<StackPanel Orientation="Horizontal">
  <Polyline Fill="Orange" Stroke="Blue" StrokeThickness="2"
            Points="40,10 70,50 10,50"  />
  <Polygon  Fill="Orange" Stroke="Blue" StrokeThickness="2"
            Points="40,10 70,50 10,50"  />
</StackPanel>

As you can see in Figure 13-17, the Polyline has been left open. The Polygon, on the other hand, has closed the shape by drawing an extra line segment between the last and first points. Both shapes have painted interiors.

A Polyline (left) and a Polygon (right)
Figure 13-17. A Polyline (left) and a Polygon (right)

Because we are free to add points wherever we like to a Polygon, it is easy to end up with a self-intersecting shape (one whose edge crosses itself). With such shapes, what counts as the interior of the shape can be ambiguous. Figure 13-18 shows such a shape, and two possible ways of filling it.

Fill rules: EvenOdd (left) and Nonzero (right)
Figure 13-18. Fill rules: EvenOdd (left) and Nonzero (right)

The Polygon class provides a FillRule property that tells WPF how to deal with ambiguous regions.[87] WPF supports two fill rules. Example 13-15 is the markup for Figure 13-18, and shows both fill rules in use.

Example 13-15. Fill rules
<StackPanel Orientation="Horizontal">
  <Polygon Fill="Orange" Stroke="Blue" StrokeThickness="2" FillRule="EvenOdd"
           Points="50,30 13,41 36,11 36,49 14,18"  />
  <Polygon Fill="Orange" Stroke="Blue" StrokeThickness="2" FillRule="Nonzero"
           Points="50,30 13,41 36,11 36,49 14,18"  />
</StackPanel>

The default rule is EvenOdd, and this is used on the left of Figure 13-18. This is the simplest rule to understand. To determine whether a particular enclosed region is inside or outside the shape, the EvenOdd rule counts the number of lines you have to cross to get from that point to one completely outside the shape. If this number is odd, the point was inside the shape. If it is even, the point is outside the shape. For example, if you start from inside the middle area of the star in Figure 13-18, you will need to cross over an even number of lines in order to get to the outside of the shape. This is why the central area of the star is unfilled when the EvenOdd rule is used.

The second fill rule, Nonzero, is subtler. From Figure 13-18, you might have thought that any enclosed area was deemed to be inside the shape, but it’s not quite that simple. The Nonzero rule performs a similar process to EvenOdd, but rather than simply counting the number of lines, it takes into account the direction in which the line is running. It either increments or decrements the count for each line it crosses, depending on the direction.[88] If the total at the end is nonzero, the point is considered to be inside the shape. The points making up the stars in Example 13-15 always proceed clockwise. This means that if you start from the center of the star, the two lines you must cross to get to the outside of the shape will always point in the same direction. This results in a count of 2 (or −2, depending on which direction you go), which is why the star on the right of Figure 13-18 has its central region filled.

In Figure 13-18, the Nonzero rule has resulted in all enclosed regions being part of the interior. However, if the outline of the shape follows a slightly more convoluted path, the results can be a little more mixed, as Example 13-16 shows.

Example 13-16. Nonzero fill rule with more complex shape
<Polygon Fill="Orange" Stroke="Blue" StrokeThickness="2" FillRule="Nonzero"
  Points="10,10 60,10 60,25 20,25 20,40 40,40 40,18 50,18 50,50 10,50" />

Figure 13-19 shows the results of Example 13-16. This illustrates that the nonzero rule is not quite as straightforward as it may at first seem.

Nonzero rule in action
Figure 13-19. Nonzero rule in action

The nonzero rule is a bit of an oddity. It was popularized by PostScript, so most drawing systems support it, but it’s not always easy to get useful results from a Polygon with this fill rule. It makes more sense in the context of the Path element, which supports multiple figures in a single shape.

Path

Path is by far the most powerful shape. All of the shapes we have looked at up to now have been supplied for convenience, because it is possible to draw all of them with a Path. Path also makes it possible to draw considerably more complex shapes than is possible with the previous shapes we have seen.

As mentioned earlier, the various classes derived from Shape are essentially high-level wrappers around underlying geometry objects. Path is explicit about this—its shape is defined by its Data property, which is of type Geometry. As we saw in the sidebar earlier, a Geometry object describes a particular shape. Table 13-3 shows the various concrete classes for representing different kinds of shapes.

Table 13-3. Geometry types

Type

Usage

CombinedGeometry

Combines two geometry objects using set operations such as intersection or union

EllipseGeometry

An ellipse

GeometryGroup

Combines multiple geometries into one multifigure geometry

LineGeometry

A single straight line

PathGeometry

Defines shapes with any combination of straight lines, elliptical arcs, and Bézier curves

RectangleGeometry

A rectangle

StreamGeometry

More efficient alternative to PathGeometry—can define all the same shapes, but cannot modify the shapes after creation

Three geometry types—RectangleGeometry, EllipseGeometry, and LineGeometry—correspond to the Rectangle, Ellipse, and Line shape types shown earlier. So this Rectangle:

<Rectangle Fill="Blue" Width="40" Height="80" />

is effectively shorthand for this Path:

<Path Fill="Blue">
  <Path.Data>
    <RectangleGeometry Rect="0, 0, 40, 80" />
  </Path.Data>
</Path>

You might be wondering when you would ever use the RectangleGeometry, EllipseGeometry, or LineGeometry in a Path instead of the simpler Rectangle, Ellipse, and Line. One reason is that Path lets you use a special kind of geometry object called a GeometryGroup to create a shape with multiple geometries.

There is a significant difference between using multiple distinct shapes, and having a single shape with multiple geometries. Look at Example 13-17, for instance.

Example 13-17. Two Ellipse elements
<Canvas>
  <Ellipse Fill="Cyan" Stroke="Black" Width="40" Height="80" />
  <Ellipse Canvas.Left="10" Canvas.Top="10" Fill="Cyan" Stroke="Black"
           Width="20" Height="60" />
</Canvas>

This draws two ellipses, one on top of the other. They both have a black outline, so you can see the smaller one inside the larger one, as Figure 13-20 shows.

Two Ellipse elements
Figure 13-20. Two Ellipse elements

Because the Ellipse shape is just a simple way of creating an EllipseGeometry, the code in Example 13-17 is equivalent to the code in Example 13-18. (As you can see, using a Path is considerably more verbose. This is why the Ellipse and other simple shapes are provided.)

Example 13-18. Two Paths with EllipseGeometry elements
<Canvas>
  <Path Fill="Cyan" Stroke="Black">
    <Path.Data>
      <EllipseGeometry Center="20, 40" RadiusX="20" RadiusY="40" />
    </Path.Data>
  </Path>
  <Path Fill="Cyan" Stroke="Black">
    <Path.Data>
      <EllipseGeometry Center="20, 40" RadiusX="10" RadiusY="30" />
    </Path.Data>
  </Path>
</Canvas>

Because the code in Example 13-18 is equivalent to that in Example 13-17, it results in exactly the same output, as previously shown in Figure 13-20. So far, using geometries instead of shapes hasn’t made a difference in the rendered results. This is because we are still using multiple shapes. So we will now show how you can put both ellipses into a single Path, and see how this affects the results. Example 13-19 shows the modified markup.

Example 13-19. One Path with two EllipseGeometry elements
<Canvas>
  <Path Fill="Cyan" Stroke="Black">
    <Path.Data>
      <GeometryGroup>
        <EllipseGeometry Center="20, 40" RadiusX="20" RadiusY="40" />
        <EllipseGeometry Center="20, 40" RadiusX="10" RadiusY="30" />
      </GeometryGroup>
    </Path.Data>
  </Path>
</Canvas>

This version has just a single path. Its Data property contains a GeometryGroup. This allows any number of geometry objects to be added to the same path. Here we have added the two EllipseGeometry elements that were previously in two separate paths. The result, shown in Figure 13-21, is clearly different from the one in Figure 13-20—there is now a hole in the middle of the shape. Because the default even-odd fill rule was in play, the smaller ellipse makes a hole in the larger one. (GeometryGroup has a FillRule property that lets you choose the nonzero rule instead if you need to.)

Path with two geometries
Figure 13-21. Path with two geometries

You can create shapes with holes only by combining multiple figures into a single shape. You could try to get a similar effect to that shown in Figure 13-21 by drawing the inner Ellipse with a Fill color of White, but that trick fails to work as soon as you draw the shape on top of something else, as Figure 13-22 shows.

Spot the fake hole
Figure 13-22. Spot the fake hole

Tip

You might be wondering whether you could just draw the inner ellipse using the Transparent color, but that doesn’t work either—if you tried this, you’d still see all of the larger ellipse, rather than what is behind it. Drawing something as totally transparent has the same effect as drawing nothing at all—that’s what transparency means. Only by knocking a hole in the shape can we see through it.

To understand why, think about the drawing process. When it renders our elements to the screen, WPF draws the items one after the other. It starts with whatever’s at the back—the text, in this case. Then it draws the shape on top of the text, which effectively obliterates the text that was underneath the shape. (It’s still there in the element tree, of course, so WPF can always redraw it later if you change or remove the shape.) Because you just drew over the text, you can’t draw another shape on top to “undraw” a hole into the first shape. So, if you want a hole in a shape, you’d better make sure that the hole is there before you draw it!

This is not to say you’d never use the Transparent color. It has a couple of uses. An animation might fade from a nontransparent color to Transparent in order to make an element disappear gradually. Also, objects that are Transparent are invisible to the eye, but not to the mouse—WPF’s input system (which was described in Chapter 4) treats all brushes as equal, ignoring transparency. So the Transparent color provides a way of making invisible clickable targets.

We have not yet looked at the most flexible geometry: PathGeometry. This is the underlying geometry used by Polyline and Polygon, but it can draw many more shapes besides.

A PathGeometry contains one or more PathFigure objects, and each PathFigure represents a single open or closed shape in the path. To define the shape of each figure’s outline, you use a sequence of PathSegment objects. Like GeometryGroup, PathGeometry also has a FillRule property to set the behavior for overlapping figures. Again, this defaults to the even-odd rule.

Tip

PathGeometry’s ability to contain multiple figures overlaps slightly with GeometryGroup’s ability to contain multiple geometries. This is just for convenience—if you need to make a shape where every piece will be a PathGeometry object, it is more compact to have a single PathGeometry with multiple PathFigures. If you just want a group of simpler geometries like LineGeometry or RectangleGeometry, it is simpler to use a GeometryGroup and avoid PathGeometry altogether.

Example 13-20 shows a simple path. This contains just a single figure in the shape of a square.

Example 13-20. A square Path
<Path Fill="Cyan" Stroke="Black">
  <Path.Data>
    <PathGeometry>
      <PathGeometry.Figures>
        <PathFigure StartPoint="0,0" IsClosed="True">
          <PathFigure.Segments>
            <LineSegment Point="50,0" />
            <LineSegment Point="50,50" />
            <LineSegment Point="0,50" />
          </PathFigure.Segments>
        </PathFigure>
      </PathGeometry.Figures>
    </PathGeometry>
  </Path.Data>
</Path>

Figure 13-23 shows the result. This seems like a vast amount of effort for such a simple result—we’ve used 15 lines of markup to achieve what we could have achieved with a single Rectangle element. This is why WPF supplies classes for the simpler shapes and geometries. You don’t strictly need any of them because you can use Path and PathGeometry instead, but the simpler shapes require much less effort. Normally you would use Path only for more complex shapes.

A square Path
Figure 13-23. A square Path

Even though Example 13-20 produces a very simple result, it illustrates most of the important features of a Path with a PathGeometry. As with all the previous examples, the geometry is in the path’s Data property. The PathGeometry is a collection of PathFigures, so all of the interesting data is inside its Figures property. This example contains just one PathFigure, but you can add as many as you like. The shape of the PathFigure is determined by the items in its Segments property.

The starting point of a PathFigure is determined by its StartPoint property. One or more segments describe the figure’s shape. In Example 13-20, these are all LineSegments because the shape has only straight edges, but several types of curves are also on offer. This particular figure is a closed shape, which is determined by the IsClosed property.

Tip

You might be wondering why LineSegments don’t work like the Line shape or a LineGeometry. With those types, we specify start and end points, as in Example 13-11. This seems simpler than LineSegment, which needs us to specify a StartPoint in the PathFigure.

However, line segments in a PathFigure can’t work like that because there cannot be any gaps in the outline of a figure. With the Line element, each Line is a distinct shape in its own right, but with a PathFigure, each segment is a part of the shape’s outline. To define a figure fully and unambiguously, each segment must start off from where the previous one finished. This is why the LineSegment only specifies an end point for the line. All of the segment types work this way.

Example 13-20 isn’t very exciting; it just uses straight line segments. We can create much more interesting shapes by using one of the curved segment types instead. Table 13-4 shows all of the segment types.

Table 13-4. Segment types

Segment type

Usage

LineSegment

Single straight line

PolyLineSegment

Sequence of straight lines

ArcSegment

Elliptical arc

BezierSegment

Cubic Bézier curve

QuadraticBezierSegment

Quadratic Bézier curve

PolyBezierSegment

Sequence of cubic Bézier curves

PolyQuadraticBezierSegment

Sequence of quadratic Bézier curves

ArcSegment lets you add elliptical curves to the edge of a shape. ArcSegment is a little more complex to use than a simple LineSegment. As well as specifying the end point of the segment, we must also specify two radii for the ellipse with the Size property.

The ellipse size and the line start and end points don’t provide enough information to define the curve unambiguously, because there are several ways to draw an elliptical arc given these constraints. Consider a segment with a particular start and end point, and a given size and orientation of ellipse. For this segment, there will usually be two ways in which we can position the ellipse so that both the start and end points lie on the boundary of the ellipse, as Figure 13-24 shows. In other words, there will be two ways of “slicing” an ellipse with a particular line.

Potential ellipse positions
Figure 13-24. Potential ellipse positions

For each way of slicing the ellipse, there will be two resulting arc segments, a small one and a large one. This means that there are four ways in which the curve could be drawn between two points.

The ArcSegment provides two flags that enable you to select which of the curves you require. IsLargeArc determines whether you get the larger or smaller slice size. SweepDirection chooses on which side of the line the slice is drawn. Example 13-21 shows markup for all four combinations of these flags. It also shows the whole ellipse.

Example 13-21. ArcSegments
<Canvas>
  <Ellipse Fill="Cyan" Stroke="Black" Width="140" Height="60" />
  <Path Fill="Cyan" Stroke="Black" Canvas.Left="180">
    <Path.Data>
      <PathGeometry>
        <PathFigure StartPoint="0,11" IsClosed="True">
          <ArcSegment Point="50,61" Size="70,30"
                      SweepDirection="Counterclockwise" IsLargeArc="False" />
        </PathFigure>
        <PathFigure StartPoint="30,11" IsClosed="True">
          <ArcSegment Point="80,61" Size="70,30"
                      SweepDirection="Clockwise" IsLargeArc="True" />
        </PathFigure>
        <PathFigure StartPoint="240,1" IsClosed="True">
          <ArcSegment Point="290,51" Size="70,30"
                      SweepDirection="Counterclockwise" IsLargeArc="True" />
        </PathFigure>
        <PathFigure StartPoint="280,1" IsClosed="True">
          <ArcSegment Point="330,51" Size="70,30"
                      SweepDirection="Clockwise" IsLargeArc="False" />
        </PathFigure>
      </PathGeometry>
    </Path.Data>
  </Path>
</Canvas>

Tip

You may be wondering why the Ellipse has a width of 140 and a height of 60, which is double the Size of each ArcSegment. This is because the ArcSegment interprets the Size as the two radii of the ellipse, whereas the Width and Height properties on the Ellipse indicate the total size.

Figure 13-25 shows the results, and as you can see, each shape has one straight diagonal line and one elliptical curve. The straight line edge has the same length and orientation in all four cases. The curved edge is from different parts of the same ellipse.

An ellipse and four arcs from that ellipse
Figure 13-25. An ellipse and four arcs from that ellipse

In Figure 13-25, the ellipse’s axes are horizontal and vertical. Sometimes you will want to use an ellipse where the axes are not aligned with your main drawing axes. ArcSegment provides a RotationAngle property, allowing you to specify the amount of rotation required in degrees.

Figure 13-26 shows four elliptical arcs. These use the same start and end points as Figure 13-25, and the same ellipse size. The only difference is that a RotationAngle of 45 degrees has been specified, rotating the ellipse before slicing it.

Four arcs from a rotated ellipse
Figure 13-26. Four arcs from a rotated ellipse

Tip

There are two degenerate cases in which there will not be two ways of slicing the ellipse. The first is when the slice cuts the ellipse exactly in half. In this case, the IsLargeArc flag is irrelevant, because both slices are exactly the same size.

The other case is when the ellipse is too small—if the widest point at which the ellipse could be sliced is narrower than the segment is long, there is no way in which the segment can be drawn correctly. (If you do make the ellipse too small, WPF seems to scale the ellipse so that it is large enough, preserving the aspect ratio between the x- and y-axes.) You should avoid this.

The remaining curve types (BezierSegment, PolyBezierSegment, QuadraticBezierSegment, and PolyQuadraticBezierSegment) are variations on the same theme. They all draw Bézier curves.

Bézier curves

Bézier curves are curved line segments joining two points using a particular mathematical formula. It is not necessary to understand the details of the formula in order to use Bézier curves. What makes Bézier curves useful is that they offer a fair amount of flexibility in the shape of the curve. This has made them very popular—most vector drawing programs offer them.[89]

Figure 13-27 shows a variety of Bézier curve segments. Each of the five lines shown here is a single BezierSegment.

Bézier curve segments
Figure 13-27. Bézier curve segments

As with all of the segment types, a BezierSegment starts from where the preceding segment left off, and defines a new end point. It also requires two “control points” to be defined, and it is these that determine the shape of the curve. Figure 13-28 shows the same curves again, but with the control points drawn on. It also shows lines connecting the control points to the segment end points, because this makes it easier to see how the control points affect the curve shapes.

Bézier curves with control points shown
Figure 13-28. Bézier curves with control points shown

The most obvious way in which the control points influence the shapes of these curves is that they determine the tangent. At the start and end of each segment, the direction in which the curve runs at that point is exactly the same as the direction of the line joining the start point to the corresponding control point.

There is a second, less obvious way in which control points work. The distance between the start or end point and its corresponding control point (i.e., the length of the straight lines added on Figure 13-28) also has an effect. This essentially determines how extreme the curvature is.

Figure 13-29 shows a set of Bézier curves similar to those in Figure 13-28. The tangents of both ends of the lines remain the same, but in each case, the distance between the start point and the first control point is reduced to one-quarter of what it was before, whereas the other is the same as before. As you can see, this reduces the influence of the first control point. In all four cases, the shape of the curve is dominated by the control point that is farther from its end point.

Bézier curves with less extreme control points
Figure 13-29. Bézier curves with less extreme control points

Example 13-22 shows the markup for the second curve segment in Figure 13-28. The Point1 property determines the location of the first control point—the one associated with the start point. Point2 positions the second control point. Point3 is the end point. (To keep things clear, the examples in this section just show the relevant PathFigure elements. If you want to see these shapes, you would of course need to put them inside a PathGeometry inside a Path, just as with the previous examples.)

Example 13-22. BezierSegment
<PathFigure StartPoint="0,50">
  <BezierSegment Point1="60,50" Point2="100,0" Point3="100,50" />
</PathFigure>

Flexible though Bézier curves are, you will rarely use just a single one. When defining shapes with curved edges, it is normal for a shape to have many Bézier curves defining its edge. WPF therefore supplies a PolyBezierSegment type, which allows multiple curves to be represented in a single segment. It defines a single Points property, which is an array of Point structures. Each Bézier curve requires three entries in this array: two control points and an end point. (As always, each segment starts from where the previous one left off.) Example 13-23 shows an example segment with two curves. Figure 13-30 shows the results.

Example 13-23. PolyBézierSegment
<PathFigure StartPoint="0,0">
  <PolyBezierSegment>
    <PolyBezierSegment.Points>
      <Point X="0" Y="10"/>
      <Point X="20" Y="10"/>
      <Point X="40" Y="10"/>
      <Point X="60" Y="10"/>
      <Point X="120" Y="15"/>
      <Point X="100" Y="50"/>
    </PolyBezierSegment.Points>
  </PolyBezierSegment>
</PathFigure>
PolyBezierSegment
Figure 13-30. PolyBezierSegment

This markup is less convenient than simply using a sequence of BezierSegment elements, which rather defeats the point. Fortunately, you can provide all of the point data in string form. This is equivalent to Example 13-23:

<PathFigure StartPoint="0,0">
  <PolyBezierSegment Points="0,10 20,10 40,10 60,10 120,15 100,50" />
</PathFigure>

Also, if you are generating coordinates from code, dealing with a single PolyBezierSegment and passing it an array of Point data is often easier than working with lots of individual segments.

Cubic Bézier curves provide a lot of control over the shape of the line. However, you might not always need that level of flexibility. The QuadraticBezierSegment uses a simpler equation that uses just one control point to define the shape of the curve. This does not offer the same range of curve shapes as a cubic Bézier curve, but if all you want is a simple shape, this reduces the number of coordinate pairs you need to provide by one-third.

QuadraticBezierSegment is similar in use to the normal BezierSegment. The only difference is that it has no Point3 property—just Point1 and Point2. Point1 is the single control point, and Point2 is the end point. PolyQuadraticBezierSegment is the multicurve equivalent. You use this in exactly the same way as PolyBezierSegment, except you need to provide only two points for each segment.

Combining shapes

Geometries can perform one more trick that we have not yet examined. We can combine geometries to form new geometries. This is different from adding two geometries to a GeometryGroup—it is possible to combine pairs of geometries in a way that forms a single geometry with a whole new shape.

Example 13-24 and Example 13-25 define paths, both of which make use of the same RectangleGeometry and EllipseGeometry. The difference is that Example 13-24 puts both into a GeometryGroup, while Example 13-25 puts them into a CombinedGeometry.

Example 13-24. Multiple geometries
<Path Fill="Cyan" Stroke="Black">
  <Path.Data>
    <GeometryGroup>
      <RectangleGeometry Rect="0,0,50,50" />
      <EllipseGeometry Center="50,25" RadiusX="30" RadiusY="10" />
    </GeometryGroup>
  </Path.Data>
</Path>
Example 13-25. Combined geometries
<Path Fill="Cyan" Stroke="Black">
  <Path.Data>
    <CombinedGeometry GeometryCombineMode="Exclude">
      <CombinedGeometry.Geometry1>
        <RectangleGeometry Rect="0,0,50,50" />
      </CombinedGeometry.Geometry1>
      <CombinedGeometry.Geometry2>
        <EllipseGeometry Center="50,25" RadiusX="30" RadiusY="10" />
      </CombinedGeometry.Geometry2>
    </CombinedGeometry>
  </Path.Data>
</Path>

Figure 13-31 shows the results of Example 13-24 and Example 13-25. Whereas the GeometryGroup has resulted in a shape with multiple figures (taking the default fill rule into account), the CombinedGeometry has produced a single figure. The ellipse geometry has taken a bite out of the rectangle geometry. This is just one of the ways in which geometries can be combined. The GeometryCombineMode property determines which is used, and Figure 13-32 shows all four available modes.

Grouping and combining geometries
Figure 13-31. Grouping and combining geometries
Combine modes: Union, Intersect, Xor, and Exclude
Figure 13-32. Combine modes: Union, Intersect, Xor, and Exclude

Union builds a shape in which any point that was inside either of the two original shapes will also be inside the new shape. Intersect creates a shape where only points that were inside both shapes will be in the new shape. Xor creates a shape where points that were in one shape or the other, but not both, will be in the new shape. Exclude creates a shape where points inside the first shape but not inside the second will be included.

Path geometry text format

We have now looked at all of the features that Path has to offer. As you have seen, we can end up with some pretty verbose markup. Fortunately, there is a shorthand mechanism that allows us to exploit most of the features we have seen without having to type quite so much.

So far, we have been setting the Data property using XAML’s property element syntax. (See Appendix A for more details on this syntax.) However, we can supply a string instead. Example 13-26 shows both techniques. As you can see, the string form is some 12 lines shorter.

Example 13-26. Path.Data as text
<!-- Longhand -->

<Path Fill="Cyan" Stroke="Black">
  <Path.Data>
    <PathGeometry>
      <PathGeometry.Figures>
        <PathFigure StartPoint="0,0" IsClosed="True">
          <LineSegment Point="50,0" />
          <LineSegment Point="50,50" />
          <LineSegment Point="0,50" />
        </PathFigure>
      </PathGeometry.Figures>
    </PathGeometry>
  </Path.Data>
</Path>


<!-- Shorthand -->

<Path Fill="Cyan" Stroke="Black" Data="M 0,0 L 50,0 50,50 0,50 Z" />

The syntax for the text form of the Path.Data property is simple. The string must contain a sequence of commands. A command is a letter followed by some numeric parameters. The number of parameters required is determined by the chosen command. Lines require just a coordinate pair. Curves require more data.

If you omit the letter, the same command will be used as last time. For instance, Example 13-26 uses the L command—this is short for Line, and it represents a LineSegment. This requires only two numbers: the coordinates of the line end point. And yet, in our example, there are six numbers. This simply indicates that there are three lines in a row. Table 13-5 lists the commands, their equivalent segment types where applicable, and their usage.

Table 13-5. Path.Data commands

Command

Command name

Segment type

Parameters

M (or m)

Move

 

Coordinate pair: the StartPoint for a new PathFigure

L (or l)

Line

LineSegment

Coordinate pair: end point

H (or h)

Horizontal line

LineSegment

Single coordinate: end x coordinate (y coordinate will be the same as before)

V (or v)

Vertical line

LineSegment

Single coordinate: end y coordinate (x coordinate will be the same as before)

C (or c)

Cubic Bézier curve

BezierSegment

Three coordinate pairs: two control points and one end point

Q (or q)

Quadratic Bézier curve

QuadraticBezierSegment

Two coordinate pairs: control point and end point

S (or s)

Smooth Bézier curve

BezierSegment

Two coordinate pairs: second control point and end point (first control point generated automatically)

T (or t)

Smooth quadratic Bézier curve

QuadraticBezierSegment

One coordinate pair: end point (control point generated automatically)

A (or a)

Elliptical arc

ArcSegment

Seven numbers: x radius, y radius, RotationAngle, IsLargeArc, SweepDirection, and end point coordinate pair

Z (or z)

Close path

 

None

F0

Even-odd fill rule

 

None

F1

Nonzero fill rule

 

None

The commands M, Z, F0, and F1 do not correspond to segments. The M command causes a new PathFigure to be started, enabling multiple figures to be represented in this compact text format. Z sets the current figure’s IsClosed property to true. F0 and F1 set the FillRule of the PathGeometry.

Notice that there are two ways to specify a BezierSegment. The C command lets you provide all of the control points. The S command generates the first control point for you—it looks at the preceding segment and makes the first control point a mirror image of the preceding one. This ensures that the segment’s tangent aligns with the preceding segment’s tangent, resulting in a smooth join between the lines. Quadratic Bézier segments have a similar facility: the Q command lets you specify the control point, whereas the T command generates the control point for you in a way that guarantees a smooth line.

You can specify any of these commands in either uppercase or lowercase. In the uppercase form, coordinates are relative to the position of the Path element. If the command is lowercase, the coordinates are taken to be relative to the end point of the preceding segment in the path.

As well as being offered for the Path.Data property, this path syntax can also be used directly with a PathGeometry—its Figures property supports the same syntax. Another geometry type also supports this mini path language: StreamGeometry. This geometry type can represent all the same shapes as a PathGeometry, but you cannot modify it once it has been created. This is because it does not support the object model of path figures and segments—from markup, it only supports the path syntax. (If you are using code, you also can build a StreamGeometry with a StreamGeometryContext object, which lets you describe the shape with a series of method calls.)

Because a StreamGeometry is immutable and because it does not maintain a tree of objects representing the shape, it can use a more efficient internal representation than a PathGeometry. If you are working with very complex shapes, or a large number of shapes, this can significantly improve performance. If you are using XAML in such scenarios, you should prefer the path syntax over the object tree, because when you set Path.Data with the path syntax, WPF creates a StreamGeometry instead of a PathGeometry.

We have now examined all of the shapes on offer. However, not all visuals are best represented with scalable shapes—sometimes we need to work with bitmap images.

Bitmaps

WPF supports bitmaps in any of the following formats:[90] BMP, JPEG, PNG, TIFF, Windows Media Photo, GIF, and ICO (Windows icon files). You can use any image format to create a brush with which to paint any shape or text, as discussed later in the "ImageBrush" section of this chapter. The System.Windows.Media.Imaging namespace provides classes that let you work with the pixels and metadata of image files. However, the simplest way to use a bitmap is with the Image element.

Image

Image simply displays an image. It derives from FrameworkElement, so you can place it anywhere in the visual tree, and it obeys the normal layout rules. You tell it what image to display by setting its Source property, as shown in Example 13-27.

Example 13-27. Image element
<Image Source="http://www.interact-sw.co.uk/images/M3/BackOfM3.jpeg" />

Setting the Source property to an absolute URL causes the image to be downloaded and displayed. Alternatively, if you embed an image file in your application as a resource, as described in Chapter 12, you can refer to it with a relative URL, as Example 13-28 illustrates.

Example 13-28. Using an image resource
<Image Source="/MyEmbeddedImage.jpeg" />

The Image element is able to resize the image. The exact behavior depends on your application’s layout. If your layout permits the Image element to size to content, it will show the image at its natural size. (We discussed sizing to content in Chapter 3.) For example, the Canvas panel never imposes a particular size on its children, so the code in Example 13-29 will display the image at its native size.

Example 13-29. Showing an image at its natural size
<Canvas>
  <Image Source="/MyEmbeddedImage.jpeg" />
</Canvas>

However, if your layout provides the Image element with a specific amount of space, by default the bitmap will be scaled to fill that space. A window’s content is constrained by the size of the window, so Example 13-30 will enlarge or reduce the image to fill the window.

Example 13-30. Scaling an image to fill the available space
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
  <Image Source="http://www.interact-sw.co.uk/images/M3/BackOfM3.jpeg" />
</Window>

The default scaling behavior is to use the same scale factor horizontally and vertically. If the available space is the wrong shape for the image, it will be made as large as possible without being too large in either dimension. Figure 13-33 shows the result of Example 13-30, and even though the Image element fills the whole window, the window’s white background is visible above and below when the window is too tall, or to the left and right where the window is too wide.

Uniform stretching
Figure 13-33. Uniform stretching

Tip

Images with a transparency channel are handled correctly—whatever is behind the image is visible through the transparent parts of the bitmap.

If you want the image to fill all of the space even when it is the wrong shape, you can set the element’s Stretch property. This defaults to Uniform, but Fill or UniformToFill will cause the image to fill the full space. These values mean exactly the same as they do for the Shape types—Shape and Image use the same Stretch enumeration type. So, as Figure 13-34 shows, Fill will distort the image if necessary to make it fit, whereas UniformToFill scales uniformly and then crops if required.

Fill (left) and UniformToFill (right)
Figure 13-34. Fill (left) and UniformToFill (right)

The examples we’ve seen so far set the Image element’s Source property with a URL. In fact, the Source property’s type is ImageSource. XAML automatically uses the appropriate type converter to turn the URL into an ImageSource, but when working in code, you will use image source objects directly.

ImageSource

ImageSource is an abstract base class used throughout WPF to represent an image. Not only does the Image element’s Source property use this type, but so do the ImageBrush class and the visual layer’s DrawingContext.DrawImage method, both of which are described later.

Two classes derive from ImageSource: DrawingImage and BitmapSource. DrawingImage has nothing to do with bitmaps—it wraps a resolution-independent drawing object. (Drawings are described later.) This means that elements capable of using an image source can work with either resolution-independent drawings or bitmaps. But because we’re looking at bitmaps right now, BitmapSource is the more interesting class. It too is abstract. Table 13-6 lists the derived concrete bitmap source types.

Table 13-6. BitmapSource types

Type

Usage

BitmapFrame

A single frame from a bitmap file (some file formats support multiple frames).

BitmapImage

Represents a bitmap at a specified URL; this is the type created when specifying a URL in XAML.

CachedBitmap

Wraps around any BitmapSource and caches it.

ColorConvertedBitmap

Wraps around any BitmapSource and converts it from one color space to another.

CroppedBitmap

Wraps around any BitmapSource and presents a cropped version.

FormatConvertedBitmap

Wraps around any BitmapSource and generates a copy with a different pixel format (e.g., grayscale).

RenderTargetBitmap

A bitmap whose contents are generated from a Visual.

TransformedBitmap

Wraps around any BitmapSource and presents a scaled and/or rotated copy.

WriteableBitmap

A bitmap whose contents can be modified at runtime.

BitmapImage is the simplest to use of these sources. You can give it a URL just as you would in XAML, as shown in Example 13-31.

Example 13-31. Using BitmapImage
Image imageElement = new Image(  );
imageElement.Source = new BitmapImage(new Uri(
  "http://www.nasa.gov/images/content/136054main_bm_072004.jpg"));

As you can see from Table 13-6, many of the bitmap source types are wrappers around other bitmap sources. You can chain sources together to perform operations such as rotation and cropping. This chaining model is used because you are often not able to modify the original image in any way—it might be compiled into your application as a resource, or it might live on an external web site. It might seem that the obvious way to handle this would be to load a bitmap and then modify it. However, this is at odds with how images are normally handled. In-memory copies are typically transient—WPF does not cache images unless you explicitly tell it to, either by using CachedBitmap or by setting the CacheOptions property of a BitmapImage.

That’s not to say you can’t use a load-then-modify approach; it’s just that it’s not necessary for cropping, transformation, color conversion, or pixel format conversion. Indeed, the chaining approach offers some advantages. For example, suppose you were writing an application that showed an image and allowed the user to crop it interactively. If you were cropping the image by modifying it, you’d need to keep a copy of the original around just in case the user decided he had cropped a little too much and wanted to go back. If you crop by chaining, say, a BitmapImage to a CroppedBitmap, you never modify the original image, so resetting the cropping is easy. (It’s also more efficient—where possible, WPF avoids generating a copy, and just applies cropping or transformations as it renders.)

Sometimes building or modifying bitmaps at runtime is necessary. For example, maybe you want to do something to the image that you cannot achieve by chaining together the built-in image sources, or perhaps you need to build a brand-new image from scratch. RenderTargetBitmap and WriteableBitmap enable you to construct your own bitmaps either from scratch or by modifying a copy of an existing bitmap.

Creating Bitmaps

RenderTargetBitmap lets you create a new a bitmap from any visual. Example 13-32 renders a red ellipse into a bitmap.

Example 13-32. Using RenderTargetBitmap
RenderTargetBitmap bmp = new RenderTargetBitmap(
    300, 150,     // Dimensions in physical pixels
    300, 300,     // Pixel resolution (dpi)
    PixelFormats.Pbgra32);

Ellipse e = new Ellipse(  );
e.Fill = Brushes.Red;
e.Measure(new Size(96, 48));
e.Arrange(new Rect(0, 0, 96, 48));

bmp.Render(e);

You can choose any resolution you like for the output—in this case, we’re creating a 300 dpi bitmap that’s 1 inch wide and 0.5 inches high. Of course, WPF’s coordinate system is resolution-independent—a device-independent pixel is always 1/96 of an inch regardless of the output resolution, so we make the ellipse’s size 96 × 48 device-independent pixels in order to fill the bitmap.

Warning

Although the RenderTargetBitmap constructor takes a parameter of type PixelFormat, it can create images in the Pbgra32 format only. If you specify anything other than that or PixelFormats.Default, it will throw an exception. Pbrga32 is a 32-bit-per-pixel format, with a premultiplied alpha channel.

Example 13-32 renders just one element, but because you can pass any visual, you are free to pass elements that have children, such as Grid and Canvas, enabling you to render multiple elements. However, there are two things to be aware of:

  • If the visual is not already a visible part of a UI, it is your responsibility to call Measure and Arrange so that it knows how big it needs to be. The generated bitmap will be empty if you fail to do this.

  • If you create a standalone visual with no parent, as Example 13-32 does, controls will not pick up their default styles and will be invisible. Consequently, only primitive elements such as Ellipse or TextBlock will appear. If you want to generate a bitmap of a UI, showing the UI in a real window before passing it to the Render method will fix this.

RenderTargetBitmap lets you build a bitmap out of any combination of WPF visuals. This provides a way to modify existing bitmaps. For example, if you want to overlay a text caption onto a bitmap, you could create a Grid containing an Image element displaying the original image, as well as a TextBlock containing the caption, and pass the Grid to the Render method, as shown in Example 13-33.

Example 13-33. Adding a caption to a bitmap
BitmapImage originalBmp = new BitmapImage(  );
originalBmp.BeginInit(  );
originalBmp.UriSource = new Uri(
   "http://www.interact-sw.co.uk/images/M3/BackOfM3.jpeg");
originalBmp.DownloadCompleted += delegate {
    Grid rootGrid = new Grid(  );
    Image img = new Image();
    img.Source = originalBmp;
    rootGrid.Children.Add(img);
    TextBlock caption = new TextBlock();
    caption.Text = "Ian's car";
    caption.FontSize = 35;
    caption.Foreground = Brushes.White;
    caption.Background = new SolidColorBrush(Color.FromArgb(128, 0, 0, 0));
    caption.VerticalAlignment = VerticalAlignment.Bottom;
    caption.HorizontalAlignment = HorizontalAlignment.Center;
    caption.Margin = new Thickness(5);
    caption.Padding = new Thickness(5);
    caption.TextAlignment = TextAlignment.Center;
    caption.textWrapping = TextWrapping.Wrap;
    rootGrid.Children.Add(caption)

    RenderTargetBitmap bmp = newRenderTargetBitmap(
        originalBmp.PixelWidth, originalBmp.PixelHeight,
        originalBmp.DpiX, originalBmp.DpiY, PixelFormats.Pbgra32);
    rootGrid.Measure(new Size(originalBmp.Width, originalBmp.Height));
    rootGrid.Arrange(new Rect(0, 0, originalBmp.Width, originalBmp.Height));
    bmp.render(rootGrid);
    // bmp now ready for use
    ...
};
originalBmp.EndInit();

This brings to the surface something that was not previously evident: bitmaps are downloaded from the Web in the background. Normally this isn’t a problem—if you connect a BitmapImage directly into an Image or ImageBrush, WPF automatically updates the display once the image is available. However, we’re now trying to build a new image based on the original, so we must wait for the original image to arrive before we start. This is why most of the work is done in the BitmapImage object’s DownloadCompleted event handler.

Example 13-33 does not modify the original bitmap file—in this case, it’s up on a public web server, so there’s no way the code could change it. Instead, the newly created RenderTargetBitmap contains the modified image. Figure 13-35 shows how it looks. (If you want to write the modified image out to disk, we’ll see how to do that shortly.)

Bitmap modified with caption
Figure 13-35. Bitmap modified with caption

RenderTargetBitmap is great if you want to build or modify a bitmap using WPF elements. However, if you want to work with raw pixel data, WriteableBitmap is a better choice. Example 13-34 uses this technique to invert all of the colors in a bitmap to form a negative image—something you could not do with a RenderTargetBitmap.

Example 13-34. Modifying pixels
BitmapImage originalBmp = new BitmapImage(  );
originalBmp.BeginInit(  );
originalBmp.UriSource = new Uri(
   "http://www.interact-sw.co.uk/images/M3/BackOfM3.jpeg");
originalBmp.DownloadCompleted += delegate {

    BitmapSource prgbaSource = new FormatConvertedBitmap(originalBmp,
                                     PixelFormats.Pbgra32, null, 0);
    WriteableBitmap bmp = new WriteableBitmap(prgbaSource);
    int w = bmp.PixelWidth;
    int h = bmp.PixelHeight;
    int[] pixelData = new int[w * h];
    int widthInBytes = 4 * w;

    bmp.CopyPixels(pixelData, widthInBytes, 0);
    for (int i = 0; i < pixelData.Length; ++i) {
        pixelData[i] ^= 0x00ffffff;
    }
    bmp.WritePixels(new Int32Rect(0, 0, w, h),
        pixelData, widthInBytes, 0);


    // bmp now ready for use
    ...
};

originalBmp.EndInit(  );

As before, the code waits until the image has been downloaded before proceeding. Once the image is available, the first thing the code does is ensure that the pixel data will be available in the format our code expects by wrapping the original in a FormatConvertedBitmap. If the original image uses a different pixel format, this will convert it for us.

Next, we load the results into a WriteableBitmap. We read out the pixel values using CopyPixels. CopyPixels is not unique to WriteableBitmap—you can read pixel values from any bitmap source—but only WriteableBitmap offers a WritePixels method to change the image. After we’ve flipped the bits of the red, green, and blue channels of the pixel, we use WritePixels to put these modified pixels back into the bitmap. Finally, we can use the WriteableBitmap as an image source—for example, we could set it as an Image element’s Source property. Figure 13-36 shows the resulting negative image.

Bitmap with inverted colors
Figure 13-36. Bitmap with inverted colors

If you are generating bitmaps with either WriteableBitmap or RenderTargetBitmap, you may not want to put the results on-screen—you might want to write them out to disk. You can do this with a bitmap encoder.

Bitmap Encoders and Decoders

A bitmap encoder is a class that knows how to generate a bitmap stream in a particular format. WPF provides encoders for all the image formats listed earlier. Encoders are named after their format (e.g., PngBitmapEncoder, JpegBitmapEncoder, etc.).

Example 13-35 shows how to write a bitmap out to disk as a JPEG file. This function works with any BitmapSource.

Example 13-35. Creating a JPEG file
static void WriteJpeg(string fileName, int quality, BitmapSource bmp) {

    JpegBitmapEncoder encoder = new JpegBitmapEncoder(  );
    BitmapFrame outputFrame = BitmapFrame.Create(bmp);
    encoder.Frames.Add(outputFrame);
    encoder.QualityLevel = quality;

    using (FileStream file = File.OpenWrite(fileName)) {
        encoder.Save(file);
    }
}

Decoders work in the opposite direction—they know how to read bitmap streams of a particular format. Decoders are used implicitly whenever you load a bitmap stream into a BitmapImage, but you can also use them explicitly. This is necessary if you wish to access bitmap metadata or retrieve all the frames in a multiframe image file.

Example 13-36 shows how to load a JPEG image with a decoder to discover the type of camera used to take the image.

Example 13-36. Reading bitmap metadata
static string GetCamera(string myJpegPath) {
    JpegBitmapDecoder decoder = new JpegBitmapDecoder(new Uri(myJpegPath),
        BitmapCreateOptions.None, BitmapCacheOption.None);
    BitmapMetadata bmpData = (BitmapMetadata) decoder.Frames[0].Metadata;
    return bmpData.CameraModel;
}

Notice that both the encoder and the decoder have a property called Frames to represent the frames of the image. For single-frame formats, this cannot contain more than one frame, but an animated GIF would contain multiple frames.

Warning

Metadata is returned only at the level of individual frames. The decoder classes all offer a Metadata property, but it is always null for all of the decoders that ship as part of the first release of WPF. Use the Metadata property of the frame instead.

There is one last bitmap feature we will examine. It is a little different from the rest, because it allows bitmap processing to be applied to any part of the UI, not just bitmaps.

Bitmap Effects

All user interface elements have a BitmapEffects property. You can use it to apply a visual effect to the element and all of its children. All of these effects use bitmap processing algorithms, hence the name. Example 13-37 applies a BlurBitmapEffect to one of its StackPanel elements.

Example 13-37. Bitmap effect
<StackPanel Orientation="Horizontal">
  <StackPanel Orientation="Vertical">
    <TextBlock Text="Abcdef" TextAlignment="Center" FontWeight="Bold" />
    <RadioButton Content="Better in position 1?" GroupName="r" />
  </StackPanel>
  <StackPanel Orientation="Vertical" Margin="10,0">
    <StackPanel.BitmapEffect>
      <BlurBitmapEffect Radius="1" />
    </StackPanel.BitmapEffect>
    <TextBlock Text="Abcdef" TextAlignment="Center" FontWeight="Bold" />
    <RadioButton Content="Or position 2?" GroupName="r" />
  </StackPanel>
</StackPanel>

As you can see in Figure 13-37, the righthand side is out of focus, thanks to the blur effect. Despite this, it’s still live—as you can see, the radio button on the righthand side has been selected. Even if you made the panel completely illegible by cranking up the blur’s Radius property to 10, the controls would continue to function because WPF’s input handling completely ignores bitmap effects.

BlurBitmapEffect
Figure 13-37. BlurBitmapEffect

Table 13-7 lists all of the built-in effects. It is possible to write custom effects, but this requires an unmanaged COM component to be written, and is beyond the scope of this book.[91]

Table 13-7. BitmapEffects

Type

Usage

BevelBitmapEffect

Creates a pseudo-3D relief effect at the edges of the content

BitmapEffectGroup

Allows multiple effects to be used on a single element

BlurBitmapEffect

Makes the image look out of focus

DropShadowBitmapEffect

Draws a soft shadow around the outline of the content

EmbossBitmapEffect

Performs a bump mapping algorithm to apply a pseudo-3D relief across the whole of the content

OuterGlowBitmapEffect

Adds a soft halo around the outline of the content

Warning

Bitmap effects are expensive. Use them sparingly.

To apply a bitmap effect, WPF must first render the content to which the effect will be applied, and then run the bitmap effect algorithm over the rendered content in order to generate the final results. This has two significant performance implications. First, it involves the creation of an intermediate render target—a block of memory in which to build the rendered content prior to processing. This increases memory usage. Second, many bitmap effects run in software. This in turn means that the content to which the effect is applied will be rendered in software.

Tempting though it may be, applying a bitmap effect to a large region (e.g., the whole window) is a very bad idea. It disables hardware rendering for the whole region, which is likely to reduce performance drastically.

So far, we’ve seen how to use bitmaps and define simple shapes, but we have been rather unadventurous in our choice of fills and outlines for our shapes. We have used nothing but standard named colors and simple outline styles. And although we’ve seen how to render bitmaps as standalone rectangles of content, we’ve not yet seen how we can combine bitmaps with shapes. So, it’s time to look at how WPF’s brush and pen classes enable more interesting drawing styles.

Brushes and Pens

To draw a shape on the screen, WPF needs to know how you would like that shape to be colored in and how its outline should be drawn. WPF provides several Brush types supporting a variety of painting styles. The Pen class extends this to provide information about stroke thickness, dash patterns, and the like.

In this section, we will look at all of the available brush types and the Pen class. However, because all brushes and pens are ultimately about deciding what colors to use where and how they are combined, we must first look at how colors are represented.

Color

WPF uses the Color structure in the System.Windows.Media namespace to represent a color. If you have worked with Windows Forms, ASP.NET, or GDI+ in the past, note that this is not the same structure as those technologies use. They use the Color structure in the System.Drawing namespace. WPF introduces this new Color structure because it can work with floating-point color values, enabling much higher color precision and greater flexibility.

The Color structure uses four numbers or “channels” to represent a color. These channels are red, green, blue, and alpha. Red, green, and blue channels are the traditional way of representing color in computer graphics. (This is because color screens work by adding these three primary colors together.) A value of 0 indicates that the color component is not present at all; 0 on all three channels corresponds to black. The alpha channel represents the level of opacity—a Color can be opaque, completely transparent, or anywhere in between these two extremes. WPF’s composition engine allows anything to be drawn with any level of transparency. A value of 0 is used to represent complete transparency, and 1 means completely opaque.

Windows has traditionally used 24 bits of color information (8 bits per channel) to represent “true” or “full” color, and 32 bits for full color with transparency. This is just about sufficient for the average computer screen. The color and brightness range of most computer displays means that 24 bits of color has always been adequate (albeit barely) for most purposes, although a sufficiently good screen can reveal the limitations. However, for many imaging applications, this is not sufficient. For example, film can accommodate a much wider range of brightness than a computer screen, so 24-bit color is simply not good enough for graphics work with film as its output medium. The same is true for many medical imaging applications. And, even for computer or video images, 24-bit color can cause problems—if images are going through many stages of processing, these can amplify the limitations of 24-bit source material.

WPF therefore supports a much higher level of detail in its representation of color. Each color channel uses 16 bits instead of 8. The Color structure still supports the use of 8-bit channels where required, because a lot of imaging software depends on such a representation. Color exposes the 8-bit channels through the A, R, G, and B properties, which accept values in the range of 0–255.[92] The higher definition representations are available through the ScA, ScR, ScG, and ScB properties, which present the channels as single-precision floating-point values ranging from 0–1.

Tip

The “Sc” in the ScA, ScR, ScG, and ScB properties refers to the fact that these support the standard “Extended RGB colour space—scRGB” color space defined in the IEC 61966-2-2 specification. (This is an international specification, hence the u in colour.) Strangely, the sc is not officially short for anything. During its development, the scRGB spec went through various names. As is often the way with standards committees, various parties had objections to the names that made any sense, so they settled on something unobjectionable but meaningless.

Various post hoc theories as to what sc might stand for have been developed. One is that it is an abbreviation of specular, suggesting the high-headroom support offered by the out-of-gamut capability. Another theory is that it could be short for standard compositing, to indicate that the color space is designed for compositing rather than for physical devices. This same thinking informed the theory that it is short for scene referred (although one member of the standards committee maintains that this last theory is absolutely wrong).

The Color class also allows color values to go out of range. This can lead to counterintuitive color values where a particular color channel may be negative, or more than 100 percent. Even though this may seem to make no sense, it can be useful to accommodate excursions outside of the 0–1 range if you are performing several image processing steps. For example, suppose you want to increase the brightness but decrease the contrast of an image—the first step might take the brightness over 100 percent, but the second step could bring it back into range. As long as your final output values are within the 0–1 range, it doesn’t necessarily matter where they went during image processing. However, if a color system is unable to accommodate out-of-range values, it must clip all colors to be within the valid range at every single stage. This range limiting can result in a degradation of image quality.

There is also a Colors class. This provides a set of standard named colors, with all the old favorites such as PapayaWhip, BurlyWood, LightGoldenrodYellow, and Brown.

You cannot use a Color directly for drawing. To draw, you need either a Brush or a Pen.

SolidColorBrush

SolidColorBrush is the simplest brush. It uses one color across the whole area being painted. It has just one property, Color. Note that this color is allowed to use transparency, despite what the word Solid suggests.

We have already been using the SolidColorBrush extensively even though we have not yet referred to it by name. This is because WPF creates this kind of brush if you specify the name of a color in markup—if you work mostly with XAML, you very rarely need to specify that you require a SolidColorBrush, because you’ll get one by default. (The only reason you would normally specify it in full is if you want to use data binding with the brush’s properties.) Consider this example:

<Rectangle Fill="Yellow" Width="100" Height="20" />

The XAML compiler will recognize Yellow as one of the standard named colors from the Colors class, and will supply a suitable SolidColorBrush. (See Appendix A for more information on how XAML maps from strings to property values.) It does not need to create the brush, because there is a Brushes class, providing a set of brushes for each of the named colors in Colors.

You will also be provided with a SolidColorBrush if your markup uses a numeric color value. Example 13-38 shows various examples of numeric colors. All but the last two begin with a # symbol and contain hexadecimal digits. A three-digit number is taken to be one digit each of red, green, and blue. A four-digit number is interpreted as alpha, red, green, and blue. These are compact formats providing just 4 bits per channel. Six- or eight-digit numbers allow 8 bits per channel for RGB or ARGB, respectively. To exploit the full accuracy of scRGB, you provide a string that starts with “sc#” followed by a space, and then four comma-separated decimal numbers representing the A, R, G, and B values. Finally, if the string starts with ContextColor, you can define a color that refers to a specific International Color Consortium (ICC) or Image Color Manager (ICM) color profile file.

Example 13-38. Numeric color values
<Rectangle Fill="#8f8"      Width="100" Height="20" />
<Rectangle Fill="#1168ff"   Width="50" Height="40" />
<Rectangle Fill="#8ff0"     Width="130" Height="10" />
<Rectangle Fill="#72ff8890" Width="70" Height="30" />
<Rectangle Fill="sc# 0.8,0.1442,0.429,0.94" Width="10" Height="20" />
<Rectangle Fill="ContextColor
file://C:/Windows/System32/spool/drivers/color/sRGB%20Color%20Space%20Profile.icm
 1.0,0.0,1.0,0.0" Width="10" Height="20" />

The SolidColorBrush is lightweight and straightforward. However, it makes for fairly flat-looking visuals. WPF offers some more interesting brushes if you want to make your user interface look a little more appealing.

LinearGradientBrush

With a LinearGradientBrush, the painted area transitions from one color to another, or even through a sequence of colors. Figure 13-38 shows a simple example.

LinearGradientBrush
Figure 13-38. LinearGradientBrush

This brush fades from black to white, starting at the top-left corner and finishing at the bottom-right corner. The fade always runs in a straight line—this brush cannot do curved transitions, hence the name “linear.” Example 13-39 shows the markup for Figure 13-38.

Example 13-39. Using a LinearGradientBrush
<Rectangle Width="80" Height="60">
  <Rectangle.Fill>
    <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
      <GradientStop Color="Black" Offset="0" />
      <GradientStop Color="White" Offset="1" />
    </LinearGradientBrush>
  </Rectangle.Fill>
</Rectangle>

The StartPoint and EndPoint properties indicate where the color transition begins and ends. These coordinates are relative to the area being filled, so 0,0 is the top left and 1,1 is the bottom right, as shown in Figure 13-39. (Note that if the brush is painting an area that is narrow or wide, the coordinate system is squashed accordingly.) You are allowed to put the StartPoint and EndPoint outside of the rectangle. For example, you could change the StartPoint of Figure 13-39 to −1,0. This would mean that only half of the fill’s color range would be used. This might seem pointless—setting the first gradient stop’s color to a shade of gray would have the same effect. However, sometimes it’s easier to tweak the look of a fill by adjusting the end points rather than by adjusting the colors.

Fill coordinate system
Figure 13-39. Fill coordinate system

Each GradientStop has an Offset property as well as a Color. This enables the fill to pass through multiple colors. Example 13-40 shows a LinearGradientBrush with multiple colors.

Example 13-40. Multiple gradient stops
<Rectangle Width="80" Height="60">
  <Rectangle.Fill>
    <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
      <GradientStop Color="Black" Offset="0" />
      <GradientStop Color="Orange" Offset="0.2" />
      <GradientStop Color="Red" Offset="0.4" />
      <GradientStop Color="Black" Offset="0.6" />
      <GradientStop Color="Blue" Offset="0.8" />
      <GradientStop Color="White" Offset="1" />
    </LinearGradientBrush>
  </Rectangle.Fill>
</Rectangle>

Figure 13-40 shows the result.

Multiple gradient stops ()
Figure 13-40. Multiple gradient stops (Figure F-22)

LinearGradientBrush is often used to provide a feeling of depth to a user interface. Example 13-41 shows a typical example. It uses just two shapes—a pair of rounded Rectangle elements. (The Grid doesn’t contribute directly to the appearance. It is there to make it easy to resize the graphic—changing the grid’s Width and Height will cause both rectangles to resize appropriately.) The second rectangle’s gradient fill fades from a partially transparent shade of white to a completely transparent color, which provides an interesting visual effect.

Example 13-41. Simulating lighting effects with linear fills
<Grid Width="80" Height="26">
  <Grid.RowDefinitions>
    <RowDefinition Height="2*" />
    <RowDefinition Height="*" />
  </Grid.RowDefinitions>

  <Rectangle Grid.RowSpan="2" RadiusX="13" RadiusY="13">
    <Rectangle.Fill>
      <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
        <GradientStop Color="Green" Offset="0" />
        <GradientStop Color="DarkGreen" Offset="1" />
      </LinearGradientBrush>
    </Rectangle.Fill>
    <Rectangle.Stroke>
      <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
        <GradientStop Color="Black" Offset="0" />
        <GradientStop Color="LightGray" Offset="1" />
      </LinearGradientBrush>
    </Rectangle.Stroke>
  </Rectangle>

  <Rectangle Margin="3,2" RadiusX="8" RadiusY="12">
    <Rectangle.Fill>
      <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
        <GradientStop Color="#dfff" Offset="0" />
        <GradientStop Color="#0fff" Offset="1" />
      </LinearGradientBrush>
    </Rectangle.Fill>
  </Rectangle>
</Grid>

Figure 13-41 shows the result. This is an extremely simple graphic, containing just two shapes. The use of gradient fills has added an impression of depth that these shapes would otherwise not have conveyed.

Simple lighting effects with linear fills ()
Figure 13-41. Simple lighting effects with linear fills (Figure F-23)

RadialGradientBrush

RadialGradientBrush is very similar to LinearGradientBrush. Both transition through a series of colors. But whereas LinearGradientBrush paints these transitions in a straight line, the RadialGradientBrush fades from a starting point out to an elliptical boundary. This opens more opportunities for making your user interface appear less flat. Example 13-42 shows an example.

Example 13-42. Using a RadialGradientBrush
<Rectangle Width="200" Height="150">
  <Rectangle.Fill>
    <RadialGradientBrush Center="0.45,0.5" RadiusX="0.3" RadiusY="0.5"
                         GradientOrigin="0.25,0.4">
      <GradientStop Color="White" Offset="0" />
      <GradientStop Color="DarkBlue" Offset="1" />
    </RadialGradientBrush>
  </Rectangle.Fill>
</Rectangle>

The RadialGradientBrush takes a list of GradientStop objects to determine the colors that the fill runs through, just like LinearGradientBrush. This example uses the RadiusX and RadiusY properties to determine the size of the elliptical boundary, and the Center property to set the position of the ellipse. The values chosen here make the fill boundary fit entirely into the shape, as Figure 13-42 shows. The area of the shape that falls outside of this boundary is filled with the color of the final GradientStop. Notice that the focal point of the fill is to the left. This is because the GradientOrigin has been set. (By default, the focal point is in the center of the ellipse.)

Simple radial fill ()
Figure 13-42. Simple radial fill (Figure F-24)

Example 13-42 makes it easy to see the effects of the properties of the RadialGradientBrush, but it’s not a very exciting example. Example 13-43 shows something a little more adventurous. It is similar to Example 13-41—both use a small number of shapes with gradient fills to convey a feeling of depth and reflection—but this example uses radial fills as well as a linear fill.

Example 13-43. Radial gradient fills
<Grid Width="16" Height="16" Margin="0,0,5,0" >
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="*" />
    <ColumnDefinition Width="10*" />
    <ColumnDefinition Width="*" />
  </Grid.ColumnDefinitions>

  <Grid.RowDefinitions>
    <RowDefinition Height="*" />
    <RowDefinition Height="20*" />
    <RowDefinition Height="6*" />
  </Grid.RowDefinitions>


  <Ellipse Grid.RowSpan="3" Grid.ColumnSpan="3" Margin="0.5">
    <Ellipse.Fill>
      <RadialGradientBrush Center="0.5,0.9" GradientOrigin="0.5,0.9"
                           RadiusX="0.7" RadiusY="0.5">
        <GradientStop Color="PaleGreen" Offset="0" />
        <GradientStop Color="Green" Offset="1" />
      </RadialGradientBrush>
    </Ellipse.Fill>
  </Ellipse>
  <Ellipse Grid.Row="1" Grid.Column="1">
    <Ellipse.Fill>
      <RadialGradientBrush Center="0.5,0.1" GradientOrigin="0.5,0.1"
                           RadiusX="0.7" RadiusY="0.5">
        <GradientStop Color="#efff" Offset="0" />
        <GradientStop Color="Transparent" Offset="1" />
      </RadialGradientBrush>
    </Ellipse.Fill>
  </Ellipse>
  <Ellipse Grid.RowSpan="3" Grid.ColumnSpan="3">
    <Ellipse.Stroke>
      <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
        <GradientStop Color="Gray" Offset="0" />
        <GradientStop Color="LightGray" Offset="1" />
      </LinearGradientBrush>
    </Ellipse.Stroke>
  </Ellipse>
</Grid>

This time, three ellipses have been used. Two have RadialGradientBrush fills, and one has a LinearGradientBrush stroke. The fill in the first ellipse creates the glow at the bottom of the drawing. The second adds the reflective highlight at the top. The third draws a bezel around the outside. Figure 13-43 shows the result. The radial fills suggest a curved surface and give the graphic a slightly translucent look.

Radial fills ()
Figure 13-43. Radial fills (Figure F-25)

ImageBrush, DrawingBrush, and VisualBrush

The ability to fill shapes with a pattern or image of some kind is often useful. WPF provides three brushes that allow us to paint shapes with whatever graphics we choose. The ImageBrush lets us paint with a bitmap. With DrawingBrush, we use a scalable drawing. VisualBrush allows us to use any UI element as the brush image—we can in effect use one piece of our user interface to paint another.

All of these brushes have a certain amount in common, so they all derive from the same base class, TileBrush.

TileBrush

ImageBrush, DrawingBrush, and VisualBrush all paint using some form of source picture. Their base class, TileBrush, decides how to stretch the source image to fill the available space, whether to repeat (tile) the image, and how to position the image within the shape. TileBrush is an abstract base class, so you cannot use it directly. It exists to define the features common to the ImageBrush, DrawingBrush, and VisualBrush.

Figure 13-44 shows the default TileBrush behavior. This figure shows three rectangles so that you can see what happens when the brush is made narrow or wide, as well as how it looks when the brush shape matches the target area shape. All three are rectangles painted with an ImageBrush specifying just the image.

Default stretching and placement (Stretch.Fill)
Figure 13-44. Default stretching and placement (Stretch.Fill)

The stretching behavior would be exactly the same for any of the tile brushes—we are using ImageBrush just as an example. Indeed, all the features discussed in this section apply to any TileBrush. Example 13-44 shows the markup used for each rectangle in Figure 13-44.

Example 13-44. Using an ImageBrush
<Rectangle>
  <Rectangle.Fill>
    <ImageBrush ImageSource="ImagesMoggie.jpg" />
  </Rectangle.Fill>
</Rectangle>

Because this specifies nothing more than which image to display, it gets the default TileBrush behavior: the brush has stretched the source image to fill the available space. We can change this behavior by modifying the brush’s Stretch property. It defaults to Fill, but we can show the image at its native size by specifying None, as Example 13-45 shows.

Example 13-45. Specifying a Stretch of None
<Rectangle>
  <Rectangle.Fill>
    <ImageBrush ImageSource="ImagesMoggie.jpg" Stretch="None" />
  </Rectangle.Fill>
</Rectangle>

The None stretch mode preserves the aspect ratio, but if the image is too large, it will simply be cropped to fit the space available, as Figure 13-45 shows.

Stretch.None
Figure 13-45. Stretch.None

For displaying images, you may want to stretch the image to match the available space without distorting the aspect ratio. TileBrush supports this with the Uniform stretch mode, shown in Figure 13-46. This scales the source image so that it fits entirely within the space available.

Stretch.Uniform
Figure 13-46. Stretch.Uniform

The Uniform stretch mode typically results in the image being made smaller than the area being filled, leaving the remainder of the space transparent. Alternatively, you can scale the image so that it completely fills the space available while preserving the aspect ratio, cropping in one dimension if necessary. The UniformToFill stretch mode does this, and it is shown in Figure 13-47.

Stretch.UniformToFill
Figure 13-47. Stretch.UniformToFill

UniformToFill is most appropriate if you are filling an area with some nonrepeating textured pattern, because it guarantees to paint the whole area. It is probably less appropriate if your goal is simply to display a picture—as Figure 13-47 shows, this stretch mode will crop images where necessary. If you want to show the whole picture, Uniform is the best choice.

All of the stretch modes except for Fill present an extra question: how should the image be positioned? With None and UniformToFill, cropping occurs, so WPF needs to decide which part of the image to show. With Uniform, the image may be smaller than the space being filled, so WPF needs to decide where to put it.

Images are centered by default. In the examples where the image has been cropped (Figure 13-45 and Figure 13-47) the most central parts are shown. In the case of Uniform, where the image is smaller than the area being painted, it has been placed in the middle of that area (Figure 13-46). You can change this with the AlignmentX and AlignmentY properties. You can set these to Left, Middle, or Right, and Top, Middle, or Bottom, respectively. Example 13-46 shows the UniformToFill stretch mode again, but this time with alignments of Left and Bottom. Figure 13-48 shows the results.

Example 13-46. Specifying a Stretch and alignment
<Rectangle>
  <Rectangle.Fill>
    <ImageBrush ImageSource="ImagesMoggie.jpg" Stretch="UniformToFill"
                AlignmentX="Left" AlignmentY="Bottom" />
  </Rectangle.Fill>
</Rectangle>
Stretch.UniformToFill, bottom-left-aligned
Figure 13-48. Stretch.UniformToFill, bottom-left-aligned

The stretch and alignment properties are convenient to use, but they do not allow you to focus on any arbitrary part of the image, or choose specific scale factors. TileBrush supports these features through the Viewbox, Viewport, ViewboxUnits, and ViewportUnits properties.

The Viewbox property chooses the portion of the image to be displayed. By default, this property is set to encompass the whole image, but you can change it to focus on a particular part. Figure 13-49 shows the UniformToFill stretch mode, but with a Viewbox set to zoom in on the front of the car.

Stretch.UniformToFill with Viewbox
Figure 13-49. Stretch.UniformToFill with Viewbox

As Example 13-47 shows, the Viewbox is specified as four numbers. The first two are the coordinates of the upper-lefthand corner of the Viewbox; the second two are the width and height of the box. By default, coordinates of 1,1 represent the entire source image.

Example 13-47. Specifying a Viewbox
<ImageBrush Stretch="UniformToFill" Viewbox="0.75,0.42,0.25,0.34"
            ImageSource="ImagesMoggie.jpg" />

Sometimes it can be more convenient to work in the coordinates of the source image itself. As Example 13-48 shows, you can do this by setting the ViewboxUnits property to Absolute. (It defaults to RelativeToBoundingBox.)

Example 13-48. Viewbox with absolute units
<ImageBrush Stretch="UniformToFill"
            ViewboxUnits="Absolute" Viewbox="593,250,200,200"
            ImageSource="ImagesMoggie.jpg" />

In this case, because an ImageBrush is being used, these are coordinates in the source bitmap. In the case of a DrawingBrush or VisualBrush, the Viewbox would use the coordinate system of the source drawing.

Although the last two examples chose which portion of the source image to focus on by specifying a Viewbox, they still relied on the Stretch property to choose how to size and position the output. If you want more precise control, you can use Viewport to choose exactly where the image should end up in the brush.

Figure 13-50 illustrates the relationship between Viewbox and Viewport. On the left is the source image—a bitmap, in this case, but it could also be a drawing or visual tree. The Viewbox specifies an area of this source image. On the right is the brush. The Viewport specifies an area within this brush. WPF will scale and position the source image so that the area specified in Viewbox ends up being painted into the area specified by Viewport.

Viewbox and Viewport
Figure 13-50. Viewbox and Viewport

As well as indicating where the contents of the Viewbox end up, the Viewport specifies the extent of the brush; it will be clipped to the size of the Viewport. Example 13-49 shows Viewport and Viewbox settings that correspond to the areas highlighted in Figure 13-50.

Example 13-49. Using Viewbox and Viewport
<ImageBrush ViewboxUnits="Absolute" Viewbox="380,285,308,243"
            Viewport="0.1,0.321,0.7, 0.557"
            ImageSource="ImagesMoggie.jpg" />

Like the Viewbox, by default the Viewport coordinates range from 0–1. The position 0,0 is the top left of the brush, and 1,1 is the bottom right. This means that the part of the image shown by the brush will always be the same, regardless of the brush size or shape. This results in a distorting behavior similar to the default StretchMode of Fill, as shown in Figure 13-51. (In fact, the Fill stretch mode is equivalent to setting the Viewbox and Viewport to be 0,0,1,1.)

Viewbox and Viewport
Figure 13-51. Viewbox and Viewport

As with the Viewbox, you can specify different units for the Viewport. The ViewportUnits property defaults to RelativeToBoundingBox, but if you change it to Absolute, the Viewport is measured using output coordinates. Note that setting the Viewport in absolute units means the image will no longer scale as the brush resizes.

In several of the preceding examples, the source image has not completely filled the area of the brush. By default, the brush is transparent in the remaining space. However, if you have specified a Viewport, you can choose other behaviors for the spare space with the TileMode property. The default is None, but if you specify Tile, as Example 13-50 does, the image will be repeated to fill the space available.

Example 13-50. Specifying a Stretch and a TileMode
<Rectangle>
  <Rectangle.Fill>
    <ImageBrush ImageSource="ImagesMoggie.jpg"
                Viewport="0,0,100,100" ViewportUnits="Absolute"
                TileMode="Tile" />
  </Rectangle.Fill>
</Rectangle>

Figure 13-52 shows the effect of the Tile tile mode. There is one potential problem with tiling. It can often be very obvious where each repeated tile starts. If your goal is simply to fill in an area with a texture, these discontinuities can jar somewhat. To alleviate this, TileBrush supports three other modes of tiling: FlipX, FlipY, and FlipXY. These mirror alternate images as shown in Figure 13-53. Although mirroring can reduce the discontinuity between tiles, for some source images it can change the look of the brush quite substantially. Flipping is typically better suited to more uniform texture-like images than pictures.

Tiling
Figure 13-52. Tiling
FlipXY tiling
Figure 13-53. FlipXY tiling

Remember that all of this scaling and positioning functionality is common to all of the brushes derived from TileBrush. However, some features are specific to the individual brush types, so we will now look at each in turn.

ImageBrush

ImageBrush paints areas of the screen using a bitmap. The ImageBrush was used to create all of the pictures in the preceding section. This brush is straightforward—you simply need to tell it what bitmap to use with the ImageSource property, as Example 13-51 shows.

Example 13-51. Using an ImageBrush
<Rectangle>
  <Rectangle.Fill>
    <ImageBrush ImageSource="ImagesMoggie.jpg" />
  </Rectangle.Fill>
</Rectangle>

To make a bitmap file available to the ImageBrush, you can add one to your project in Visual Studio. The file in Example 13-51 was in a subdirectory of the project called Images, and was built into the project as a resource. To do this, select the bitmap file in Visual Studio’s Solution Explorer and then, in the Properties panel, make sure the Build Action property is set to Resource. This embeds the bitmap into the executable, enabling the ImageBrush to find it at runtime. (See Chapter 12 for more information on how binary resources are managed.) Alternatively, you can specify an absolute URL for this property—you could, for example, display an image from a web site.[93]

Tip

ImageBrush is quite happy to deal with images with a transparency channel (also known as an alpha channel). Not all image formats support partial transparency, but some—such as the PNG, WMP, and BMP formats—can. (And, to a lesser extent, GIF. It supports only fully transparent or fully opaque pixels. This is effectively a 1-bit alpha channel.) Where an alpha channel is present, the ImageBrush will honor it.

DrawingBrush

The ImageBrush is convenient if you have a bitmap you need to paint with. However, bitmaps do not fit in well with resolution independence. The ImageBrush will scale bitmaps correctly for your screen’s resolution, but bitmaps tend to become blurred when scaled. DrawingBrush does not suffer from this problem, because you usually provide a scalable vector image as its source. This enables a DrawingBrush to remain clear and sharp at any size and resolution.

The vector image is represented by a Drawing object. This is an abstract base class. You can draw shapes with a GeometryDrawing—this allows you to construct drawings using all of the same geometry elements supported by Path. You can also use bitmaps and video with ImageDrawing and VideoDrawing. Text is supported with GlyphRunDrawing. Finally, you can combine these using the DrawingGroup.

Even if you use nothing but shapes, you will still probably want to group the shapes with a DrawingGroup. Each GeometryDrawing is effectively equivalent to a single Path, so if you want to draw using different pens and brushes, or if you want your shapes to overlap rather than combine, you will need to use multiple GeometryDrawing elements.

Example 13-52 shows a Rectangle that uses a DrawingBrush for its Fill. This brush paints the same visuals seen earlier in Figure 13-41. Because each rectangular element that makes up the drawing uses different linear gradient fills, they both get their own GeometryDrawing, nested inside a DrawingGroup.

Example 13-52. Using DrawingBrush
<Rectangle Width="80" Height="30">
  <Rectangle.Fill>
    <DrawingBrush>
      <DrawingBrush.Drawing>
        <DrawingGroup>
          <DrawingGroup.Children>
            <GeometryDrawing>
              <GeometryDrawing.Brush>
                <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                  <GradientStop Color="Green" Offset="0" />
                  <GradientStop Color="DarkGreen" Offset="1" />
                </LinearGradientBrush>
              </GeometryDrawing.Brush>

              <GeometryDrawing.Pen>
                <Pen Thickness="0.02">
                  <Pen.Brush>
                    <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                      <GradientStop Color="Black" Offset="0" />
                      <GradientStop Color="LightGray" Offset="1" />
                    </LinearGradientBrush>
                  </Pen.Brush>
                </Pen>
              </GeometryDrawing.Pen>
              <GeometryDrawing.Geometry>
                <RectangleGeometry RadiusX="0.2" RadiusY="0.5"
                                   Rect="0.02,0.02,0.96,0.96" />
              </GeometryDrawing.Geometry>
            </GeometryDrawing>

            <GeometryDrawing>
              <GeometryDrawing.Brush>
                <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                  <GradientStop Color="#dfff" Offset="0" />
                  <GradientStop Color="#0fff" Offset="1" />
                </LinearGradientBrush>
              </GeometryDrawing.Brush>
              <GeometryDrawing.Geometry>
                <RectangleGeometry RadiusX="0.1" RadiusY="0.5"
                                   Rect="0.1,0.07,0.8,0.5" />
              </GeometryDrawing.Geometry>
            </GeometryDrawing>
          </DrawingGroup.Children>
        </DrawingGroup>
      </DrawingBrush.Drawing>
    </DrawingBrush>
  </Rectangle.Fill>
</Rectangle>

With a DrawingBrush, the Viewbox defaults to 0,0,1,1. All of the coordinates and sizes in Example 13-52 are relative to this coordinate system. If you would prefer to work with coordinates over a wider range, you can simply set the Viewbox to the range you require, and the ViewboxUnits to Absolute. We already saw how to use the Viewbox in Example 13-47. The only difference with DrawingBrush is that you’re using it to indicate an area of the drawing, rather than a bitmap.

Note that we can use the Viewbox to focus on some subsection of the picture, just as we did earlier with the ImageBrush. We can modify the DrawingBrush in Example 13-52 to use a smaller Viewbox, as shown in Example 13-53.

Example 13-53. Viewbox and DrawingBrush
<DrawingBrush Viewbox="0.5,0,0.5,0.25">

The result of this is that most of the drawing is now outside of the Viewbox, so the brush shows only a part of the whole drawing, as Figure 13-54 shows.

DrawingBrush with small Viewbox
Figure 13-54. DrawingBrush with small Viewbox

DrawingBrush is very powerful, as it lets you use more or less any graphics you like as a brush, and because it is vector-based, the results remain crisp at any scale. It does have one drawback if you are using it from markup, though: it is somewhat cumbersome to use from XAML. Consider that Example 13-52 produces the same appearance as Example 13-41, but these examples are 48 lines long and 30 lines long, respectively.

The DrawingBrush is much more verbose because it requires us to work with geometry objects rather than higher-level constructs such as the Grid or Rectangle used in Example 13-41. (Note that this problem is less acute when using this brush from code, where the higher-level objects are not much more convenient to use than geometries. The verbosity is really only a XAML issue.) Moreover, higher-level features such as the ability to exploit layout or controls are not available in a DrawingBrush. Fortunately, VisualBrush allows us to paint with these higher-level elements.

VisualBrush

The VisualBrush can paint with the contents of any element derived from Visual. Because Visual is the base class of all WPF user interface elements, this means that in practice, you can plug any markup you like into a VisualBrush. The brush is “live” in that if the brush’s source visual changes, anything painted with the brush will automatically update.

Example 13-54 shows a Rectangle filled using a VisualBrush. The brush’s visuals have been copied directly from Example 13-41, resulting in a much simpler brush than the equivalent DrawingBrush. (The results look exactly the same as Figure 13-41—the whole point of the VisualBrush is that it paints areas to look just like the visuals it wraps.)

Example 13-54. Using a VisualBrush
<Rectangle Width="80" Height="30">
  <Rectangle.Fill>
    <VisualBrush>
      <VisualBrush.Visual>
        <Grid Width="80" Height="26">
          <Grid.RowDefinitions>
            <RowDefinition Height="2*" />
            <RowDefinition Height="*" />
          </Grid.RowDefinitions>
          <Rectangle Grid.RowSpan="2" RadiusX="13" RadiusY="13">
            <Rectangle.Fill>
              <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                <GradientStop Color="Green" Offset="0" />
                <GradientStop Color="DarkGreen" Offset="1" />
              </LinearGradientBrush>
            </Rectangle.Fill>
            <Rectangle.Stroke>
              <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                <GradientStop Color="Black" Offset="0" />
                <GradientStop Color="LightGray" Offset="1" />
              </LinearGradientBrush>
            </Rectangle.Stroke>
          </Rectangle>

          <Rectangle Margin="3,2" RadiusX="8" RadiusY="12">
            <Rectangle.Fill>
              <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                <GradientStop Color="#dfff" Offset="0" />
                <GradientStop Color="#0fff" Offset="1" />
              </LinearGradientBrush>
            </Rectangle.Fill>
          </Rectangle>

        </Grid>
      </VisualBrush.Visual>
    </VisualBrush>
  </Rectangle.Fill>
</Rectangle>

Tip

You might be wondering why on earth you would ever use a DrawingBrush when VisualBrush is so much more flexible—VisualBrush can support any element, whereas DrawingBrush supports only the low-level Drawing and Geometry classes. However, DrawingBrush is more efficient. A drawing doesn’t carry the overhead of a full FrameworkElement for every drawing primitive. Although it takes more effort to create a DrawingBrush, it consumes fewer resources at runtime. If you want your user interface to have particularly intricate visuals, the DrawingBrush will enable you to do this with lower overhead. If you plan to use animation, this low overhead may translate to smoother-looking animations.

VisualBrush makes it very easy to create a brush that looks exactly like some part of your user interface. You could use this to create effects such as reflections, as Figure 13-55 shows, or to project the user interface onto a 3D surface. (We show this latter technique in Chapter 17.)

Reflection effect with VisualBrush
Figure 13-55. Reflection effect with VisualBrush

Example 13-55 shows how to create a reflection effect with a VisualBrush. The user interface to be reflected has been omitted for clarity—you would place this inside the Grid named mainUI. The important part is the Rectangle, which has been painted with a VisualBrush based on mainUI. This example also uses a ScaleTransform to flip the image upside down.

Example 13-55. Simulating a reflection with VisualBrush
<Grid>
  <Grid.RowDefinitions>
    <RowDefinition />
    <RowDefinition Height="40"/>
  </Grid.RowDefinitions>

    <Grid x:Name="mainUI">

      ...User interface to be reflected goes here...

    </Grid>
    <Rectangle Grid.Row="1">
      <Rectangle.LayoutTransform>
       <ScaleTransform ScaleY="-1" />
      </Rectangle.LayoutTransform>

      <Rectangle.Fill>
        <VisualBrush Visual="{Binding ElementName=mainUI}" />
      </Rectangle.Fill>

      <Rectangle.OpacityMask>
        <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
          <GradientStop Offset="0" Color="Transparent" />
          <GradientStop Offset="1" Color="White" />
        </LinearGradientBrush>
      </Rectangle.OpacityMask>
    </Rectangle>

</Grid>

Tip

The reflection is live in only one direction: if the main UI updates, the reflection will update to match, but you cannot interact with it.

As you can see from Figure 13-55, the image fades out toward the bottom. We achieved this by applying an OpacityMask. All user interface elements support this OpacityMask property. Its type is Brush. Only the transparency channel of the brush is used; the opacity of the element to which the mask is applied is determined by the opacity of the brush. In this case, we’ve used a LinearGradientBrush that fades to transparent, and this is what causes the Rectangle to fade to transparency.

Remember that VisualBrush derives from TileBrush. This means that you are not obliged to paint the target element with the whole of the source visual—you can use Viewport and Viewbox to be more selective. For example, you could use this to implement a magnifying glass feature.[94]

Pen

Brushes are used to fill the interior of a shape. To draw the outline of a shape, WPF needs a little more information—not only does it need a brush in order to color in the line, but it also needs to know how thick you would like the line to be drawn, and whether you want a dash pattern and/or end caps. The Pen class provides this information.

A Pen is always based on a brush, meaning that we can use all of the drawing effects we’ve seen so far when drawing outlines. You set the brush using the Brush property of the Pen class.

Tip

Remember that if you are working with any of the high-level shape elements, you will not work with a Pen directly. A Pen is used under the covers; you set all of the properties indirectly. Table 13-2 showed how Shape properties correspond to Pen properties.

You will typically deal directly with a Pen only if you work at a lower level, such as with the GeometryDrawing in a DrawingBrush.

You set the line width with the Thickness property. For simple outlines, this and Brush may be the only properties you set. However, Pen has more to offer. For example, you can set a dash pattern with the DashArray property. This is simply an array of numbers. Each number corresponds to the length of a particular segment in the dash pattern. Example 13-56 illustrates the simplest possible pattern.

Example 13-56. DashArray
<Rectangle Stroke="Black" StrokeThickness="5" StrokeDashArray="1" />

This indicates that the first segment in the dash pattern is of length 1. The dash pattern repeats, and because only one segment has been specified, every segment will be of length 1. Figure 13-56 shows the result.

Simple dash pattern
Figure 13-56. Simple dash pattern

Example 13-57 shows two slightly more interesting pattern sequences. Note that the second case supplies an odd number of segments. This means that the first time around, the solid segments will be of size 6 and the gap will be of size 1, but when the sequence repeats, the solid segment will be of length 1 and the gaps of size 6. So the effective length of the dash pattern is doubled. Figure 13-57 shows the results of both patterns.

Example 13-57. Dash patterns
<Rectangle Stroke="Black" StrokeThickness="5" StrokeDashArray="10 1 3 1" />
<Rectangle Stroke="Black" StrokeThickness="5" StrokeDashArray="6 1 6" />
Longer dash patterns
Figure 13-57. Longer dash patterns

WPF can draw corners in three different ways. You can set the LineJoin property to Miter, Bevel, or Round. These are shown in Figure 13-58.

LineJoin types: Miter, Bevel, and Round
Figure 13-58. LineJoin types: Miter, Bevel, and Round

For open shapes such as Line and PolyLine, you can specify the shape of the starts and ends of lines with the StartLineCap and EndLineCap properties. The DashCap property specifies the shape with which dashes start and end. These properties support four styles of caps: Round, Triangle, Flat, and Square. These are shown in Figure 13-59. Flat and Square both square off the ends of lines. The distinction is that with Flat, the flat end intersects the end point of the line, but with Square, it extends beyond it. The amount by which it overshoots the line is equal to half the line thickness.

Line cap styles: Round, Triangle, Flat, and Square
Figure 13-59. Line cap styles: Round, Triangle, Flat, and Square

Transformations

Support for high-resolution displays is an important feature of WPF. This is enabled in part by the emphasis on the use of scalable vector graphics rather than bitmaps. But as experience with GDI+ and GDI32 has shown, if scalability is not integrated completely into the graphics architecture, resolution independence is very hard to achieve consistently in practice.

WPF’s support for scaling is built in at a fundamental level. Any element in the user interface can have a transformation applied, making it easy to scale or rotate anything in the user interface.

As we saw in Chapter 3, all user interface elements have RenderTransform and LayoutTransform properties. These are of type Transform, which is an abstract base class. There are derived classes implementing various affine transformations,[95] listed in Table 13-8.

Table 13-8. Transform types

Transform class

Usage

MatrixTransform

General-purpose transform based on 3 × 3 matrix

RotateTransform

Rotates around a point

ScaleTransform

Scales in x and/or y

SkewTransform

Shears (e.g., converts a square into a rhombus)

TransformGroup

Combines several transforms into one

TranslateTransform

Moves items by a specified vector

Most of these are just convenience classes—you can represent all supported transformations by the MatrixTransform class. This contains a 3 × 3 matrix, allowing any affine transformation to be used. However, the other transform types are often easier to work with than the set of numbers in a matrix.

Example 13-58 shows the use of a TransformGroup to apply a ScaleTransform and a RotateTransform to the RenderTransform property of a TextBlock.

Example 13-58. Using RenderTransform
<StackPanel Orientation="Horizontal">
  <TextBlock>
    <TextBlock.RenderTransform>
      <TransformGroup>
        <ScaleTransform ScaleX="2" ScaleY="2" />
        <RotateTransform Angle="10" />
      </TransformGroup>
    </TextBlock.RenderTransform>
    Hello,
  </TextBlock>
  <TextBlock>world</TextBlock>
</StackPanel>

Notice that we have used a TransformGroup here to combine the effects of two transforms. (Note that the rotation angle is specified in degrees in a RotateTransform, rather than radians, which are slightly more common in computational geometry. Likewise, positive numbers are clockwise, contrary to the usual mathematical convention.) Figure 13-60 shows the results.

RenderTransform
Figure 13-60. RenderTransform

The order in which you apply transforms is usually significant, because each transform in a TransformGroup builds on the ones before it. For example, if you add a TranslateTransform to Example 13-58 to move the Hello text right by 30 device-independent pixels, the effect is different depending on whether it appears before or after the other transforms. The lefthand side of Figure 13-61 shows the result when the translation occurs first, and the righthand side shows the result when it occurs last. In the first case, the text has moved twice as far to the left; this is because the ScaleTransform was applied after the translation, doubling its effects.

Adding a TranslateTransform before (left) and after (right)
Figure 13-61. Adding a TranslateTransform before (left) and after (right)

Visual Layer Programming

The shape elements can provide a convenient way to work with graphics. However, in some situations, creating all the shape elements required to represent a drawing, and adding them to the UI tree, may be more trouble than it’s worth. Data binding can often provide a solution—the shape classes all derive from FrameworkElement, so they can participate in data binding like any other user interface element. However, sometimes your data may be structured in such a way that it’s easier or more efficient to write code that performs a series of drawing operations based on the data. For this reason, WPF provides a “visual layer” API as a lower-level alternative to shape elements. (In fact, the shape elements are all implemented on top of this visual layer.) This API lets us write code that renders content on demand.

Tip

A visual is a visible object. A WPF application’s appearance is formed by composing all of its visuals onto the screen. Because WPF builds on top of the visual layer, every element is a visual—the FrameworkElement base class derives indirectly from Visual. Programming at the visual layer simply involves creating a visual and writing code that tells WPF what we’d like to appear in that visual.

Even at this low level, WPF behaves very differently from Win32. The way in which graphics acceleration is managed means that your on-demand rendering code is called much less often than it would be in a classic Windows application.

Rendering On Demand

The key to custom on-demand rendering is the OnRender method. WPF calls this method when it needs your component to generate its appearance. (This is how the built-in shape classes render themselves.)

Tip

The virtual OnRender method is defined by the UIElement class. Most elements derive from this indirectly via FrameworkElement, which adds core features such as layout and data binding.

Example 13-59 shows a custom element that overrides OnRender.

Example 13-59. A custom OnRender implementation
class MyFramedTextRenderer : FrameworkElement {
    protected override void OnRender(DrawingContext drawingContext) {
        Debug.WriteLine("OnRender");

        drawingContext.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 50));

        FormattedText text = new FormattedText("Hello, world",
            CultureInfo.CurrentUICulture, FlowDirection.LeftToRight,
            new Typeface("Verdana"), 24, Brushes.Black);
        drawingContext.DrawText(text, new Point(3, 3));
    }
}

The OnRender method is passed a single parameter of type DrawingContext. This is the low-level drawing API in WPF. It provides a set of primitive drawing operations, which are listed in Table 13-9. Example 13-59 uses the DrawRectangle and DrawText methods.

Note that the DrawingContext uses the Brush and Pen classes to indicate how shapes should be filled and outlined. We can also pass in the same Geometry and Drawing objects we saw earlier in the chapter.

Table 13-9. DrawingContext drawing operations

Operation

Usage

DrawDrawing

Draws a Drawing object.

DrawEllipse

Draws an ellipse.

DrawGeometry

Draws any Geometry object.

DrawGlyphRun

Draws a series of glyphs (i.e., text elements) offering detailed control over typography.

DrawImage

Draws a bitmap image.

DrawLine

Draws a line (a single segment).

DrawRectangle

Draws a rectangle.

DrawRoundedRectangle

Draws a rectangle with rounded corners.

DrawText

Draws text.

DrawVideo

Draws a rectangular region that can display video.

PushTransform

Sets a transform that will be applied to all subsequent drawing operations until Pop is called; if a transform is already in place, the net effect will be the combination of all the transforms currently pushed.

PushClip

Sets a clip region that will be applied to all subsequent drawing operations until Pop is called; as with PushTransform, multiple active clip regions will combine with one another.

PushEffect

Applies a BitmapEffect to all subsequent drawing operations until Pop is called; as with transforms and clips, multiple calls to this method will combine effects.

PushOpacity

Sets a level of opacity that will be applied to all subsequent drawing operations until Pop is called; as with transforms and clips, multiple opacities are combined.

Pop

Removes the transform, clip region, or opacity added most recently by PushTransform, PushClip, or PushOpacity. If those methods have been called multiple times, calls to Pop remove their effects in reverse order. (The transforms, clip regions, and opacities behave like a stack.)

Because our custom element derives from FrameworkElement, it integrates naturally into any WPF application. Example 13-60 shows markup for a window that uses this custom element—we can use it just like we’d use any custom element. Figure 13-62 shows this window.

Example 13-60. Loading a custom visual into a window
<Window x:Class="VisualRender.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:VisualRender"
  Title="Visual Layer Rendering">
  <Canvas>
    <local:MyFramedTextRenderer Canvas.Top="10" Canvas.Left="10"
         x:Name="customRender" />
  </Canvas>
</Window>
Visual layer rendering in action
Figure 13-62. Visual layer rendering in action

Notice that the OnRender function in Example 13-59 calls Debug.WriteLine. If the program is run inside a debugger, this will print a message to the debugger output window each time OnRender is called. This enables us to see how often WPF asks our custom visual to render itself. If you are accustomed to how the standard on-demand painting in Win32 and Windows Forms works, you might expect to see this called regularly whenever the window is resized, or partially obscured and uncovered. In fact, it is called just once!

It turns out that on-demand rendering is not as similar to old-style Win32 rendering as you might think. WPF will call your OnRender function when it needs to know what content your visual displays, but the way graphics acceleration works in WPF means that this happens far less often than the equivalent repaints in Win32. WPF caches the rendering instructions. (This rendering style is sometimes referred to as retained mode, whereas the Win32 style is immediate mode.) The extent and form of this caching are not documented, but caching clearly occurs. Moreover, it is subtler than simple bitmap-based caching. We can add this code to the host window in Example 13-60 (this would go in the code-behind file):

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) {
    customRender.RenderTransform = new ScaleTransform(6, 6);
}

This applies a transform to our element, scaling it up by a factor of 6. When clicking on the user interface, the custom visual expands as you would expect, and yet OnRender is not called. Moreover, the enlarged visual does not show any of the pixelation or blurring artifacts you would see with a simple bitmap scale—it continues to be sharp, as you can see in Figure 13-63.

Scaled custom rendering
Figure 13-63. Scaled custom rendering

This outcome indicates that WPF is retaining scalable information about the contents of the visual. It is able to redraw our visual’s on-screen appearance without bothering our OnRender method, even when the transformation has changed. This is in part due to the acceleration architecture, but also because transformation support is built into WPF at the most fundamental levels. WPF’s ability to redraw without calling OnRender allows the user interface to remain intact on-screen even if our application is busy.

If the state of our object should change in a way that needs the appearance to be updated, we can call the InvalidateVisual method. This will cause WPF to call our OnRender method, allowing us to rebuild the appearance.

Note that when you override OnRender, you would typically also override the MeasureOverride and ArrangeOverride methods. Otherwise, WPF’s layout system will have no idea how large your element is. The only reason we got away without doing this here is that we used the element on a Canvas, which doesn’t care how large its children are. To work in other panels, it is essential to let the layout system know your size. Example 13-61 shows custom rendering of text along with layout logic.

Example 13-61. Custom rendering with layout logic
class CustomTextRenderer : FrameworkElement {
    FormattedText text = new FormattedText("Hello, world",
        CultureInfo.CurrentUICulture, FlowDirection.LeftToRight,
        new Typeface("Verdana"), 24, Brushes.Black);

    protected override void OnRender(DrawingContext drawingContext) {
        drawingContext.DrawText(text, new Point(0, 0));
    }

    protected override Size MeasureOverride(Size availableSize) {
        text.MaxTextWidth = availableSize.Width;
        text.MaxTextHeight = availableSize.Height;
        return new Size(text.Width, text.Height);
    }
    protected override Size ArrangeOverride(Size finalSize) {
        text.MaxTextWidth = finalSize.Width;
        text.MaxTextHeight = finalSize.Height;
        return finalSize;
    }
}

Chapter 3 described the MeasureOverride and ArrangeOverride methods in more detail. Example 13-61 defers to the FormattedText class to work out how much space is required. We describe FormattedText in the next chapter.

Where Are We?

WPF provides a range of high-quality rendering and composition services. A set of shape elements supports various drawing primitives. Several brush types are available for determining how shapes are painted, and pens augment brushes to define how outlines are drawn. Transformability is supported at all levels, making it easy to scale a user interface to any resolution or size. And, a low-level API is available for working at the “visual” layer when necessary.



[87] * In some graphics systems, this is described as the “winding” rule.

[88] * WPF doesn’t document whether the positive direction is clockwise or counterclockwise. This is because it doesn’t matter—as long as you are consistent, the final outcome is the same either way.

[89] * If you’d like to understand the formula for Bézier curves, http://mathworld.wolfram.com/beziercurve.html (http://tinysells.com/69) and http://en.wikipedia.org/wiki/b%c3%a9zier_curve (http://tinysells.com/70) both provide good descriptions.

[90] * The imaging system is extensible, so it’s possible to add support for custom formats. This requires unmanaged COM components to be written, and is beyond the scope of this book. See http://msdn2.microsoft.com/en-us/library/ms737408.aspx (http://tinysells.com/111) for information about the API for extending WPF imaging.

[91] * See http://msdn2.microsoft.com/en-us/library/ms735322.aspx (http://tinysells.com/108) for information on the API for building custom WPF bitmap effects.

[92] * The old GDI+ Color structure exposed 8-bit properties of the same names, which may be useful if you need to port code.

[93] * If you don’t use an absolute URL, a property of type ImageSource will be treated as a relative pack URI. So, the image in Example 13-51 is handled as a relative pack URI, which resolves to a resource compiled into the component. Pack URIs and resources were described in Chapter 12.

[95] * An affine transformation is one in which features arranged in a straight line before the transform remain in a straight line after the transform. Note that 3D perspective transformations do not preserve straight lines.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset