Chapter 31. Bitmaps, Brushes, and Drawings


Computer graphics has traditionally been divided into the two opposing domains of raster graphics and vector graphics. Raster graphics involves bitmaps, which often encode real-world images, whereas vector graphics involves lines, curves, and filled areas. The two classes that derive from FrameworkElement most frequently used to display graphics objects seem to parallel this division. The Image class (which I first introduced in Chapter 3) is generally enlisted to display a bitmap, whereas the derivatives of the Shape class (which I also introduced in Chapter 3 but explored in more detail in Chapter 27) offer the most straightforward approach to displaying vector graphics. For most basic graphics requirements, Image and the Shape derivatives are really all you need.

However, the graphics capabilities of the Microsoft Windows Presentation Foundation (WPF) are really not so clearly divided between raster graphics and vector graphics. It's true that the Image class is used mostly to display bitmaps, but the class is not restricted to bitmaps. You can also use Image to display objects of type DrawingImage; you got a little taste of this capability in the About box in the YellowPad program in Chapter 22, which uses DrawingImage to display my signature. A DrawingImage object is always based on a Drawing object, and the word drawing usually refers to a picture composed of vector graphics elements, but Drawing is not restricted to vector graphics. A Drawing object can actually be a mix of vector graphics, raster graphics, and video.

This chapter sorts out the various ways in which raster graphics and vector graphics intermingle in the WPF, and it also finishes a discussion about brushes that began in Chapter 2. In that early chapter, I demonstrated how you can create solid and gradient brushes. But you can also base brushes on Drawing objects, bitmaps, or objects of type Visual. Because UIElement derives from Visual, you can base a brush on elements such as TextBlock and controls such as Button, making for some interesting effects.

Let's begin with bitmaps. Bitmaps in the WPF are supported by the abstract BitmapSource class and its descendants. Both BitmapSource and DrawingImage (which is generally but not always used to display vector graphics) directly descend from ImageSource, as shown in the following class hierarchy:

Object

  DispatcherObject (abstract)

    DependencyObject

      Freezable (abstract)

        Animatable (abstract)

          ImageSource (abstract)

            BitmapSource (abstract)

              BitmapFrame (abstract)

              BitmapImage

              CachedBitmap

              ColorConvertedBitmap

              CroppedBitmap

              FormatConvertedBitmap

              RenderTargetBitmap

              TransformedBitmap

            DrawingImage

You generally use an Image element to display one of these objects by setting the Source property of Image to an object of type ImageSource. The ImageSource class defines read-only Height and Width properties in device-independent units.

BitmapSource is the first class in the hierarchy that is unambiguously a bitmap. It defines read-only PixelWidth and PixelHeight properties, read-only DpiX and DpiY properties indicating the image resolution, and a read-only Format property of type PixelFormat. Unless the bitmap is very strange, the Format property is set to one of the 26 static read-only properties of the PixelFormats class. For example, a value of PixelFormats.Bgr32 indicates 32 bits per pixel, with 1-byte blue, green, and red values (in that order) and 1 unused byte per pixel. PixelFormats .Gray8 indicates 8 bits per pixel, encoding a gray shade. PixelFormats.Indexed8 indicates 8 bits per pixel, where the values index a color palette table. If the bitmap contains a color table, that is available from the Palette property of BitmapSource. The Palette property is of type BitmapPalette, which has a Colors property of type IList<Color>.

Of the classes that inherit from BitmapSource, the most frequently used is undoubtedly BitmapImage, mainly because it has a constructor that accepts a Uri object. That constructor (and a property named UriSource) lets you load a bitmap from a local file, a file on the network, or a file embedded in the application. File types BMP, JPEG, GIF, TIFF, and PNG are supported.

The ShowMyFace program in Chapter 3 contained the following code to create a BitmapImage object from a file on my Web site and then to display it with the Image element:

Uri uri = new Uri("http://www.charlespetzold.com/PetzoldTattoo.jpg"); BitmapImage bitmap = new BitmapImage(uri); Image img = new Image(); img.Source = bitmap; 


In XAML you simply set the Source property of Image to the URI string:

<Image Source="http://www.charlespetzold.com/PetzoldTattoo.jpg" /> 


Obviously, the XAML implementation gets a lot of help from ImageSourceConverter.

In code, you can alternatively create a BitmapImage using a parameterless constructor and then load the image by setting the UriSource or StreamSource property. Use whichever property is convenient, but enclose the code to set the property within calls to BeginInit and EndInit:

BitmapImage bitmap = new BitmapImage(); bitmap.BeginInit(); bitmap.UriSource = uri; bitmap.EndInit(); 


Yes, there is an advantage to this approach because you can also rotate the bitmap in increments of 90 degrees as it's being loaded:

BitmapImage bitmap = new BitmapImage(); bitmap.BeginInit(); bitmap.UriSource = uri; bitmap.Rotation = Rotation.Rotate90; bitmap.EndInit(); 


In XAML, you can rotate a BitmapImage by breaking out the Source property of Image as a property element that encloses a BitmapImage element:

<Image>     <Image.Source>         <BitmapImage UriSource="http://www.charlespetzold.com/PetzoldTattoo.jpg"                      Rotation="Rotate90" />     </Image.Source> </Image> 


You can also set the SourceRect property of BitmapImage to use only a subset of the entire image:

<Image>     <Image.Source>         <BitmapImage UriSource="http://www.charlespetzold.com/PetzoldTattoo.jpg"                      SourceRect="150 100 200 200" />     </Image.Source> </Image> 


Besides loading an existing bitmap into memory, you can also create new bitmaps in code, and you can put images on those bitmaps. There are two distinct ways to put images on bitmaps that you create. You can draw on the bitmap, or you can define the actual bitmap bits that make up the image. To save a bitmap that you create in this way, use the Save method of one of the classes that derive from BitmapEncoder.

If you want to create a bitmap whose image consists of graphics objects that your program draws on the bitmap, you first create an object of type RenderTargetBitmap:

RenderTargetBitmap renderbitmap =     new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Default); 


The first two arguments are the pixel width and height of the bitmap. The second two arguments are the horizontal and vertical resolutions in dots per inch (which you can set to whatever you want). The last argument indicates the format of the pixels. With RenderTargetBitmap, you must use either PixelFormats.Default or PixelFormats.Pbgra32, which stands for "premultiplied blue-green-red-alpha 32 bits." Each pixel requires 4 bytes, which are in the order blue, green, red, and alpha channel. The values of the three primary colors for each pixel have been premultiplied by the transparency indicated by the alpha channel.

When you first create the RenderTargetBitmap, the image is entirely transparent. The RenderTargetBitmap class has a method named Render that has an argument of type Visual, so you can draw on this bitmap in the same way you draw on a printer page. You can call Render multiple times. To clear the bitmap of images and restore it to its pristine state, call the Clear method.

Here's a program that uses RenderTargetBitmap to create a bitmap that is 100 pixels square. It then draws a rounded rectangle on the bitmap. The window's background is colored khaki so you can see that the corners of the bitmap remain transparent.

DrawGraphicsOnBitmap.cs

[View full width]

//--------- -------------------------------------------- // DrawGraphicsOnBitmap.cs (c) 2006 by Charles Petzold //------------------------------------------------ ----- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; namespace Petzold.DrawGraphicsOnBitmap { public class DrawGraphicsOnBitmap : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new DrawGraphicsOnBitmap()); } public DrawGraphicsOnBitmap() { Title = "Draw Graphics on Bitmap"; // Set background to demonstrate transparency of bitmap. Background = Brushes.Khaki; // Create the RenderTargetBitmap object. RenderTargetBitmap renderbitmap = new RenderTargetBitmap(100, 100, 96, 96, PixelFormats.Default); // Create a DrawingVisual object. DrawingVisual drawvis = new DrawingVisual(); DrawingContext dc = drawvis.RenderOpen(); dc.DrawRoundedRectangle(Brushes.Blue, new Pen(Brushes.Red, 10), new Rect(25, 25, 50, 50), 10, 10); dc.Close(); // Render the DrawingVisual on the RenderTargetBitmap. renderbitmap.Render(drawvis); // Create an Image object and set its Source to the bitmap. Image img = new Image(); img.Source = renderbitmap; // Make the Image object the content of the window. Content = img; } } }



By default, the Image element stretches the bitmap to fill the available area while preserving the aspect ratio. When a bitmap is stretched by Image, the colors of the pixels are interpolated to avoid a boxy look. Images with sharp edges (such as this one) seem blurry. To force Image to display the bitmap in its natural size, set the Stretch property:

img.Stretch = Stretch.None; 


Just as you can display elements such as panels and controls on the printer page, you can display elements on a bitmap. The following program creates a UniformGrid and puts 32 ToggleButton objects on its surface. The buttons have no content, but notice that some of the buttons have their IsChecked property set, which results in a slightly darker background color. The darker buttons seem to form another rounded rectangle.

