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.
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.
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.
<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.
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.
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.
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.
<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.
<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>
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.
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.
<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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Shape Type | Usage |
| An ellipse |
| A single straight line |
| A shape using any mixture of straight lines and curves |
| A closed shape made from straight lines |
| An open shape made from straight lines |
| A rectangle, optionally with rounded corners |
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.
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.
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.
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
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.
<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.
<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>
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.
<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.
To draw a rectangle with rounded corners, use the RadiusX
and RadiusY
properties, as Example 13-9 illustrates.
<Rectangle Width="100" Height="50" Fill="Black" RadiusX="30" RadiusY="20"
/>
Figure 13-13 shows the result.
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.
Figure 13-14 shows the result.
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.
<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).
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.
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.
<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.
As with the Line
class, the
point coordinates in a Polyline
are
relative to wherever the containing panel chooses to locate the
Polyline
.
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.
<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.
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.
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.
<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.
<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.
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
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.
Type | Usage |
| Combines two geometry objects using set operations such as intersection or union |
| An ellipse |
| Combines multiple geometries into one multifigure geometry |
| A single straight line |
| Defines shapes with any combination of straight lines, elliptical arcs, and Bézier curves |
| A rectangle |
| More efficient
alternative to |
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.
<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.
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.)
<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.
<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.)
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.
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.
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.
<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.
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 LineSegment
s 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.
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.
Segment type | Usage |
| Single straight line |
| Sequence of straight lines |
Elliptical arc | |
| Cubic Bézier curve |
| Quadratic Bézier curve |
| Sequence of cubic Bézier curves |
| 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.
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.
<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>
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.
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.
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 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
.
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.
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.
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.)
<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.
<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>
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.
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
.
<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>
<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.
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.
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.
<!-- 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.
Command | Command name | Segment type | Parameters |
| Move | Coordinate pair: the
| |
| Line | | Coordinate pair: end point |
| Horizontal line | | Single coordinate: end x coordinate (y coordinate will be the same as before) |
| Vertical line | | Single coordinate: end y coordinate (x coordinate will be the same as before) |
| Cubic Bézier curve | | Three coordinate pairs: two control points and one end point |
| Quadratic Bézier curve | | Two coordinate pairs: control point and end point |
| Smooth Bézier curve | | Two coordinate pairs: second control point and end point (first control point generated automatically) |
| Smooth quadratic Bézier curve | | One coordinate pair: end point (control point generated automatically) |
| Elliptical arc | | Seven numbers: x
radius, y radius, |
| Close path | None | |
| Even-odd fill rule | None | |
| 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.
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
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.
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.
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.
<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.
<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.
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.
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
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.
Type | Usage |
| A single frame from a bitmap file (some file formats support multiple frames). |
| Represents a bitmap at a specified URL; this is the type created when specifying a URL in XAML. |
| Wraps around any
|
| Wraps around any
|
| Wraps around any
|
| Wraps around any
|
| A bitmap whose contents
are generated from a |
| Wraps around any
|
| 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.
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.
RenderTargetBitmap
lets you
create a new a bitmap from any visual. Example 13-32 renders a red ellipse into a
bitmap.
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.
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.
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.)
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
.
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.
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.
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
.
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.
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.
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.
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.
<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.
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]
Type | Usage |
| Creates a pseudo-3D relief effect at the edges of the content |
| Allows multiple effects to be used on a single element |
| Makes the image look out of focus |
| Draws a soft shadow around the outline of the content |
| Performs a bump mapping algorithm to apply a pseudo-3D relief across the whole of the content |
| Adds a soft halo around the outline of the content |
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.
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.
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.
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
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.
<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.
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.
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.
<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.
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.
<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.
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.
<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.
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.
<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.)
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.
<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.
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
.
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.
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.
<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.
<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.
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.
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.
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.
<Rectangle> <Rectangle.Fill> <ImageBrush ImageSource="ImagesMoggie.jpg"Stretch="UniformToFill"
AlignmentX="Left" AlignmentY="Bottom"
/> </Rectangle.Fill> </Rectangle>
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.
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.
<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
.)
<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
.
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.
<ImageBrushViewboxUnits="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
.)
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.
<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.
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
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.
<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]
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.
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
.
<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.
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
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.
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.)
<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>
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.)
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.
<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>
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]
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.
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.
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.
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.
<RectangleStroke="Black" StrokeThickness="5" StrokeDashArray="10 1 3 1"
/> <RectangleStroke="Black" StrokeThickness="5" StrokeDashArray="6 1 6"
/>
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.
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.
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.
Transform class | Usage |
| General-purpose transform based on 3 × 3 matrix |
| Rotates around a point |
Scales in x and/or y | |
| Shears (e.g., converts a square into a rhombus) |
| Combines several transforms into one |
| 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
.
<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.
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.
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.
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.
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.)
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
.
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.
Usage | |
| Draws a |
| Draws an ellipse. |
| Draws any |
| Draws a series of glyphs (i.e., text elements) offering detailed control over typography. |
| Draws a bitmap image. |
| Draws a line (a single segment). |
| Draws a rectangle. |
| Draws a rectangle with rounded corners. |
| Draws text. |
| Draws a rectangular region that can display video. |
| Sets a transform that
will be applied to all subsequent drawing operations until
|
| Sets a clip region that
will be applied to all subsequent drawing operations until
|
| Applies a |
| Sets a level of opacity
that will be applied to all subsequent drawing operations
until |
| Removes the transform,
clip region, or opacity added most recently by |
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.
<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:
MyFramedTextRendererCanvas.Top="10" Canvas.Left="10"
x:Name="customRender" /> </Canvas> </Window>
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.
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.
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.
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.
[94] * For an example of this technique, see http://www.interact-sw.co.uk/iangblog/2007/03/28/wpfmagnifyupdate, or http://tinysells.com/100.
[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.