7.3. Brushes and Pens
In order to draw a shape onscreen, 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 augments 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, since 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.
7.3.1. Color
WPF uses the Color structure in the System.Windows.Media
namespace to represent a color. Note that if you have worked with Windows
Forms, ASP.NET, or GDI+ in the past, this is not the same structure 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 mixing together these
three primary colors.) A value of zero indicates that the color component is
not present at all; zero on all three channels corresponds to black. The alpha
channel represents the level of opacitya Color can be opaque,
completely transparent, or anywhere in between these two extremes. WPF's
composition engine supports transparency fully, so anything can be drawn with
any level of transparency. Zero is used to represent complete transparency.
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
screenthe color and brightness range of normal computer displays is such that
24 bits of color has always been adequate for most purposes. However, for many
imaging applications, this is not sufficient. For example, film can accommodate
a much wider range of brightness than a computer screen, and 24-bit color is
simply not adequate 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 problemsif 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 0 to 255.
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 to 1.
 |
The "Sc" in the ScA, ScR, ScG,
and ScB properties refers to the fact that they support the standard
"Extended RGB colour spacescRGB" color space defined in the IEC 61966-2-2
specification. (This is an international specification, hence the "u" in
"colour.") The "sc" is short for "scene" because this is nominally a scene-referred
color space. This means that color values in the scRGB space represent the
colors of the original image. This is different from how computer images are
often storedtraditionally we have used output-referred color spaces,
where the color values do not need to be mapped before being displayed on the
target device.
Output-referred color spaces are efficient to work with, as long
as they happen to target the output device type you are working with. However,
scene-referred color spaces preserve all of the information that was available
at the point at which the image was generated or captured. For highly accurate
color representation, scene-referred models are therefore clearly better, even
though they are slightly less efficient to work with.
|
|
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.
7.3.2. SolidColorBrush
SolidColorBrush is the simplest brush. It uses one
color across the whole area being painted. It has just one property, Color.
Note that this color is allowed to use transparency, despite what the word
"solid" suggests.
We have already been using the SolidColorBrush extensively
even though we have not yet referred to it by name. This is because WPF creates
this kind of brush if you specify the name of a color in markupif you work
mostly with markup, 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 7-26 shows various examples of numeric colors. They all 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, but
provide just four bits per channel. Six- or eight-digit numbers allow eight
bits per channel for RGB or ARGB, respectively. (To exploit the full 16-bit
accuracy of scRGB, you would need to set the property using property-element
syntaxthere is no text shorthand. See
Appendix A for more information on XAML's property-element syntax.)
Example 7-26. Numeric color values
<Rectangle Fill="#8f8" Width="100" Height="20" />
<Rectangle Fill="#1168ff" Width="50" Height="40" />
<Rectangle Fill="#8ff0" Width="130" Height="10" />
<Rectangle Fill="#72ff8890" Width="70" Height="30" />
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.
7.3.3. LinearGradientBrush
With a LinearGradientBrush, the painted area
transitions from one color to another, or even through a sequence of colors.
Figure 7-32 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 lineyou cannot do curved transitions, hence the name "linear."
Example 7-27 shows the markup for
Figure 7-32.
Example 7-27. Using a LinearGradientBrush
<Rectangle Width="80" Height="60">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color="Black" Offset="0" />
<GradientStop Color="White" Offset="1" />
</LinearGradientBrush.GradientStops>
</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 7-33.
(Note that if the brush is painting an area that is narrow or wide, the
coordinate system is squashed accordingly.)
Example 7-27 uses
the property-element syntax to initialize the brush. In this particular
example, this wasn't strictly necessary, because XAML supports a more compact
syntax. This is exactly equivalent to
Example 7-27:
<Rectangle Width="80" Height="60" Fill="LinearGradient 0,0 1,1 Black White" />
This compact syntax allows the most important aspects to be set.
The two pairs of numbers correspond to the StartPoint and EndPoint
properties, and the remaining two items are color names. If you want the
gradient to be vertical or horizontal, you can use an even simpler syntax:
<Rectangle Width="80" Height="60" Fill="VerticalGradient Black White" />
<Rectangle Width="80" Height="60" Fill="HorizontalGradient Black White" />
In all of these string representations, the start and end color
are specified. These correspond to the GradientStop elements in
Example 7-27. Notice that in the fully expanded compound-property
version, each GradientStop has an Offset property as well as
a Color. This enables the more verbose style to achieve something that
the string representations cannot. It allows the fill to pass through multiple
colors. Example 7-28 shows a LinearGradientBrush
with multiple colors.
Example 7-28. Multiple gradient stops
<Rectangle Width="80" Height="60">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<LinearGradientBrush.GradientStops>
<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.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
The result is shown in
Figure 7-34. Note that the shorthand string syntaxes shown earlier do
not support multiple color valuesyou have to use the full property-element
syntax if you want this effect.
LinearGradientBrush is often used to add a feeling of
depth to a user interface. Example
7-29 shows a typical example. It uses just two shapesa pair of rounded Rectangle
elements. (The Grid doesn't contribute directly to the appearance. It
is there to make it easy to resize the graphicchanging the grid's Width
and Height will cause both rectangles to resize appropriately.) The
second rectangle's gradient fill fades from a partially transparent shade of
white to a completely transparent color, which provides an interesting visual
effect.
Example 7-29. Simulating lighting effects with linear
fills
<Grid Width="80" Height="26">
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Rectangle Grid.RowSpan="2" RadiusX="13" RadiusY="13"
Fill="VerticalGradient Green DarkGreen"
Stroke="VerticalGradient Black LightGray" />
<Rectangle Margin="3,2" RadiusX="8" RadiusY="12"
Fill="VerticalGradient #dfff #0fff" />
</Grid>
Figure 7-35 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.
7.3.4. RadialGradientBrush
RadialGradientBrush is very similar to LinearGradientBrush.
Both allow transitions through a series of colors. But while LinearGradientBrush
paints these transitions in a straight line, the RadialGradientBrush fades
from a starting point out to an elliptical boundary. This opens up more
opportunities for making your user interface appear less flat, as shown in
Example 7-30.
Example 7-30. Using a RadialGradientBrush
<Rectangle Width="200" Height="150">
<Rectangle.Fill>
<RadialGradientBrush Center="0.5,0.4" RadiusX="0.3" RadiusY="0.5"
GradientOrigin="0.25,0.4">
<RadialGradientBrush.GradientStops>
<GradientStop Color="White" Offset="0" />
<GradientStop Color="DarkBlue" Offset="1" />
</RadialGradientBrush.GradientStops>
</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 7-36 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 7-30 makes
it easy to see the effects of the properties of the RadialGradientBrush,
but it's not a very exciting example.
Example 7-31 shows something a little more adventurous. It is similar
to Example 7-29it uses a
small number of shapes with gradient fills to convey a feeling of depth and
reflection, but this time using radial fills.
Example 7-31. Radial gradient fills
<Grid Width="16" Height="16" Margin="0,0,5,0" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="20*" />
<RowDefinition Height="6*" />
</Grid.RowDefinitions>
<Ellipse Grid.RowSpan="3" Grid.ColumnSpan="3" Margin="0.5">
<Ellipse.Fill>
<RadialGradientBrush Center="0.5,0.9" RadiusX="0.7" RadiusY="0.5">
<RadialGradientBrush.GradientStops>
<GradientStop Color="PaleGreen" Offset="0" />
<GradientStop Color="Green" Offset="1" />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Grid.Row="1" Grid.Column="1">
<Ellipse.Fill>
<RadialGradientBrush Center="0.5,0.1" RadiusX="0.7" RadiusY="0.5">
<RadialGradientBrush.GradientStops>
<GradientStop Color="#efff" Offset="0" />
<GradientStop Color="Transparent" Offset="1" />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Grid.RowSpan="3" Grid.ColumnSpan="3"
Stroke="VerticalGradient Gray LightGray" />
</Grid>
This time, three ellipses have been used, two with RadialGradientBrush
fills and one with 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.
The result is shown in Figure 7-37.
The radial fills suggest a curved surface and give the graphic a slightly
translucent look.
7.3.5. ImageBrush, and VisualBrush, DrawingBrush
It is often useful to fill shapes with a pattern or image of
some kind. WPF provides three brushes that allow us to use whatever graphics we
choose as a brush. The ImageBrush lets us paint with a bitmap. With DrawingBrush
we use a scalable drawing. VisualBrush allows us to use any markup at
all as the brush imagewe 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
.
7.3.5.1. 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 API common to the ImageBrush, DrawingBrush, and VisualBrush.)
Figure 7-38 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
the effect when the brush shape matches the target-area shape. All three are
rectangles painted with an ImageBrush specifying nothing more than the
image.
While Figure 7-38
was drawn with an ImageBrush, the behavior would be exactly the same
for any of the tile brushes. We'll look at the ImageBrush in more
detail shortly, but for now Example
7-32 shows the basic usage.
Example 7-32. Using an ImageBrush
<Rectangle>
<Rectangle.Fill>
<ImageBrush ImageSource="Images\Moggie.jpg" />
</Rectangle.Fill>
</Rectangle>
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
7-33 shows.
Example 7-33. Specifying a Stretch of None
<Rectangle>
<Rectangle.Fill>
<ImageBrush ImageSource="Images\Moggie.jpg" Stretch="None" />
</Rectangle.Fill>
</Rectangle>
This preserves the aspect ratio, but if the image is large, it
will simply be cropped to fit the space available, as
Figure 7-39 shows.
For displaying images, you may be more likely to 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 7-40. This scales the source image down 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. The default behavior is to paint
the image wherever the alignment properties specify and to leave the remainder
of the shape transparent. However, you can choose other behaviors for the spare
space with the TileMode property. The default is None, but if
you specify Tile, as in Example
7-34, the image will be repeated to fill the space available.
Example 7-34. Specifying a Stretch and a TileMode
<Rectangle>
<Rectangle.Fill>
<ImageBrush ImageSource="Images\Moggie.jpg"
Stretch="Uniform" TileMode="Tile" />
</Rectangle.Fill>
</Rectangle>
Figure 7-41 shows
the effect of the Uniform stretch mode and the Tile tile mode
combined. There is one potential problem with tiling. It can often be very
obvious where each repeated tile starts. If your goal was 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 alternate mirror images, as shown in
Figure 7-42. Although mirroring can reduce the discontinuity between
tiles, for some source images it can change the look of the brush quite
substantially.
An alternative to tiling is to 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 is shown in
Figure 7-43.
UniformToFill is most appropriate if you are filling an
area with some non-repeating textured pattern, because it guarantees it will
paint the whole area. It is probably less appropriate if your goal is simply to
display a pictureas Figure 7-43
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 (Figures 7-39
and 7-43) 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
7-40). You can change this with the AlignmentX and AlignmentY
properties. These can be set to Left, Middle, or Right,
and Top, Middle, or Bottom, respectively.
Example 7-35 shows the UniformToFill stretch mode again, but
this time with alignments of Left and Bottom.
Figure 7-44 shows the results
Example 7-35. Specifying a stretch and alignment
<Rectangle>
<Rectangle.Fill>
<ImageBrush ImageSource="Images\Moggie.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 choose to focus on any arbitrary part of the image or
choose specific scale factors. TileBrush supports these features
through the Viewbox, Viewport, 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 7-45 shows the UniformToFill stretch mode, but with a Viewbox
set to zoom in on the front of the car.
As Example 7-36
shows, the Viewbox is specified as four numbers. The first two are the
coordinates of the upper-left-hand corner of the Viewbox, and the
second two are the width and height of the box. These coordinates are relative
to the source image. 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.
Example 7-36. Specifying a Viewbox
<ImageBrush Stretch="UniformToFill" Viewbox="593,250,200,200"
ImageSource="Images\Moggie.jpg" />
While Viewbox lets you choose which portion of the
source image you would like to focus on, Viewport lets you choose
where that image should end up in the brush. Its functionality overlaps with
the alignment properties, but Viewport allows much more control.
Figure 7-46 illustrates
the relationship between Viewbox and Viewport. On the left,
we see the source imagea 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, we see 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.
The Viewport does not specify the extent of the brush.
As Figure 7-46 shows, the
brush does not need to be the same size as the Viewport. Nor will the
brush be clipped to be the size of the Viewport. All the Viewport
and Viewbox do is establish the scale and position at which the source
image will be drawn in the target brush.
Example 7-37 shows Viewport and Viewbox settings that
correspond to the areas highlighted in
Figure 7-46.
Example 7-37. Using Viewbox and Viewport
<ImageBrush
Viewbox="380,285,308,243"
Viewport="0.1,0.321,0.7, 0.557"
ImageSource="Images\Moggie.jpg" />
Notice in Example 7-37
that while the Viewbox coordinates are relative to the source image,
the Viewport uses numbers between 0 and 1. By default, the Viewport
coordinate system is based on the full size of the brush, with 0,0 being at the
top left and 1,1 being at the bottom right. This means that the area of the
image shown by the brush will always be the same, regardless of the brush size.
This results in a distorting behavior similar to the default StretchMode
of Fill, as shown in Figure
7-47. (In fact the Fill stretch mode is equivalent to setting
the Viewbox to be the size of the source image, and the Viewport
to be "0,0,1,1".)
You can specify different units for the Viewport with
the ViewportUnits property. The default is RelativeToBoundingBox,
but if you change it to Absolute, the Viewport is measured
using the user interface's coordinate system.
Remember that all of this scaling and positioning functionality
is common to all of the brushes derived from TileBrush. We will now
look at the features specific to the individual brushes.
7.3.5.2. ImageBrush
ImageBrush paints areas of the screen using a bitmap.
The ImageBrush was used to create all of the pictures in the previous
section. This brush is straightforwardyou simply need to tell it what bitmap to
use with the ImageSource property, as
Example 7-38 shows.
Example 7-38. Using an ImageBrush
<Rectangle>
<Rectangle.Fill>
<ImageBrush ImageSource="Images\Moggie.jpg" />
</Rectangle.Fill>
</Rectangle>
To make a bitmap file available to the ImageBrush,
simply add one to your project in Visual Studio 2005. The file in
Example 7-38 was in a subdirectory of the project called Images.
The bitmap must be built into the project as a resource. To do this, select the
bitmap file in Visual Studio 2005'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
6 for more information on how binary resources are managed.)
Alternatively, you can specify an absolute URL for this propertyyou could, for
example, display an image from a web site.
 |
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 and BMP
formats, can. (And to a lesser extent, GIF can do so as well. It only supports
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.
|
|
7.3.5.3. DrawingBrush
The ImageBrush is convenient if you have a bitmap you
need to paint with. However, bitmaps do not fit in well with resolution
independence. The ImageBrush will scale bitmaps correctly for your
screen's resolution, but bitmaps tend to become blurred when scaled. DrawingBrush
does not suffer from this problem, because you usually provide a scalable
vector image as its source. This enables a DrawingBrush to remain
clear and sharp at any size and resolution.
The vector image is represented by a Drawing object.
This is an abstract base class. Shapes can be drawn 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 together using the DrawingGroup
.
Even if you use nothing but shapes, you will still probably want
to group the shapes together 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 7-39 shows a Rectangle using a DrawingBrush for
its Fill.
Example 7-39. Using DrawingBrush
<Rectangle Width="80" Height="30">
<Rectangle.Fill>
<DrawingBrush>
<DrawingBrush.Drawing>
<DrawingGroup>
<DrawingGroup.Children>
<GeometryDrawing Brush="VerticalGradient Green DarkGreen">
<GeometryDrawing.Pen>
<Pen Thickness="0.02"
Brush="VerticalGradient Black LightGray" />
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<RectangleGeometry RadiusX="0.2" RadiusY="0.5"
Rect="0.02,0.02,0.96,0.96" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="VerticalGradient #dfff #0fff">
<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>
The brush in Example
7-39 paints the same visuals seen earlier in
Figure 7-35. Because each of the rectangular elements that make up the
drawing uses different linear gradient fills, they both get their own GeometryDrawing,
nested inside a DrawingGroup.
With a DrawingBrush, the Viewbox
defaults to 0,0,1,1. All of the coordinates and sizes in
Example 7-39 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. We already saw how to use the Viewbox in
Example 7-36. 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 7-39 to use a smaller Viewbox, as shown in
Example 7-40.
Example 7-40. Viewbox and DrawingBrush
<DrawingBrush Viewbox="0.5,0,0.5,0.25">
The result is that most of the drawing is now outside of the Viewbox,
so the brush only shows a part of the whole drawing, as
Figure 7-48 shows.
DrawingBrush is extremely powerful, as it lets you use
more or less any graphics you like as a brush, and because it is vector based,
the results remains 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 7-39 produces
the same appearance as Example 7-29,
but these examples are 28 lines and 10 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 7-29. (Note that this problem is less acute when using this
brush from code, where the higher-level objects are no more convenient to use
than geometries. So this is really only a XAML issue.) Fortunately, VisualBrush
allows us to paint with these higher-level elements.
7.3.5.4. VisualBrush
The VisualBrush can paint with the contents of any
element derived from Visual. Since 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.
Example 7-41 shows a Rectangle filled using a VisualBrush.
Example 7-41. Using a VisualBrush
<Rectangle Width="80" Height="30">
<Rectangle.Fill>
<VisualBrush Viewbox="0,0,80,26">
<VisualBrush.Visual>
<Grid Width="80" Height="26">
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Rectangle Grid.RowSpan="2" RadiusX="13" RadiusY="13"
Fill="VerticalGradient Green DarkGreen"
Stroke="VerticalGradient Black LightGray" />
<Rectangle Margin="3,2" RadiusX="8" RadiusY="12"
Fill="VerticalGradient #dfff #0fff" />
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
The brush's visuals in
Example 7-41 have been copied directly from
Example 7-29, resulting in a much simpler brush than the equivalent DrawingBrush.
(The results look exactly the same as
Figure 7-35the whole point of the VisualBrush is that it
paints areas to look just like the visuals it wraps.)
You might be wondering why on earth you would ever use a DrawingBrush
when VisualBrush is so much simpler. One reason we have both is that DrawingBrush
is more efficienta 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
so with lower overheads. 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, or to make the user interface appear to
rotate in 3-D. (This goes beyond the scope of this book.)
7.3.6. Pen
Brushes are used to fill the interior of a shape. To draw the
outline of a shape, WPF needs a little more informationnot only does it need a
brush in order to color in areas of the screen, 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 these.
A Pen is always based on a brush, meaning that all of
the drawing effects we've seen so far can be used when drawing outlines. The
brush is set using the Brush property.
 |
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, but you set all of the properties indirectly.
Table 7-1 shows 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.
|
|
The line width is set 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 7-42 illustrates
the simplest possible pattern.
Example 7-42. DashArray
<Rectangle Stroke="Black" StrokeThickness="5" StrokeDashArray="1" />
This indicates that the first segment in the dash pattern is of
length one. The dash pattern repeats, and since only one segment length has
been specified, every segment will be of length 1.
Figure 7-49 shows the result.
Example 7-43 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. The results of both
patterns are shown in Figure 7-50.
Example 7-43. Dash patterns
<Rectangle Stroke="Black" StrokeThickness="5" StrokeDashArray="10 1 3 1" />
<Rectangle Stroke="Black" StrokeThickness="5" StrokeDashArray="6 1 6" />
Corners can be drawn in three different ways. The LineJoin
property can be set to Miter, Bevel, or Round, shown
from left to right in Figure 7-51.
For open shapes such as Line or 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 that dashes start and end with. These properties support four styles of
cap: Round, triangle, Flat, and Square,
shown from top to bottom in Figure
7-52. 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.
|