DrawButtonsOnBitmap.cs

[View full width]

//---------------------------------------------------- // DrawButtonsOnBitmap.cs (c) 2006 by Charles Petzold //---------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; namespace Petzold.DrawButtonsOnBitmap { public class DrawButtonsOnBitmap : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new DrawButtonsOnBitmap()); } public DrawButtonsOnBitmap() { Title = "Draw Buttons on Bitmap"; // Create a UniformGrid for hosting buttons. UniformGrid unigrid = new UniformGrid(); unigrid.Columns = 4; // Create 32 ToggleButton objects on UniformGrid. for (int i = 0; i < 32; i++) { ToggleButton btn = new ToggleButton(); btn.Width = 96; btn.Height = 24; btn.IsChecked = (i < 4 | i > 27) ^ (i % 4 == 0 | i % 4 == 3); unigrid.Children.Add(btn); } // Size the UniformGrid. unigrid.Measure(new Size(Double .PositiveInfinity, Double .PositiveInfinity)); Size szGrid = unigrid.DesiredSize; // Arrange the UniformGrid. unigrid.Arrange(new Rect(new Point(0, 0), szGrid)); // Create the RenderTargetBitmap object. RenderTargetBitmap renderbitmap = new RenderTargetBitmap((int)Math .Ceiling(szGrid.Width), (int)Math .Ceiling(szGrid.Height), 96, 96, PixelFormats.Default); // Render the UniformGrid on the RenderTargetBitmap. renderbitmap.Render(unigrid); // Create an Image object and set its Source to the bitmap. Image img = new Image(); img.Source = renderbitmap; // Make the Image object the content of the window. Content = img; } } }



When you display elements and controls on the printer page, it is essential that you call Measure and Arrange on the parent element so it doesn't have a size of 0.

The other approach to creating bitmaps in code involves setting the actual bitmap bits. To create a bitmap from the bitmap bits, you use a static Create method of BitmapSource:

BitmapSource bitmap =     BitmapSource.Create(width, height, 96, 96, pixformat, palette, array, stride); 


An alternative form of the BitmapSource.Create method has an IntPtr argument. The first two arguments indicate the desired pixel width and height of the bitmap. You set the second two arguments to the horizontal and vertical resolution in dots per inch. The fifth argument is generally a static property of the PixelFormats class.

For bitmap formats that require a color tablespecifically, bitmaps created using static properties of PixelFormats beginning with the word Indexedthe next argument is an object of type BitmapPalette, which is a collection of Color objects. This collection can have fewer than the number of entries implied by the format. For example, PixelFormats.Indexed4 means that each pixel is encoded as 4 bits, so the BitmapPalette collection can have a maximum of 16 colors. It doesn't make sense for it to have as few as four colors, because then you can use PixelFormats.Indexed2 and encode each pixel with 2 bits. Before you create your own BitmapPalette object, take a look at the precreated BitmapPalette objects available as static properties of the BitmapPalettes class. For formats that don't require a color table, set this argument to null.

Regardless of the pixel format of the bitmap, each row of pixels in the bitmap consists of a whole number of bytes. For example, suppose you create a bitmap with a width of 5 pixels and a format of PixelFormats.Indexed4, which means that each pixel requires 4 bits. Multiply 5 pixels by 4 bits and you get 20 bits, or 2.5 bytes. Each row requires a whole number of bytes, and that whole number is 3. The first byte stores the first and second pixels of the row (the first pixel in the most significant 4 bits of the byte, and the second pixel in the least significant 4 bits); the second byte stores the third and fourth pixels; and the third byte stores the fifth pixel. The least significant 4 bits of the third byte in each row are ignored.

Another example: You're creating a bitmap with a width of 5 pixels and a format of PixelFormats .Rgb24. Each pixel requires 3 bytes (red first, then green, then blue), and the number of bytes required for each row is 15.

You generally set the last argument of BitmapSource.Create, called stride, to the whole number of bytes required for each row. That's a value of 3 for the first example and 15 for the second example. However, you can optionally set the stride argument to a larger integer than the calculated value. (In some earlier Windows graphical programming environments, stride values were required to be a multiple of 4 to improve the efficiency of rendering bitmaps on 32-bit microprocessors. This is not a requirement for the WPF.)

The array argument you pass as the seventh argument to BitmapSource.Create is a single-dimension numeric array. If this is a byte array, the number of elements in the array is equal to the stride value times the pixel height of the bitmap. (In essence, the stride value informs BitmapSource.Create how to access the array.) You can also use a short, ushort, int, or uint array for this purpose, in which case the number of elements in the array can be halved or quartered. The least significant byte in multibyte numeric elements corresponds to the leftmost pixel. If the stride is not an even number, a particular element of a ushort array, for example, will contain information for the end of one row and the beginning of the next row. This is allowed. The only thing that's not allowed is for a single byte to contain data that straddles two rows.

The following program creates a bitmap with a PixelFormats.Indexed8 format. The color table contains 256 entries, which are various combinations of red and blue. The 256-pixel square bitmap contains one-byte pixels that index this color table to display the various red and blue combinations in a pattern, with black at the upper-left corner and magenta at the lower-right corner.

CreateIndexedBitmap.cs

[View full width]

//---------------------------------------------------- // CreateIndexedBitmap.cs (c) 2006 by Charles Petzold //---------------------------------------------------- using System; using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; namespace Petzold.CreateIndexedBitmap { public class CreateIndexedBitmap : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new CreateIndexedBitmap()); } public CreateIndexedBitmap() { Title = "Create Indexed Bitmap"; // Create palette with 256 colors, combining red and blue. List<Color> colors = new List<Color>(); for (int r = 0; r < 256; r += 17) for (int b = 0; b < 256; b += 17) colors.Add(Color.FromRgb((byte)r, 0 , (byte)b)); BitmapPalette palette = new BitmapPalette(colors); // Create bitmap bit array. byte[] array = new byte[256 * 256]; for (int x = 0; x < 256; x++) for (int y = 0; y < 256; y++) array[256 * y + x] = (byte)(( (int)Math.Round(y / 17.0) << 4) | (int)Math.Round(x / 17.0)); // Create bitmap. BitmapSource bitmap = BitmapSource.Create(256, 256, 96, 96, PixelFormats.Indexed8, palette, array , 256); // Create an Image object and set its Source to the bitmap. Image img = new Image(); img.Source = bitmap; // Make the Image object the content of the window. Content = img; } } }



The following program displays a similar image but with much finer gradation because it uses a bitmap format of PixelFormats.Bgr32. Each pixel requires 4 bytes, so for convenience the program defines an int array for storing the pixel values. The upper byte of each 4-byte value is not used with this pixel format.

CreateFullColorBitmap.cs

[View full width]

//--------- --------------------------------------------- // CreateFullColorBitmap.cs (c) 2006 by Charles Petzold //------------------------------------------------ ------ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; namespace Petzold.CreateFullColorBitmap { public class CreateFullColorBitmap : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new CreateFullColorBitmap()); } public CreateFullColorBitmap() { Title = "Create Full-Color Bitmap"; // Create bitmap bit array. int[] array = new int[256 * 256]; for (int x = 0; x < 256; x++) for (int y = 0; y < 256; y++) { int b = x; int g = 0; int r = y; array[256 * y + x] = b | (g << 8) | (r << 16); } // Create bitmap. BitmapSource bitmap = BitmapSource.Create(256, 256, 96, 96, PixelFormats.Bgr32, null, array, 256 * 4); // Create an Image object and set its Source to the bitmap. Image img = new Image(); img.Source = bitmap; // Make the Image object the content of the window. Content = img; } } }



Many of the other classes that derive from BitmapSource create new bitmaps from existing bitmaps. You can often use these classes in XAML. For example, the following stand-alone XAML program uses CroppedBitmap to load an image from my Web site and crop it to the dimensions indicated in the SourceRect property. This bitmap becomes the source for FormatConvertedBitmap, which converts it to a 2-bit-per-pixel gray shade format. The result is a source for TransformedBitmap, which rotates the image 90 degrees. Finally, Image displays the resultant cropped, converted, and transformed image.

ConvertedBitmapChain.xaml

[View full width]

<!-- === ==================================================== ConvertedBitmapChain.xaml (c) 2006 by Charles Petzold ============================================= ========== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Image> <Image.Source> <TransformedBitmap> <TransformedBitmap.Transform> <RotateTransform Angle="90" /> </TransformedBitmap.Transform> <TransformedBitmap.Source> <FormatConvertedBitmap DestinationFormat="Gray2"> <FormatConvertedBitmap.Source> <CroppedBitmap Source= "http://www .charlespetzold.com/PetzoldTattoo.jpg" SourceRect="120 80 220 200" /> </FormatConvertedBitmap .Source> </FormatConvertedBitmap> </TransformedBitmap.Source> </TransformedBitmap> </Image.Source> </Image> </Page>



