Image Brush Drawing Brush and Visual Brush

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

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

TileBrush

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

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

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

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

Example 13-44. Using an ImageBrush

<Rectangle> <Rectangle.Fill> <ImageBrush ImageSource="Images\Moggie.jpg" />

</Rectangle.Fill> </Rectangle>

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

Example 13-45. Specifying a Stretch of None

<Rectangle> <Rectangle.Fill>

<ImageBrush ImageSource="Images\Moggie.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.

Figure 13-45. Stretch.None

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

Figure 13-46. Stretch.Uniform

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

Figure 13-47. Stretch.UniformToFill

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

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

Images are centered by default. In the examples where the image has been cropped (Figures 13-45 and 13-47) the most central parts are shown. In the case of Uniform, where the image is smaller than the area being painted, it has been placed in the middle of that area (Figure 13-46). You can change this with the AlignmentX and

AlignmentY properties. You can set these to Left, Middle, or Right, and Top, Middle, or Bottom, respectively. Example 13-46 shows the UniformToFill stretch mode again, but this time with alignments of Left and Bottom. Figure 13-48 shows the results.

Example 13-46. Specifying a Stretch and alignment

<Rectangle> <Rectangle.Fill>

<ImageBrush ImageSource="Images\Moggie.jpg" Stretch="UniformToFill" AlignmentX="Left" AlignmentY="Bottom" />

</Rectangle.Fill> </Rectangle>

Figure 13-48. StretchUmformToFill, bottom-left-aligned

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

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

Figure 13-49. Stretch.UniformToFill with Viewbox

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

Example 13-47. Specifying a Viewbox

<ImageBrush Stretch="UniformToFill" Viewbox="0.75,0.42,0.25,0.34" ImageSource="Images\Moggie.jpg" />

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

Example 13-48. Viewbox with absolute units

<ImageBrush Stretch="UniformToFill"

ViewboxUnits="Absolute" Viewbox="593,250,200,200"

ImageSource="Images\Moggie.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.

Brush

Source

Source

Figure 13-50. Viewbox and Viewport

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

Example 13-49. Using Viewbox and Viewport

<ImageBrush ViewboxUnits="Absolute" Viewbox="380,285,308,243" Viewport="0.1,0.321,0.7, 0.557"

ImageSource="Images\Moggie.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.)

Figure 13-51. Viewbox and Viewport

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

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

Example 13-50. Specifying a Stretch and a TileMode

<Rectangle> <Rectangle.Fill> <ImageBrush ImageSource="Images\Moggie.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.

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

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

ImageBrush

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

Example 13-51. Using an ImageBrush

<Rectangle> <Rectangle.Fill>

<ImageBrush ImageSource="Images\Moggie.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.*

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

DrawingBrush

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

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

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

* 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.

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

Example 13-52. Using DrawingBrush

<Rectangle Width="80" Height="30"> <Rectangle.Fill> <DrawingBrush> <DrawingBrush.Drawing> <DrawingGroup>

<DrawingGroup.Children> <GeometryDrawing> <GeometryDrawing.Brush> <LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <GradientStop Color="Green" Offset="0" /> <GradientStop Color="DarkGreen" Offset="1" /> </LinearGradientBrush> </GeometryDrawing.Brush>

<GeometryDrawing.Pen> <Pen Thickness="0.02"> <Pen.Brush>

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

Rect="0.02,0.02,0.96,0.96" /> </GeometryDrawing.Geometry> </GeometryDrawing>

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

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

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

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

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

Figure 13-54. DrawingBrush with small Viewbox

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

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

VisualBrush

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

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

Example 13-54. Using a VisualBrush

<Rectangle Width="80" Height="30"> <Rectangle.Fill> <VisualBrush> <VisualBrush.Visual>

<Grid Width="80" Height="26"> <Grid.RowDefinitions> <RowDefinition Height="2*" /> <RowDefinition Height="*" /> </Grid.RowDefinitions>

<Rectangle Grid.RowSpan="2" RadiusX="13" RadiusY="13"> <Rectangle.Fill> <LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <GradientStop Color="Green" Offset="0" /> <GradientStop Color="DarkGreen" Offset="l" /> </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="l" /> </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.)

Figure 13-55. Reflection effect with VisualBrush

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

Example 13-55. Simulating a reflection with VisualBrush

<Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="40"/> </Grid.RowDefinitions>

<Grid x:Name="mainUI">

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

Example 13-55. Simulating a reflection with VisualBrush (continued)

<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="l" Color="White" /> </LinearGradientBrush> </Rectangle.OpacityMask> </Rectangle>

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.*

+3 0

Post a comment