7.1. Graphics Fundamentals
WPF makes it easy to use graphics in your application and to
exploit the power of your graphics hardware. There are many aspects of the
graphics architecture that contribute to this goal. This most important of
these is integration.
7.1.1. Integration
Graphical elements can be integrated into any part of your user
interface. Many GUI technologies tend to separate graphics off into a separate
world. This requires a "gear shift" 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
systemsyou cannot mix the two freely.
WPF, on the other hand, treats shapes as elements in the UI tree
like any other. So, we are free to mix them in with any other kind of element.
Example 7-1 shows various examples of this.
Example 7-1. Mixing graphics with other elements
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<TextBlock>Mix</TextBlock>
<Rectangle Fill="Red" Width="20" />
<TextBlock>text</TextBlock>
<Ellipse Fill="Blue" Width="40" />
<TextBlock>and</TextBlock>
<Button>Controls</Button>
</StackPanel>
<Ellipse DockPanel.Dock="Left" Fill="Green" Width="100" />
<Button DockPanel.Dock="Left">Foo</Button>
<TextBlock FontSize="24" TextWrap="Wrap">
And of course you can put graphics into
your text: <Ellipse Fill="Cyan" Width="50" Height="20" />
</TextBlock>
</DockPanel>
As you can see, graphical elements can be added seamlessly into
the markup. Layout works with graphics exactly as it does for any other
element. The results can be seen in Figure
7-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 graphics
from data, it might make more sense to do everything from code.
Most of the techniques shown in this chapter can be used 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, they can even be intermingled. Notice how in
Figure 7-1 the ellipse on the right-hand 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 controlyou would have to write a whole new control from
scratch that draws both the text and the ellipse. This mixing goes both waysnot
only can you mix controls into your graphics, you can also use graphical
elements to customize the look of your control, as
Chapter 5 showed.
This mixing isn't just about simple elements like text blocksit
also works for controls. For example,
Figure 7-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 being able to display a bitmap. But bitmaps are pretty
inflexiblethey are just a block of fixed graphicsso 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. The markup
for this button is shown in Example
7-2.
Example 7-2. Adding graphics to a Button
<Button>
<StackPanel Orientation="Horizontal">
<Canvas Width="20" Height="18" VerticalAlignment="Center">
<Ellipse Canvas.Left="1" Canvas.Top="1" Width="16" Height="16"
Fill="Yellow" Stroke="Black" />
<Ellipse Canvas.Left="4.5" Canvas.Top="5" Width="2.5" Height="3"
Fill="Black" />
<Ellipse Canvas.Left="11" Canvas.Top="5" Width="2.5" Height="3"
Fill="Black" />
<Path Data="M 5,10 A 3,3 0 0 0 13,10" Stroke="Black" />
</Canvas>
<TextBlock VerticalAlignment="Center">Click!</TextBlock>
</StackPanel>
</Button>
Of course, buttons with images are not a new idea, but
traditionally, buttons enabled this by allowing an image to be set. For
example, the Windows Forms Button has an Image property, and
in Cocoa, NSButton has a setImage method. This implementation
is pretty inflexiblethese controls allow a single caption and a single image to
be set. Compare this to Example 7-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 7-3
uses a Grid to arrange text and some ellipses within a Button.
The results are shown in Figure 7-3.
Example 7-3. Layout within a button
<Button HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Ellipse Grid.Column="0" Grid.Row="0" Fill="Blue" Width="10" Height="10" />
<Ellipse Grid.Column="2" Grid.Row="0" Fill="Blue" Width="10" Height="10" />
<Ellipse Grid.Column="0" Grid.Row="2" Fill="Blue" Width="10" Height="10" />
<Ellipse Grid.Column="2" Grid.Row="2" Fill="Blue" Width="10" Height="10" />
<Ellipse Grid.ColumnSpan="3" Grid.RowSpan="3" Stroke="LightGreen"
StrokeThickness="3" />
<TextBlock Grid.Column="1" Grid.Row="1" VerticalAlignment="Center">Click!</
TextBlock>
</Grid>
</Button>
In WPF, there is rarely any need for elements to provide
inflexible properties such as Text or Image. If it makes
sense for an element to present nested content, then it'll do just thatit will
present whatever mixture of elements you choose to provide.
If you are familiar with two-dimensional drawing technologies
such as Quartz 2D , GDI+
, or good old GDI32 , another advantage in the way
drawing is done may well have struck you. We no longer need to write a function
to respond to redraw requestsWPF can keep the screen repainted for us. This is
because WPF lets us represent drawings as objects.
7.1.2. Drawing Object Model
With many GUI technologies, applications that want customized
visuals are required to be able to re-create their appearance from scratch. The
usual technique for showing a custom appearance was to write code that
performed a series of drawing operations in order to construct the display.
This code would run when the relevant graphics first needed to be displayed. In
some systems, the OS did not retain a copy of what the application drew, so
this method would end up running any time an area needed repaintinge.g., if a
window was obscured and then uncovered.
When using this approach, where a rendering function constructs
the whole image, updating individual elements is often problematic. 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 impactsuch as
the size, location, or colorWPF will automatically update the display.
To illustrate this technique,
Example 7-4 shows a simple window containing several ellipses. Each of
these is represented by an Ellipse object, which we will use from the
code-behind file to update the display.
Example 7-4. Changing graphical elements
<Window x:Class="ChangeItem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005"
Text="Change Item"
>
<Canvas>
<Ellipse Canvas.Left="10" Canvas.Top="30" Fill="Indigo"
Width="40" Height="20" MouseLeftButtonDown="OnClick" />
<Ellipse Canvas.Left="20" Canvas.Top="40" Fill="Blue"
Width="40" Height="20" MouseLeftButtonDown="OnClick" />
<Ellipse Canvas.Left="30" Canvas.Top="50" Fill="Cyan"
Width="40" Height="20" MouseLeftButtonDown="OnClick" />
<Ellipse Canvas.Left="40" Canvas.Top="60" Fill="LightGreen"
Width="40" Height="20" MouseLeftButtonDown="OnClick" />
<Ellipse Canvas.Left="50" Canvas.Top="70" Fill="Yellow"
Width="40" Height="20" MouseLeftButtonDown="OnClick" />
</Canvas>
</Window>
Every ellipse's MouseLeftButtonDown event is handled by
an OnClick method defined in the code-behind file for this window. As
Example 7-5 shows, the OnClick method simply increases the Width
property of whichever Ellipse raised the event. The result is that
clicking on any ellipse will make it wider.
Example 7-5. Changing a shape at runtime.
using System.Windows;
using System.Windows.Shapes;
namespace ChangeItem
{
public partial class MainWindow : Window
{
public MainWindow( ) : base( )
{
InitializeComponent( );
}
private void OnClick(object sender, RoutedEventArgs e)
{
Ellipse r = (Ellipse) sender;
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
necessarywhen you set a property on an Ellipse object, WPF ensures
that the screen is updated appropriately. Moreover, WPF is aware that the items
all overlap, as shown in Figure 7-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, there are
still situations in which this object-model approach for drawing 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. These are described later in this chapter, in the "Visual-Layer
Programming" section.
|
|
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 shapesgeometric shapes can be scaled and rotated
without loss of image quality. This high-quality transformability is an
important feature of drawing in WPF.
7.1.3. Resolution Independence
Not only have graphics cards improved dramatically since the
first GUIs appeared, so have screens. For a long time, the only mainstream
display technology was the Cathode Ray Tube (CRT). Color CRTs offered only
limited resolutionthey struggled 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 of this book has a two-year-old laptop whose
display has a resolution of 150 pixels per inch. At the time of writing,
displays are available with over 200 pixels per inch. It is possible to create
even higher pixel densities. However, there is a problem with using these
screens on current operating systems: everything ends up being so small that it
becomes unusable. This is because of a pixel-based development culturethe vast
majority of applications measure out their user interfaces in pixels.
This is not entirely the result of technical limitations. Ever
since the first version of Windows NT shipped, it has been 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 itmost applications don't 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 7-5 shows a Windows Forms application that uses GDI+ to draw
text and graphics scaled to an arbitrary size.
Notice in Figure 7-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 while Windows
Forms offers some features to help with scaling the rest of the UI, it's not
automaticyou have to take deliberate and nontrivial steps to build a
resolution-independent UI in Windows Forms.
7.1.3.1. Scaling and rotation
WPF solves this problem by supporting transformations at a
fundamental level. Instead of just providing scalability at the 2-D drawing
level, WPF builds it 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 7-2, we can exploit this scalability by modifying just the first
line:
<Button LayoutTransform="scale 3 3">
The LayoutTransform property is available on all
user-interface elements in WPF, so you can scale an entire window just as
easily as a single button. Many kinds of transformation are available, and
these will be discussed in more detail later. For now, we are simply asking to
enlarge the button by a factor of 3 in both x- and y-dimensions.
Figure 7-6 shows the enlarged button.
Comparing the original
Figure 7-2 with Figure 7-6,
the latter is obviously larger, as you would expect. More significantly, the
details have become crisper. The rounded edges of the button are easier to see
than they were 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
Figure 7-6 with the examples in
Figure 7-7.
Figure 7-7 shows
what happens if you simply enlarge the original small-button bitmap. 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 simply 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 can hold a
candle to Figure 7-6.
7.1.3.2. Resolution, coordinates, and "pixels"
This support for scaling graphics
means that there is no fixed relationship between the coordinates your
application uses and the pixels onscreen. This is true even if you do not use
scaling transforms yourselfa 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
.
The official definition of a device-independent pixel in
WPF is 1/96th of an inch. If you specify the width of a shape as 96
pixels, this means that it should be exactly one inch wide. WPF will use as
many physical pixels as are required to fill one inch. For example,
high-resolution laptop screens 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 relies on the system-wide display settings to
determine the physical pixel size. You can adjust them through the Windows
Display Properties applet. 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.
|
|
You might be wondering why WPF uses the somewhat curious choice
of 1/96 inches, and why it calls this a "pixel." The reason is that 96dpi is
the default display DPI in Windows when running with Normal 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, and on screens with a high pixel density, WPF will scale your
drawings for you so that they remain at the correct physical size.
WPF's ability 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, onscreen 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
drawing primitives.
7.1.4. Shapes, Brushes and Pens
Most of the classes in WPF's drawing toolkit fall into one of
three categories: shapes, brushes , and pens
. There are many variations on these themes, and we will examine them in detail
later. However, to get anywhere at all with graphics, a basic understanding of
these classes is mandatory.
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 simple and multi-segment, using Line
and Polyline, respectively. Arbitrary filled shapes can also be
created. Polygon enables shapes whose edges are all straight, but if
you want curved edges, the Path class supports filled shapes as well
as curved lines. Figure 7-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. There are many brush
types 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
imagesthe ImageBrush uses a bitmap, and
the DrawingBrush uses a scalable drawing. Finally, the VisualBrush
lets you take any visual treeany chunk of user interface you likeand use that
as a brush to paint some other shape. This makes it easy to achieve effects
like reflections of whole sections of your user interface.
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 such as line thickness, dash
patterns, and end-cap details. Figure
7-9 shows a few of the effects available using brushes and pens.
7.1.5. Composition
The final key feature of the graphics architecture is
composition. In computer graphics, the term composition refers to the
process of combining multiple shapes or images
together to form the final output. WPF's composition model
is very different from the model on which 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 that is particularly important when
combining non-rectangular 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
onscreen 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 onscreen, 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 the Direct3D model. This may seem odd since the majority of WPF's
drawing functionality is two-dimensional, but most of the 3-D-oriented
functionality on a modern graphics card can also be used to draw 2-D shapes.
For example, pixel shaders can be used to implement the advanced ClearType
mechanism when composing text into a UI. WPF exploits the same ultra-fast
polygon drawing facilities used by 3-D games to render primitive shapes.
Moreover WPF caches portions of the visual tree on the graphics card,
significantly improving the repainting performance when changing small details
onscreen.
Now that we've seen the core concepts underpinning the WPF
graphics system, let's take a closer look at the details.
|