The first class hierarchy I showed you in this chapter focused on ImageSource, which is the type of the object you set to the Source property of Image. Here's that class hierarchy again with an ellipsis in place of all the bitmap-related classes that derive from BitmapSource:

Object

  DispatcherObject (abstract)

    DependencyObject

      Freezable (abstract)

        Animatable (abstract)

          ImageSource (abstract)

            BitmapSource (abstract)

              ...

            DrawingImage

This class hierarchy implies that you can also set the Source property of Image to an object of type DrawingImage. That word "drawing" in the class name seems to imply vector graphics, and that's usually (but not always) the case. In general, a DrawingImage is a composite of vector graphics, bitmaps, and video.

DrawingImage has a rather exalted position in the class hierarchy. It seems to be positioned as the vector equivalent to bitmaps, which might imply that DrawingImage is being groomed to become an interchange medium for vector graphicsperhaps the XAML equivalent of graphics metafiles. There is no indication that Microsoft has such big plans for DrawingImage, but if you want to think of DrawingImage as a graphics metafile, you won't be too far off.

The concept of Image displaying a chunk of vector graphics may be a little startling at first, so let me begin by showing you a simple example.

ImageDisplaysVectorGraphics.xaml

[View full width]

<!-- === =========== ================================================ ImageDisplaysVectorGraphics.xaml (c) 2006 by Charles Petzold ============================================= ================= --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Image Stretch="None"> <Image.Source> <DrawingImage> <DrawingImage.Drawing> <GeometryDrawing Brush="Blue"> <GeometryDrawing.Pen> <Pen Brush="Red" Thickness="5" /> </GeometryDrawing.Pen> <GeometryDrawing.Geometry> <EllipseGeometry Center="0,0" RadiusX="50" RadiusY="50" /> </GeometryDrawing.Geometry> </GeometryDrawing> </DrawingImage.Drawing> </DrawingImage> </Image.Source> </Image> </Page>



The Source property of Image is broken out as the property element Image.Source. This is set to an object of type DrawingImage. DrawingImage has only one modifiable propertya property named Drawing of type Drawing. One class that derives from Drawing is GeometryDrawing. A GeometryDrawing is a Geometry objectwhich, as you learned in Chapter 28 combines the closest that the WPF comes to pure analytic geometrywith a Brush and a Pen used to render that object. In this example, the Geometry property of GeometryDrawing is set to an EllipseGeometry object.

You don't need to break out the Geometry property of GeometryDrawing as a property element. You can instead use the geometry markup syntax. The following markup describes a triangle right in the GeometryDrawing start tag.

<GeometryDrawing Brush="Blue" Geometry="M 0 0 L 100 0 L 0 100 Z"> 


It's useful to examine the ImageDisplaysVectorGraphics.xaml file starting with the most nested element and working out: The file begins with a Geometry object, specifically of type EllipseGeometry. A geometry is really just coordinate points. That geometry, along with a Brush and Pen, becomes a GeometryDrawing. The GeometryDrawing has gone beyond pure analytic geometry and now has real colors and pen dimensions. As you'll see shortly, the GeometryDrawing class derives from Drawing. A DrawingImage object is always based on a Drawing object. DrawingImage has a constructor that accepts a Drawing object and a property named Drawing of type Drawing. From ImageSource, DrawingImage inherits two read-only properties named Width and Height that provide the width and height of the Drawing in device-independent units. In this example, those Width and Height properties would both indicate 105 units, which is the diameter of the ellipse plus 2.5 units of pen thickness on each side.

The Image element has its Stretch property set to None. This causes the ellipse to be displayed in its actual size of 105 units square. Remove that Stretch attribute and the ellipse becomes as large as the area allowed for it while still maintaining its correct aspect ratio. You have to set Stretch to Fill to persuade Image to ignore the aspect ratio of the drawing.

GeometryDrawing is one of five classes that derive from Drawing, as the following class hierarchy shows:

Object

  DispatcherObject (abstract)

    DependencyObject

      Freezable (abstract)

        Animatable (abstract)

          Drawing (abstract)

            DrawingGroup

            GeometryDrawing

            GlyphRunDrawing

            ImageDrawing

            VideoDrawing

The DrawingGroup class is very important. It defines a Children property that stores a collection of other Drawing objects. This is how you combine vector graphics and bitmaps into one composite drawing. Very often, when WPF documentation refers to an object of type Drawing, in reality that object will be of type DrawingGroup, so it's potentially a mix of different types of graphics objects.

The other four classes that derive from Drawing are more specific. As you saw in the previous XAML file, the GeometryDrawing object combines a Geometry object with Brush and Pen.

The GlyphRunDrawing class combines a GlyphRun object with a foreground brush. A GlyphRun is a series of characters in a particular font and size, but this class is very difficult to use, and I won't be discussing it here.

An ImageDrawing object generally references a bitmap, which you set to the ImageSource property. You must also set the Rect property of ImageDrawing to give this bitmap a specific size.

The VideoDrawing class has a Player property of type MediaPlayer and a Rect property to give it a specific size. I won't be demonstrating this class.

For the last several paragraphs, I've been discussing a class named DrawingImage, and now I've just introduced a new class called ImageDrawing. Not only are the class names very similar but you can set a DrawingImage object to the ImageSource property of an ImageDrawing object, and you can set an ImageDrawing object to the Drawing property of a DrawingImage object. Here's a little chart that may or may not add to your confusion.

Class

Derives From

Contains Property Named

Of Type

DrawingImage

ImageSource

Drawing

Drawing

ImageDrawing

Drawing

ImageSource

ImageSource


Of course, a big part of this confusion is that we prefer to keep raster graphics and vector graphics separate, but both classes combine the words "image" (which suggests raster graphics) and "drawing" (which suggests vector graphics). Perhaps you can think of DrawingImage as a drawing that can be treated the same way as an image; that is, you can display it using the Image element. ImageDrawing is generally an image that is treated as a drawing, which means that it can be combined with other drawings into a DrawingGroup.

Here's a stand-alone XAML file that uses both DrawingImage and ImageDrawing in the ways they were intended to be used, rather than in the various ways implied by the circular nature of the property definitions. This XAML file contains an Image element whose Source property has been set to an object of type DrawingImage. The Drawing property of this DrawingImage is set to a DrawingGroup object whose children include an ImageDrawing object referencing a bitmap, three GeometryDrawing objects that draw a frame around the bitmap, and a wire to hang the frame on the wall.

PictureAndFrame.xaml

[View full width]

<!-- === =============================================== PictureAndFrame.xaml (c) 2006 by Charles Petzold ============================================= ===== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Image Stretch="None"> <Image.Source> <DrawingImage> <DrawingImage.Drawing> <DrawingGroup> <!-- Bitmap image of fixed size. --> <ImageDrawing Rect="5 5 200 240" ImageSource= "http://www .charlespetzold.com/PetzoldTattoo.jpg" /> <!-- Dotted pen for scalloped pattern effect. --> <GeometryDrawing> <GeometryDrawing.Pen> <Pen Brush="DodgerBlue" Thickness="10" DashCap="Round"> <Pen.DashStyle> <DashStyle Dashes="0 1" /> </Pen.DashStyle> </Pen> </GeometryDrawing.Pen> <GeometryDrawing.Geometry> <RectangleGeometry Rect="5 5 200 240" /> </GeometryDrawing .Geometry> </GeometryDrawing> <!-- Solid pen to hide half the dotted pen. --> <GeometryDrawing> <GeometryDrawing.Pen> <Pen Brush="DodgerBlue" Thickness="5" /> </GeometryDrawing.Pen> <GeometryDrawing.Geometry> <RectangleGeometry Rect="2.5 2.5 205 245" /> </GeometryDrawing .Geometry> </GeometryDrawing> <!-- Wire to hang the frame on the wall. --> <GeometryDrawing Geometry="M 10 0 L 105 -50 L 200 0" > <GeometryDrawing.Pen> <Pen Brush="Black" /> </GeometryDrawing.Pen> </GeometryDrawing> </DrawingGroup> </DrawingImage.Drawing> </DrawingImage> </Image.Source> </Image> </Page>



DrawingGroup is intended for assembling composite images, which in practice come mostly from ImageDrawing objects (generally bitmaps) and GeometryDrawing objects. GeometryDrawing objects are based on geometries, so they already have specific coordinates and sizes. A bitmap always has a metrical size as well, which is implied by its pixel dimensions and resolution. But a way to position that bitmap relative to other graphical objects is also required here. That's the purpose of the Rect property defined by ImageDrawing. It not only gives the bitmap a specific size but it also positions that bitmap within a two-dimensional coordinate space.

Notice that a separate GeometryDrawing object is required for each Brush or Pen object you use for coloring the geometries. If you have several Geometry objects that are to be colored with the same brush and pen, you can combine them into a GeometryGroup and use that as the basis for the GeometryDrawing.

The DrawingGroup class doesn't restrict itself to defining a Children property that lets you assemble a composite drawing; the class also defines several properties that let you alter that composite drawing.

To set a clipping region for the composite drawing, you set the ClipGeometry property of DrawingGroup to a Geometry object. You can put this markup in PictureAndFrame.xaml to cut a corner off the portrait:

<DrawingGroup ClipGeometry="M 0 -50 L 210 -50 L 210 120 L 0 250 Z" > 


To make the drawing partially transparent, you can set the Opacity or OpacityMask properties. For example, this change to the DrawingGroup start tag makes the entire drawing 50 percent transparent:

<DrawingGroup Opacity="0.5"> 


You set the OpacityMask property to a Brush object. All the colors of the Brush are ignored except for the alpha channel of each color, which is used to set the transparency of the DrawingGroup. You can put the following markup anywhere within the DrawingGroup start and end tags but not within another property element or child of DrawingGroup:

<DrawingGroup.OpacityMask>     <RadialGradientBrush>         <GradientStop Offset="0" Color="White" />         <GradientStop Offset="1" Color="Transparent" />     </RadialGradientBrush> </DrawingGroup.OpacityMask> 


The drawing remains opaque in the center but fades near the boundaries. An OpacityMask property is also defined by UIElement so you can use this technique with any element.

Another property defined by both UIElement and DrawingGroup is BitmapEffect, which lets you use classes defined in the System.Windows.Media.Effects namespace for applying common visual effects to items. For example, try this:

<DrawingGroup.BitmapEffect>     <DropShadowBitmapEffect /> </DrawingGroup.BitmapEffect> 


This applies a drop shadow to the lower right of the picture frame. You can control the color, size, and other aspects of this drop shadow with properties defined by DropShadowBitmapEffect. This one makes the whole thing seem a bit radioactive:

<DrawingGroup.BitmapEffect>     <OuterGlowBitmapEffect GlowColor="Red" /> </DrawingGroup.BitmapEffect> 


And don't worry about your eyesight when you try this one:

<DrawingGroup.BitmapEffect>     <BlurBitmapEffect /> </DrawingGroup.BitmapEffect> 


The DrawingGroup class also defines a Transform property that lets you apply a transform to the entire drawing, perhaps if you've discovered that it's really much larger than you'd prefer:

<DrawingGroup.Transform>     <ScaleTransform ScaleX="0.25" ScaleY="0.25" /> </DrawingGroup.Transform> 


Because Image is effectively centering the composite drawing within the Page, any translation transforms are ignored. To "swing" the picture on its nail, you can animate the RenderTransform of the Image object itself. This markup can go right after the Image start tag:

<Image.RenderTransform>     <RotateTransform x:Name="xform" /> </Image.RenderTransform> <Image.RenderTransformOrigin>     <Point X="0.5" Y="0" /> </Image.RenderTransformOrigin> <Image.Triggers>     <EventTrigger RoutedEvent="Image.Loaded">         <BeginStoryboard>             <Storyboard TargetName="xform" TargetProperty="Angle">                 <DoubleAnimation From="-10" To="10" AutoReverse="True"                                  RepeatBehavior="Forever"                                  AccelerationRatio="0.5"                                  DecelerationRatio="0.5" />             </Storyboard>         </BeginStoryboard>     </EventTrigger> </Image.Triggers> 


Along with exploring the power of high-level drawing classes, it's also helpful to be familiar with the low-level drawing facilities, if only to get a sense of the overall capabilities and limitations of the system. The lowest-level drawing methods you can use and still call yourself a full-fledged Windows Presentation Foundation application are those defined by the DrawingContext class.

As you'll recall, a class that derives from UIElement or FrameworkElement can override the OnRender method to draw the element. The single parameter to OnRender is an object of type DrawingContext. It is also possible to create an object of type DrawingVisual and to use the RenderOpen method to obtain a DrawingContext object. You then call methods of the DrawingContext class to draw on this object, and you conclude by calling Close. You're left with a DrawingVisual object that stores the graphics. This is the technique you use when printing and when displaying graphics on a bitmap of type RenderTargetBitmap.

If you think of DrawingContext as the class that defines all the capabilities and limitations of the WPF graphics system, it has surprisingly few drawing methods. In the following statements, dc is an object of type DrawingContext, brush is a Brush, pen is a Pen, rect is a Rect, anything that begins with pt is a Point, and anything that begins with x or y is a double.

Of course, DrawingContext has a method to draw a single line between two points:

dc.DrawLine(pen, pt1, pt2); 


DrawingContext also includes a version of DrawLine that has two arguments of type AnimationClock:

dc.DrawLine(pen, pt1, anima1, pt2, anima2); 


An OnRender method can create one or two AnimationClock objects to animate the two points. So, with one DrawLine call, OnRender can display a graphic that changes over time without further intervention. I demonstrated animation in an OnRender method in the RenderTheAnimation program in the previous chapter.

The next method in DrawingContext draws a rectangle:

dc.DrawRectangle(brush, pen, rect); 


The Rect structure indicates the location of the upper-left corner of the rectangle, and its width and height. The Brush or Pen arguments can be set to null to suppress the drawing of the rectangle's interior or perimeter. There is also a version of DrawRectangle that includes an AnimationClock argument that animates the Rect object.

The DrawRoundedRectangle method draws a rectangle with rounded corners. The extent of the rounding is governed by the last two arguments:

dc.DrawRoundedRectangle(brush, pen, rect, xRadius, yRadius); 


A version of DrawRoundedRectangle with three AnimationClock objects can animate the rectangle or either of the two radii.

You can use DrawRoundedRectangle to draw an ellipse by calculating the last two arguments based on the Rect argument:

dc.DrawRoundedRectangle(brush, pen. rect, rect.Width / 2, rect.Height / 2); 


Or you can use the DrawEllipse method for that purpose:

dc.DrawEllipse(brush, pen, ptCenter, xRadius, yRadius); 


Notice that this method is a little different from DrawRectangle because you specify the center of the figure. A second version of DrawEllipse has three AnimationClock arguments for the center and the two radii.

You'll remember the DrawText method that requires an argument of type FormattedText:

dc.DrawText(formtxt, ptOrigin); 


Another method defined by DrawingContext requires a GlyphRun, which is a collection of characters at precise locations:

dc.DrawGlyphRun(brush, glyphrun); 


In the following method, the media argument is an instance of MediaPlayer, a class that can play movies and sound files by referencing the URI of a local or remote media file:

dc.DrawVideo(media, rect); 


The final three drawing methods handle generalized vector graphics, raster graphics, and a combination of the two. To render a Geometry object on the screen, you must supply a Brush and Pen in this DrawingContext method call:

dc.DrawGeometry(brush, pen, geometry); 


At the other extreme is the method that renders a bitmap. The first argument is of type ImageSource, which (as you know now) also encompasses the DrawingImage class as well as BitmapSource class and its derivatives:

dc.DrawImage(imgsrc, rect); 


If you haven't already guessed, the final drawing method accepts a Drawing argument:

dc.DrawDrawing(drawing); 


DrawingContext also includes a few methods to set clipping, opacity, and transforms. These all take the form of methods that push the attribute on a stack so it applies to all future drawing calls until Pop removes it from the stack. The PushClip method takes an argument of type Geometry and sets a clipping region. The PushOpacity method takes a double argument that ranges from 0 (transparent) to 1 (opaque), and a second version of PushOpacity includes an AnimationClock. The PushTransform method takes an argument of type Transform.

Just how important is Drawing in the whole scheme of WPF graphics? When you create a DrawingVisual object and then call RenderOpen to obtain a DrawingContext, when you're finished drawing on the DrawingContext object, the DrawingVisual stores all the graphics you've drawn. It stores these graphics as a read-only property named Drawing that returns an object of type DrawingGroup.

I tackled the subject of brushes in the second chapter in this book, but that chapter didn't include all the available types of brushes. It is now time to explore those other brushes. Here's the complete Brush class hierarchy:

Object

  DispatcherObject (abstract)

    DependencyObject

      Freezable (abstract)

        Animatable (abstract)

          Brush (abstract)

            GradientBrush (abstract)

              LinearGradientBrush

              RadialGradientBrush

            SolidColorBrush

            TileBrush (abstract)

              DrawingBrush

              ImageBrush

              VisualBrush

Note the three classes that derive from TileBrush. These brushes are based on other graphical objects. The DrawingBrush has a Drawing property. Often this will be a GeometryDrawing object (which means the brush will display some vector graphics), but the Drawing could include bitmaps or media players. The ImageBrush has an ImageSource property. Often this will be a BitmapImage object, but it could be a DrawingImage object and include various graphics objects. The VisualBrush has a Visual property. That could be a DrawingVisual object created by drawing on a DrawingContext, but recall that UIElement and FrameworkElement also derive from Visual. VisualBrush is often used to display controls or other parts of the user interface, perhaps as a shadow or reflection.

The three types of brushes that derive from TileBrush all work in fundamentally the same way. A surface is covered with a drawing, an image, or a visual. However, you have lots of options for how the drawing (or image or visual) is stretched or tiled over the surface. These options are all controlled through eight properties defined by TileBrush and inherited by the other three brushes. Mastery of these eight properties of TileBrush governs how effectively you use these three types of brushes.

Here's a simple ImageBrush example that uses the default settings of those eight TileBrush properties and sets the ImageSource property of the brush to a bitmap from my Web site. The brush is set to the Background property of a Page.

SimpleImageBrush.xaml

[View full width]

<!-- === ================================================ SimpleImageBrush.xaml (c) 2006 by Charles Petzold ============================================= ====== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Page.Background> <ImageBrush ImageSource="http://www.charlespetzold .com/PetzoldTattoo.jpg"> </ImageBrush> </Page.Background> </Page>



When you run this program in XAML Cruncher, you'll see that the bitmap is stretched to fill the page's entire background. If the aspect ratio of the page doesn't match the aspect ratio of the bitmap, the image is distorted. This is not the default approach that Image takes in displaying bitmaps, but it's more appropriate when you're using a bitmap as a brush. You want that bitmap to emcompass the entire area that the brush coversin this case, the entire Page.

Here's a simple VisualBrush example with the same overall structure as SimpleImage Brush.xaml.

SimpleVisualBrush.xaml

[View full width]

<!-- === ================================================= SimpleVisualBrush.xaml (c) 2006 by Charles Petzold ============================================= ======= --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Page.Background> <VisualBrush> <VisualBrush.Visual> <Button Content="Button?" /> </VisualBrush.Visual> </VisualBrush> </Page.Background> </Page>



The Visual property of VisualBrush is broken out as a property element. That Visual property is set to a Button element, and that's what the brush looks likea big button. The Button displays the text "Button?" because it's not really a button. It doesn't respond to the mouse or keyboard. It's the visual image of a button, stretched to fill the entire background of the page without regard to the original aspect ratio. Again, if using a button image as a brush is what you want, this is good default behavior.

To complete the package, here's a simple DrawingBrush example.

SimpleDrawingBrush.xaml

[View full width]

<!-- === ================================================== SimpleDrawingBrush.xaml (c) 2006 by Charles Petzold ============================================= ======== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Page.Background> <DrawingBrush> <DrawingBrush.Drawing> <GeometryDrawing Brush="Red"> <GeometryDrawing.Pen> <Pen Brush="Blue" /> </GeometryDrawing.Pen> <GeometryDrawing.Geometry> <EllipseGeometry Center="0 ,0" RadiusX="10" RadiusY="10" /> </GeometryDrawing.Geometry> </GeometryDrawing> </DrawingBrush.Drawing> </DrawingBrush> </Page.Background> </Page>



This is an ellipse centered at the point (0, 0), with a blue outline and a red interior. The entire ellipse is stretched to fill the background of the page, and the width of the blue pen is stretched proportionally. Because the ellipse radii are so small (10 units), the 1-unit default thickness of the blue line around the circumference probably appears quite thick.

Here's a more extensive DrawingBrush object, which is intended to resemble interlocked blue and pink fish swimming in opposite directions.

FishBrush.xaml

[View full width]

<!-- ============================================ FishBrush.xaml (c) 2006 by Charles Petzold ============================================ --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Page.Background> <DrawingBrush> <DrawingBrush.Drawing> <DrawingGroup> <!-- Fill the background with pink. --> <GeometryDrawing Brush="Pink"> <GeometryDrawing.Geometry> <RectangleGeometry Rect="0 0 200 100" /> </GeometryDrawing.Geometry> </GeometryDrawing> <GeometryDrawing Brush="Aqua"> <GeometryDrawing.Pen> <Pen Brush="Blue" Thickness="2" /> </GeometryDrawing.Pen> <GeometryDrawing.Geometry> <GeometryGroup> <!-- Draw the outline of the blue fish. --> <PathGeometry> <PathFigure StartPoint="200 0" IsClosed="True" IsFilled="True"> <BezierSegment Point1="150 100" Point2="50 -50" Point3="0 50" /> <BezierSegment Point1="50 150" Point2="150 0" Point3="200 100" /> </PathFigure> </PathGeometry> <!-- Draw the fish eyes. --> <EllipseGeometry Center="35 35" RadiusX="5" RadiusY="5" /> <EllipseGeometry Center="165 85" RadiusX="5" RadiusY="5" /> </GeometryGroup> </GeometryDrawing.Geometry> </GeometryDrawing> </DrawingGroup> </DrawingBrush.Drawing> </DrawingBrush> </Page.Background> </Page>



The XAML here looks rather deeply nested, but the overall structure is the same as in the previous examples. The file defines a Page with its Background property set to a DrawingBrush. The Drawing property of the DrawingBrush is set to a DrawingGroup containing two GeometryDrawing objects. The first object defines a rectangle 200 units wide and 100 units high. The second contains a PathGeometry that draws the outline of a fish and contains two EllipseGeometry elements that draw the fish eyes.

When you run this program in XAML Cruncher, you'll see the figure fill up the page. In the center is a blue fish swimming left. On the bottom is half a pink fish swimming right, and on the top is another pink half fish swimming right. Obviously this drawing was designed for tiling, but that's not something that happens by default.

Based on the coordinates of the various Geometry objects in FishBrush.xaml, it's easy to determine that the figure is 200 units wide and 100 units high. But as a result of the physical thickness of the pen and the default LineJoin property of Miter, the Drawing object is actually a little larger that those dimensions. To determine the actual dimensions of the Drawing object, you can give the DrawingGroup element a name:

<DrawingGroup x:Name="drawgrp"> 


You can then insert this Label control right before the Page end tag:

<Label Content="{Binding ElementName=drawgrp, Path=Bounds}" /> 


The drawing is closer to 202 units wide and 108 units high. In XAML Cruncher you can actually see a white perimeter around the pink rectangle where the blue pen extends. You can reduce that white perimeter considerably by giving the pen a LineJoin property of Bevel:

<Pen Brush="Blue" Thickness="2" LineJoin="Bevel" /> 


And you might start fiddling with some coordinates so that the Drawing is really 200 units wide by 100 units high, but don't bother. The white perimeter will be annoying at first, but eventually you'll see a way to make it disappear.

So you now have XAML files containing ImageBrush, VisualBrush, and DrawingBrush objects. Each of these brushes is based on an object that has a distinct metrical size. For the bitmap in the ImageBrush, that metrical size is calculated by dividing the pixel dimensions of the bitmap by the resolution in dots per inch. For the Button in the VisualBrush, that metrical size is the size of the Button as determined by its MeasureOverride method. For the Drawing object in the DrawingBrush, the metrical size is available from the Bounds property of the Drawing object. In all cases, that metrical size and aspect ratio is ignored, and the object is stretched to fill the area covered by the brush.

Let's start playing. All the properties I'll be describing in this discussion of brushes are defined by TileBrush, so you can set these properties in the ImageBrush, VisualBrush, or DrawingBrush elements of the four XAML files that use these brushes.

The default stretching behavior is governed by the property defined by the TileBrush named Stretch, which you set to a member of the Stretch enumeration. The default is Stretch.Fill, which stretches without regard to aspect ratio.

You might try the other Stretch options in either (or all) of the XAML files. Try this in SimpleImageBrush.xaml:

<ImageBrush Stretch="Uniform" ... 


Or try this in SimpleVisualBrush.xaml:

<VisualBrush Stretch="Uniform"> 


Or try this in SimpleDrawingBrush.xaml or FishBrush.xaml:

<DrawingBrush Stretch="Uniform"> 


In all cases, you'll see the image (or visual or drawing) assume its correct aspect ratio. The brush still covers the entire background of the page, but the image on the brush does not. The bitmap, button, or drawing fills the background in one dimension but not the other. In the other dimension, the brush is transparent on both sides of the bitmap, button, or drawing.

If you set Stretch to Uniform, and the image occupies the full height of the page, you can move it to the left or right side of the brush by setting the AlignmentX property defined by TileBrush to the Left or Right members of the AlignmentX enumeration. The default is Center. Similarly, if the image occupies the full width of the page, you can move it to the top or bottom side by setting AlignmentY to Top or Bottom.

Another option for Stretch is UniformFill. In this case the image (or visual or drawing) has a correct aspect ratio, but it fills the background anyway through truncation. By default, the image is centered in the brush. If the image fits within the height of the page, and parts of the left and right side are truncated, you can set AlignmentX to Left to truncate only the right side. Similarly, if parts of the top and bottom are truncated and you set AlignmentY to Top, you truncate only the bottom part of the image.

The final option for Stretch is None, which makes the image (or visual or drawing) become its metrical size and sit in the center of the brush. You can move it to one of the sides or corners with a combination of AlignmentX and AlignmentY.

One important option of the classes that derive from TileBrush is, of course, tiling, which means that the image (or visual or drawing) repeats itself in the horizontal and vertical dimensions. You control tiling (in part) with the TileMode property, which you set to a member of the TileMode enumeration. The default TileMode setting is TileMode.None. To enable tiling, you usually set the TileMode property to TileMode.Tile.

By itself, setting TileMode to TileMode.Tile doesn't seem to do anything. By default, the size of each tile is assumed to be the same as the size of the brush, so each tile is the size of the page, and that means you're seeing only one tile.

To get multiple tiles, you must set the Viewport and ViewportUnits properties. But first you must make a decision about these tiles. Do you want to fix the number of tiles that comprise the brush? Or do you want to fix the metrical size of the tiles?

Suppose you want an ImageBrush to always have 10 tiles horizontally and 5 tiles vertically, regardless of the size of the brush. The implication is that you want the width of each tile to be one-tenth of the total width of the brush and the height of each tile to be one-fifth (or 20 percent) of the total height of the brush. You set the Viewport property to indicate those dimensions:

<ImageBrush TileMode="Tile" Viewport="0 0 0.1 0.2" ... 


By default, the coordinate system associated with the Viewport property is based on the total size of the brush, just like the points you specify for gradient brushes. The Viewport property is of type Rect, but the Left and Right properties of Rect (the first two numbers in the string you set to the Viewport property) are ignored here. The Width and Height properties of Rect indicate the width and height of the tile as a fraction of the width and height of the brush. The default Viewport is equivalent to the string "0 0 1 1", which makes the tile the same size as the brush.

With the VisualBrush example, here's how you make an array of 10 buttons horizontally and 10 buttons vertically:

<VisualBrush TileMode="Tile" Viewport="0 0 0.1 0.1"> 


In FishBrush.xaml, you can make 25 tiles horizontally and 33.33 tiles vertically with this markup:

<DrawingBrush TileMode="Tile" Viewport="0 0 0.04 0.03"> 


Here the white space around each tile will seem quite unpleasant and will interfere with the tiled effect. But please be patient.

When you use Viewport in its default mode, the dimension of each tile is based on the dimension of the brush and the Viewport dimensions, so the image in each tile will (in general) be distorted. If you want to fix the number of horizontal and vertical tiles, but you also want to preserve the aspect ratio of the original image, you can set Stretch to Uniform (in which case there will be some blank space on the sides) or UniformToFill (which will truncate part of the image). But don't set Stretch to None, because that causes the image to be displayed in its metrical size. It's likely that the tile will be smaller than the unstretched image and thus will contain only a little part of that image.

If you want each tile to be a fixed size, you will probably set that size proportional to the metrical size of the image so the aspect ratio is preserved. You can use device-independent units in the Viewport property if you set the ViewportUnits property to the enumeration value BrushMappingMode.Absolute. (The default is BrushMappingMode.RelativeToBoundingBox.) This is the same enumeration you use to set the MappingMode property defined by GradientBrush.

The image displayed by the SimpleImageBrush.xaml file has an aspect ratio of approximately 1:1.19, so the following markup sets each tile to a width of about one-half inch and a height about 19 percent greater:

<ImageBrush TileMode="Tile" Viewport="0 0 50 60" ViewportUnits="Absolute" ... 


When setting ViewportUnits to Absolute, the first two numbers of Viewport make a difference in how the tile at the upper-left corner of the brush is displayed. With values set to 0 and 0, the upper-left corner of the upper-left tile is positioned at the upper-left corner of the brush. But try values of 25 and 30:

<ImageBrush TileMode="Tile" Viewport="25 30 50 60" ViewportUnits="Absolute" ... 


The tiles are the same size, but the whole array of tiles is shifted 25 units left and 30 units up, so you see only the lower-right quadrant of the tile at the upper-left corner of the brush.

The drawing displayed by FishBrush.xaml has an aspect ratio of 2:1, so it's easy to size the tile appropriately:

<DrawingBrush TileMode="Tile" Viewport="0 0 60 30" ViewportUnits="Absolute"> 


TileBrush defines two properties similar to Viewport and ViewportUnits, called Viewbox and ViewboxUnits. The Viewbox specifies a rectangle within the image (or drawing or visual) that is used for tiling. By default, you specify it in coordinates relative to the bounding box. Here's some markup for SimpleImageBrush.xaml:

<ImageBrush TileMode="Tile" Viewport="0 0 50 60" ViewportUnits="Absolute"             Viewbox="0.3 0.05 0.4 0.6" ... 


In this case, only 40 percent of the width of the bitmap is used for tiling, starting 30 percent from the left side. Only 60 percent of the height of the bitmap is used, starting 5 percent from the top. You can use negative values for the first two numbers, or values greater than 1 for the second two numbers, which both have the effect of creating some transparent areas around the image.

Using Viewbox in Absolute mode lets you eliminate the blank areas around the FishBrush.xaml tiles caused by the finite pen width:

<DrawingBrush TileMode="Tile" Viewport="0 0 60 30" ViewportUnits="Absolute"               Viewbox="0 0 200 100" ViewboxUnits="Absolute"> 


I've already discussed the two members of the TileMode enumeration named None and Tile. The enumeration has three additional members. The FlipX option causes every other column of tiles to be flipped around its vertical axis, like a mirror image. The FlipY option causes every other row of tiles to be flipped upside down. Combine the effects by using FlipXY. Sometimes you can turn an otherwise normal image into an almost abstract design. Try this use of FlipXY in SimpleImageBrush.xaml:

<ImageBrush TileMode="FlipXY" Viewport="0 0 50 60" ViewportUnits="Absolute"             Viewbox="0.3 0.10 0.4 0.6" ... 


One interesting application of a VisualBrush is in decorating a user interface with a shadow or a reflection of controls and elements. The following stand-alone XAML file creates a StackPanel containing two TextBlock elements and three CheckBox controls. Another StackPanel is colored with a VisualBrush background that shows a reflection of those controls. Notice the Visual property of the VisualBrush is set to a binding of the StackPanel containing the controls. An OpacityMask that's based on a LinearGradientBrush that make the image fade near the bottom.

ReflectedControls.xaml

[View full width]

<!-- === ================================================= ReflectedControls.xaml (c) 2006 by Charles Petzold ============================================= ======= --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Page.Resources> <Style TargetType="{x:Type TextBlock}"> <Setter Property="VerticalAlignment" Value="Bottom" /> <Setter Property="FontFamily" Value="Lucida Calligraphy" /> <Setter Property="FontSize" Value="36" /> </Style> <Style TargetType="{x:Type CheckBox}"> <Setter Property="FontSize" Value="24" /> <Setter Property="Margin" Value="12" /> </Style> </Page.Resources> <!-- StackPanel for controls and their reflections. --> <StackPanel> <!-- StackPanel for controls. --> <StackPanel Name="pnlControls" Orientation="Horizontal" HorizontalAlignment="Center"> <TextBlock Text="Check..." /> <StackPanel HorizontalAlignment="Center"> <CheckBox Content="CheckBox 1" /> <CheckBox Content="CheckBox 2" /> <CheckBox Content="CheckBox 3" /> </StackPanel> <TextBlock Text="...Boxes" /> </StackPanel> <!-- StackPanel for reflection. --> <StackPanel Height="{Binding ElementName=pnlControls, Path=ActualHeight}"> <!-- VisualBrush inverts image of controls. --> <StackPanel.Background> <VisualBrush Visual="{Binding ElementName=pnlControls}" Stretch="None"> <VisualBrush.RelativeTransform> <TransformGroup> <ScaleTransform ScaleX="1" ScaleY="-1" /> <TranslateTransform Y="1" /> </TransformGroup> </VisualBrush.RelativeTransform> </VisualBrush> </StackPanel.Background> <!-- OpacityMask makes it fade. --> <StackPanel.OpacityMask> <LinearGradientBrush StartPoint="0 0" EndPoint="0 1"> <GradientStop Offset="0" Color="#80000000" /> <GradientStop Offset="1" Color="#00000000" /> </LinearGradientBrush> </StackPanel.OpacityMask> </StackPanel> </StackPanel> </Page>



Although the CheckBox buttons in the VisualBrush are nonfunctional, they dynamically change their appearance when the real CheckBox buttons are checked.

You can animate a Geometry that is the basis of a DrawingBrush, although this next little program might persuade you not to do so.

AnimatedDrawingBrush.xaml

[View full width]

<!-- === ==================================================== AnimatedDrawingBrush.xaml (c) 2006 by Charles Petzold ============================================= ========== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Page.Background> <DrawingBrush TileMode="Tile" Stretch="None" Viewport="0 0 12 12" ViewportUnits="Absolute"> <DrawingBrush.Drawing> <GeometryDrawing Brush="Blue"> <GeometryDrawing.Geometry> <EllipseGeometry x :Name="elipsgeo" Center="0 0" /> </GeometryDrawing.Geometry> </GeometryDrawing> </DrawingBrush.Drawing> </DrawingBrush> </Page.Background> <Page.Triggers> <EventTrigger RoutedEvent="Page.Loaded"> <BeginStoryboard> <Storyboard TargetName="elipsgeo" RepeatBehavior="Forever"> <DoubleAnimation Storyboard .TargetProperty="RadiusX" From="4" To="6" Duration="0:0:0.25" AutoReverse="True" /> <DoubleAnimation Storyboard .TargetProperty="RadiusY" From="6" To="4" Duration="0:0:0.25" AutoReverse="True" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Page.Triggers> </Page>



The final program in this book doesn't animate a DrawingBrush, but it does animate an Ellipse element filled with a DrawingBrush. It is a program I call HybridClock, and it's compiled as a XAML Browser Application. The clock contains a round face like a normal analog clock, but it has only one hand that sweeps around the clock to indicate the seconds. The clock hand itself is a digital clock of sorts because it displays a text string that provides the current date and time. A context menu lets you pick a preferred date and time format.

The following class provides the clock with the current date and time formatted in a string. The class implements the INotifyPropertyChanged interface and provides a PropertyChanged event for the DateTime property, so this class can be a source in a data binding. This is how the date and time displayed by the clock's hand is updated.

ClockTicker.cs

[View full width]

//-------------------------------------------- // ClockTicker.cs (c) 2006 by Charles Petzold //-------------------------------------------- using System; using System.ComponentModel; using System.Windows.Threading; namespace Petzold.HybridClock { public class ClockTicker : INotifyPropertyChanged { string strFormat = "F"; // Event required for interface. public event PropertyChangedEventHandler PropertyChanged; // Public property. public string DateTime { get { return System.DateTime.Now .ToString(strFormat); } } public string Format { set { strFormat = value; } get { return strFormat; } } // Constructor. public ClockTicker() { DispatcherTimer timer = new DispatcherTimer(); timer.Tick += TimerOnTick; timer.Interval = TimeSpan.FromSeconds (0.10); timer.Start(); } // Timer event handler triggers PropertyChanged event. void TimerOnTick(object sender, EventArgs args) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs ("DateTime")); } } }



The HybridClock project includes a small application definition file that simply references the only other XAML file in the project.

HybridClockApp.xaml

[View full width]

<!-- ================================================= HybridClockApp.xaml (c) 2006 by Charles Petzold ============================================= ==== --> <Application x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.HybridClockApp" xmlns="http://schemas.microsoft.com/winfx/2006 /xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml" StartupUri="HybridClockPage.xaml" />



When considering how this clock would work, I knew that I wanted to enclose the whole thing in a Viewbox so it would get larger and smaller as the user changed the window size. But I also recognized that the text string that formed the clock's single hand would cause scaling problems. If the user selected a date and time format that included the text month, the length of the string would change as the month changed. The length of the string would also change when the time went from 12:59 to 1:00 or when the date advanced from the ninth of the month to the tenth. For the most part, I wanted the XAML file to compensate for these changes.

The total clock contains no fewer than eight TextBlock elements, each of which displays the same date and time string. The first of these eight TextBlock elements contains a binding to the ClockTicker object; the others contain a binding to the Text property of that first TextBlock. Each TextBlock exists in a side-by-side pair with another TextBlock. One pair is used to set the horizontal size of the clock face, and another pair is used to set the vertical size. These four TextBlock elements are on a transparent panel. The two other pairs rotate in unison around their mutual centers. Only one TextBlock is visible at any time. When the hand is straight up or straight down, an animation causes one to fade out and another to fade in so that the text is never upside down.

HybridClockPage.xaml

[View full width]

<!-- === =============================================== HybridClockPage.xaml (c) 2006 by Charles Petzold ============================================= ===== --> <Page x: xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml" xmlns:src="/books/4/266/1/html/2/clr-namespace:Petzold.HybridClock" WindowTitle="Hybrid Analog/Digital Clock" Title="Hybrid Analog/Digital Clock" Background="Pink"> <!-- Resource for class that provides current time. --> <Page.Resources> <src:ClockTicker x:Key="clock" /> </Page.Resources> <!-- ToolTip has program copyright notice. --> <Page.ToolTip> <TextBlock TextAlignment="Center"> Hybrid Analog/Digital Clock <LineBreak />&#x00A9; 2006 by Charles Petzold <LineBreak />www.charlespetzold.com </TextBlock> </Page.ToolTip> <Viewbox> <!-- Outer single-cell grid encompasses the whole clock face. --> <Grid> <Ellipse> <Ellipse.Fill> <SolidColorBrush Color="{x:Static src:HybridClockPage .clrBackground}" /> <!-- Unfortunately, this radial brush hurt performance. --> <!-- RadialGradientBrush GradientOrigin="0.4, 0.4"> <RadialGradientBrush .GradientStops> <GradientStop Offset="0" Color="White" /> <GradientStop Offset="1" Color="{x:Static src :HybridClockPage.clrBackground}" /> </RadialGradientBrush .GradientStops> </RadialGradientBrush --> </Ellipse.Fill> </Ellipse> <!-- This inner single-cell Grid is sized by the text width. --> <Grid Name="grd" Margin="12"> <!-- Make two invisible horizontal date/time strings. --> <StackPanel Orientation="Horizontal" Opacity="0" VerticalAlignment="Center"> <TextBlock Name="datetime" Text="{Binding Source={StaticResource clock}, Path=DateTime}" /> <TextBlock Text="{Binding ElementName=datetime, Path=Text}" /> </StackPanel> <!-- Make two invisible vertical date/time strings. --> <StackPanel Orientation="Horizontal" Opacity="0" HorizontalAlignment="Center"> <TextBlock Text="{Binding ElementName=datetime, Path=Text}" /> <TextBlock Text="{Binding ElementName=datetime, Path=Text}" /> <StackPanel.LayoutTransform> <RotateTransform Angle="90" /> </StackPanel.LayoutTransform> </StackPanel> <!-- Make two rotatable date/time strings. --> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" StackPanel .RenderTransformOrigin="0.5 0.5"> <TextBlock Text="{Binding ElementName=datetime, Path=Text}" Opacity="0" /> <TextBlock Text="{Binding ElementName=datetime, Path=Text}" Name="txt1" Opacity="0.5"/> <StackPanel.RenderTransform> <RotateTransform x :Name="xform1"/> </StackPanel.RenderTransform> </StackPanel> <!-- Make two more rotatable date/ time strings. --> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" RenderTransformOrigin="0.5 0.5"> <TextBlock Text="{Binding ElementName=datetime, Path=Text}" Name="txt2" Opacity="0.5" /> <TextBlock Text="{Binding ElementName=datetime, Path=Text}" Opacity="0" /> <StackPanel.RenderTransform> <RotateTransform x :Name="xform2"/> </StackPanel.RenderTransform> </StackPanel> </Grid> <!-- Rotatable mask (defined in code). --> <Ellipse Name="mask" RenderTransformOrigin="0.5 0.5" > <Ellipse.RenderTransform> <RotateTransform x:Name="xform3"/> </Ellipse.RenderTransform> </Ellipse> </Grid> </Viewbox> <Page.Triggers> <EventTrigger RoutedEvent="Page.Loaded"> <BeginStoryboard> <Storyboard x:Name="storyboard"> <!-- Double animations turn the text and opacity mask. --> <DoubleAnimation Storyboard .TargetName="xform1" Storyboard .TargetProperty="Angle" From="-90" To="270" Duration="0:1:0" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard .TargetName="xform2" Storyboard .TargetProperty="Angle" From="-270" To="90" Duration="0:1:0" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard .TargetName="xform3" Storyboard .TargetProperty="Angle" From="-90" To="270" Duration="0:1:0" RepeatBehavior="Forever" /> <!-- Key frame animations do fades. --> <DoubleAnimationUsingKeyFrames Storyboard .TargetName="txt1" Storyboard .TargetProperty="Opacity" Duration="0:1:0" RepeatBehavior="Forever"> <LinearDoubleKeyFrame Value="1" KeyTime="0:0:0.5" /> <DiscreteDoubleKeyFrame Value="1" KeyTime="0:0:29.5" /> <LinearDoubleKeyFrame Value="0" KeyTime="0:0:30.5" /> <DiscreteDoubleKeyFrame Value="0" KeyTime="0:0:59.5" /> <LinearDoubleKeyFrame Value="0.5" KeyTime="0:1:0" /> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard .TargetName="txt2" Storyboard .TargetProperty="Opacity" Duration="0:1:0" RepeatBehavior="Forever"> <LinearDoubleKeyFrame Value="0" KeyTime="0:0:0.5" /> <DiscreteDoubleKeyFrame Value="0" KeyTime="0:0:29.5" /> <LinearDoubleKeyFrame Value="1" KeyTime="0:0:30.5" /> <DiscreteDoubleKeyFrame Value="1" KeyTime="0:0:59.5" /> <LinearDoubleKeyFrame Value="0.5" KeyTime="0:1:0" /> </DoubleAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Page.Triggers> </Page>



Because the size of the clock face is based on the length of the text displayed by the clock hand, any changes to the length of text (for example, when one month ends and another starts) results in a change to the size of the clock face. However, the clock face is inside a Viewbox, and the Viewbox immediately adjusts its contents so it appears to maintain the same size. What seems to change is the font size of the text relative to the size of the clock face. (You can see this more dramatically if you right-click the clock and select a much shorter or longer date and time format.)

These dramatic changes in the actual size of the clock face prevented me from including the clock ticks in the XAML file. I decided to do those in code and to change them whenever the length of the text string changed. I also included a rotating ellipse in the XAMLthis is the ellipse that's rotated with the third DoubleAnimation objectfilled with a brush with a varying amount of transparency. This brush seems to make the tick marks pop into view as the hand approaches them and then to slowly fade out. This brush had to be recalculated in code whenever the text length changed.

Here's the code part of the HybridClock project. Besides handling the tick marks and the partially transparent ellipse, it also manages the context menu.

HybridClockPage.xaml.cs

[View full width]

//--------- -------------------------------------------- // HybridClockPage.xaml.cs (c) 2006 by Charles Petzold //------------------------------------------------ ----- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; namespace Petzold.HybridClock { public partial class HybridClockPage : Page { // Initialize colors for XAML access. public static readonly Color clrBackground = Colors.Aqua; public static readonly Brush brushBackground = Brushes.Aqua; // Save transforms for clock ticks. TranslateTransform[] xform = new TranslateTransform[60]; public HybridClockPage() { InitializeComponent(); // Set the Storyboard BeginTime property. storyboard.BeginTime = -DateTime.Now .TimeOfDay; // Set handler for context menu Opened event. ContextMenu menu = new ContextMenu(); menu.Opened += ContextMenuOnOpened; ContextMenu = menu; // Set handler for Loaded event. Loaded += WindowOnLoaded; } void WindowOnLoaded(object sender, EventArgs args) { // Create tick marks around clock. for (int i = 0; i < 60; i++) { Ellipse elips = new Ellipse(); elips.HorizontalAlignment = HorizontalAlignment.Center; elips.VerticalAlignment = VerticalAlignment.Center; elips.Fill = Brushes.Blue; elips.Width = elips.Height = i % 5 == 0 ? 6 : 2; TransformGroup group = new TransformGroup(); group.Children.Add(xform[i] = new TranslateTransform (datetime.ActualWidth, 0)); group.Children.Add( new TranslateTransform(grd .Margin.Left / 2, 0)); group.Children.Add( new TranslateTransform(-elips .Width / 2, -elips.Height /2)); group.Children.Add( new RotateTransform(i * 6)); group.Children.Add( new TranslateTransform(elips .Width / 2, elips.Height /2)); elips.RenderTransform = group; grd.Children.Add(elips); } // Make the opacity mask. MakeMask(); // Set event handler for change in size of the date/time string. datetime.SizeChanged += DateTimeOnSizeChanged; } // Date/time string changes: Recalculate transforms and mask. void DateTimeOnSizeChanged(object sender, SizeChangedEventArgs args) { if (args.WidthChanged) { for (int i = 0; i < 60; i++) xform[i].X = datetime.ActualWidth; MakeMask(); } } // Make the opacity mask. void MakeMask() { DrawingGroup group = new DrawingGroup(); Point ptCenter = new Point(datetime .ActualWidth + grd.Margin.Left, datetime .ActualWidth + grd.Margin.Left); // Calculate 256 wedges around circle. for (int i = 0; i < 256; i++) { Point ptInner1 = new Point(ptCenter.X + datetime.ActualWidth * Math.Cos(i * 2 * Math.PI / 256), ptCenter.Y + datetime.ActualWidth * Math.Sin(i * 2 * Math.PI / 256)); Point ptInner2 = new Point(ptCenter.X + datetime.ActualWidth * Math.Cos((i + 2) * 2 * Math.PI / 256), ptCenter.Y + datetime.ActualWidth * Math.Sin((i + 2) * 2 * Math.PI / 256)); Point ptOuter1 = new Point(ptCenter.X + (datetime .ActualWidth + grd.Margin.Left) * Math.Cos(i * 2 * Math.PI / 256), ptCenter.Y + (datetime .ActualWidth + grd.Margin.Left) * Math.Sin(i * 2 * Math.PI / 256)); Point ptOuter2 = new Point(ptCenter.X + (datetime .ActualWidth + grd.Margin.Left) * Math.Cos((i + 2) * 2 * Math.PI / 256), ptCenter.Y + (datetime .ActualWidth + grd.Margin.Left) * Math.Sin((i + 2) * 2 * Math.PI / 256)); PathSegmentCollection segcoll = new PathSegmentCollection(); segcoll.Add(new LineSegment (ptInner2, false)); segcoll.Add(new LineSegment (ptOuter2, false)); segcoll.Add(new LineSegment (ptOuter1, false)); segcoll.Add(new LineSegment (ptInner1, false)); PathFigure fig = new PathFigure (ptInner1, segcoll, true); PathFigureCollection figcoll = new PathFigureCollection(); figcoll.Add(fig); PathGeometry path = new PathGeometry(figcoll); byte byOpacity = (byte)Math.Min (255, 512 - 2 * i); SolidColorBrush br = new SolidColorBrush( Color.FromArgb(byOpacity, clrBackground.R, clrBackground.G , clrBackground.B)); GeometryDrawing draw = new GeometryDrawing(br, new Pen(br, 2), path); group.Children.Add(draw); } DrawingBrush brush = new DrawingBrush (group); mask.Fill = brush; } // Initialize context menu for date/time format. void ContextMenuOnOpened(object sender, RoutedEventArgs args) { ContextMenu menu = sender as ContextMenu; menu.Items.Clear(); string[] strFormats = { "d", "D", "f", "F", "g", "G", "M", "R", "s", "t", "T", "u", "U", "Y" }; foreach (string strFormat in strFormats) { MenuItem item = new MenuItem(); item.Header = DateTime.Now .ToString(strFormat); item.Tag = strFormat; item.IsChecked = strFormat == (Resources["clock"] as ClockTicker).Format; item.Click += MenuItemOnClick; menu.Items.Add(item); } } // Process clicked item on context menu. void MenuItemOnClick(object sender, RoutedEventArgs args) { MenuItem item = (sender as MenuItem); (Resources["clock"] as ClockTicker) .Format = item.Tag as string; } } }



The MakeMask method is easily the most complex part of this job. It is responsible for making the DrawingBrush object that is used to set the Fill property of the Ellipse element identified in the XAML file with the name "mask." This DrawingBrush object is mostly transparent except for wedges around the circumference of a circle. These wedges coincide with the tick marks displayed by the clock. The wedges are colored the same as the background of the clock face and are gradated in transparency. The result is that only half the tick marks behind the Ellipse are visible, and those that are visible range from fully visible to fully transparent.

The real magic occurs when this Ellipse is rotated and the individual tick marks seem to suddenly pop into view and then fade away. An alternative (perhaps practical only if there were much fewer tick marks) would be to animate the Opacity property of each one independently. But I rather prefer my solution.

HybridClock is built mostly from elements that are hidden from view. These invisible elements provide structure to the visible elements, and sometimes (as with the clock hand as it passes 6:00 and 12:00) the visible and invisible change roles, mutually offering support for the sake of an overall effect.




Applications = Code + Markup. A Guide to the Microsoft Windows Presentation Foundation
Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation (Pro - Developer)
ISBN: 0735619573
EAN: 2147483647
Year: 2006
Pages: 72

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net