Chapter 27. Graphical Shapes


The world of two-dimensional computer graphics is roughly divided between raster graphics (bitmaps) and vector graphics (lines, curves, and filled areas), but the key word in this statement is roughly. Considerable overlap exists between these two poles. When an ellipse is filled with a brush based on a bitmap, is that raster graphics or vector graphics? It's a little bit of both. When a bitmap is based on a vector drawing, is that raster graphics or vector graphics? Again, it's a little bit of both.

So far in this book, I've shown you how to use the Image element to display bitmaps and various classes from the System.Windows.Shapes namespace (also known as the Shapes library) to display vector graphics. These classes might be all that you'll ever need for applications that make only a modest use of graphics. However, these high-level classes are really just the tip of the graphics iceberg in the Windows Presentation Foundation. In the chapters ahead I will explore WPF graphicsincluding animationin more detail, culminating with the merging of raster graphics and vector graphics in images, drawings, and brushes.

In this chapter, I want to examine the Shapes library more rigorously than I have in previous chapters. What makes the Shapes library convenient is that the classes derive from FrameworkElement, as shown in the following class hierarchy:

Object

    DispatcherObject (abstract)

        DependencyObject

             Visual (abstract)

                  UIElement

                       FrameworkElement

                            Shape (abstract)

                                 Ellipse

                                 Line

                                 Path

                                 Polygon

                                 Polyline

                                 Rectangle

As a result of this illustrious ancestry, objects based on the Shape classes can render themselves on the screen and handle mouse, stylus, and keyboard input, much like regular controls.

The Shape class defines two properties of type Brush: Stroke and Fill. The Stroke property indicates the brush used for drawing lines (including the outlines of the Ellipse, Rectangle, and Polygon), while the Fill property is the brush that fills interiors. By default, both of these properties are null, and if you don't set one or the other you won't see the object.

Although it's not immediately obvious until you begin studying all of their properties, the classes that derive from Shape reveal two different rendering paradigms. The Line, Path, Polygon, and Polyline classes all include properties that let you define the object in terms of two-dimensional coordinate pointseither Point objects or something equivalent. For example, Line contains properties named X1, Y1, X2, and Y2 for the beginning and end points of the line.

However, Ellipse and Rectangle are different. You don't define these objects in terms of points. If you want these graphical objects to be a particular size, you use the Width and Height properties that these classes inherit from FrameworkElement. But you aren't required to set these properties. Very early in this book I demonstrated how to create an Ellipse object and set it as the content of a window so that the Ellipse fills the client area.

Getting a graphical object on the screen with the ease of Ellipse is certainly satisfying and convenient. But generally you want to combine several graphical objects into a composite image, probably with some overlap. To display multiple graphical objects you need a panel that can accommodate overlapping children. Panels such as DockPanel, StackPanel, WrapPanel, Grid, and UniformGrid are very good at keeping elements separated from each other, and for this reason, they're usually not quite adequate for generalized graphics programming.

The Canvas panel is excellent for displaying graphics because that's what it was made for. When displaying an Ellipse or a Rectangle object on a Canvas panel, you must set the Width and Height properties or the object will have a zero dimension and will not be visible. (Well, that's not entirely true. The MinWidth and MinHeight properties also suffice, and an object with zero dimensions might be visible if the Stroke property isn't null and the StrokeThickness is greater than 1. But you see the point.)

In addition to setting the Width and Height of the Ellipse or Rectangle, you'll probably want to set the attached properties Canvas.Left and Canvas.Top. These indicate the location of the upper-left corner of the Ellipse or Rectangle relative to the upper-left corner of the Canvas. Instead of Canvas.Left you can use Canvas.Right to indicate the location of the right side of the object relative to the right side of the Canvas. Rather than Canvas.Top you can use Canvas.Bottom to position the bottom of the object relative to the bottom of the Canvas.

With the other Shape derivativesLine, Path, Polygon, and Polylineit isn't necessary to set the Canvas attached properties because the coordinates of the object indicate its position on the Canvas. For example, here's a Line element in XAML:

<Line X1="100" Y1="50" X2="400" Y2="100" Stroke="Blue" /> 


The start of that line is the point (100, 50), and that means that the line begins 100 device-independent units from the left side of the Canvas and 50 units from the top. The end of the line is the point (400, 100), also relative to the top-left corner of the Canvas.

Although the Canvas attached properties aren't required on the Line element, you can include them. For example:

<Line X1="100" Y1="50" X2="400" Y2="100" Stroke="Blue"       Canvas.Left="25" Canvas.Top="150" /> 


In effect, the Canvas.Left value is added to all the X coordinates of the line, and Canvas.Top is added to all the Y coordinates. The line is shifted 25 units to the right and 150 units down. It now begins at the point (125, 200) and ends at (425, 250) relative to the upper-left corner of the Canvas. The Canvas.Left and Canvas.Top properties can be negative to shift the element left and up.

If you set Canvas.Right rather than Canvas.Left, the rightmost point of the line will be shifted so that it is Canvas.Right units to the left of the right side of the Canvas. Set Canvas.Right to a negative value to shift the rightmost point of the object beyond the right side of the Canvas. The Canvas.Bottom property works similarly for positioning an element relative to the bottom of the Canvas.

Canvas is not the only type of panel that can accommodate children in the freeform manner required of most graphics programming. A single cell of a Grid panel can also host multiple children, and in some cases you might find a Grid cell convenient for this purpose. It's OK!

You can put multiple Line, Path, Polygon, and Polyline elements in a Grid cell, and their coordinates will position them relative to the upper-left corner of the cell. However, if you set HorizontalAlignment of any of these elements to Right, the object will be positioned at the right of the cell. If you set HorizontalAlignment to Center, the object will be positioned in the center of the cell. But not quite. The width of the object is calculated as the distance from 0 to the rightmost X coordinate. For example, the first Line element shown in the preceding example would be treated as if it had a width of 400, and that total width is what's centered, so the object itself will be somewhat to the right of center. Similar logic is used for VerticalAlignment.

Multiple Ellipse and Rectangle objects don't work very well in a Grid cell. You can set the Width and Height of these elements, and a Margin property as well, but you're really left with HorizontalAlignment and VerticalAlignment to position them, and that's just not very versatile.

Let's take a closer look at the Polygon and Polyline classes. (I'll save Path for the next chapter.) The Polygon and Polyline classes are very similar. Each has a Points property of type PointCollection. As you discovered in Chapter 20, in XAML you can supply an array of points by alternating X and Y coordinate values:

Points="100 50 200 50 200 150 100 150" 


These are the four points of a square with an upper-left corner at (100, 50) and a lower-right corner at (200, 150). Of course, you can append a px to a number to more explicitly indicate device-independent coordinates, or you can use cm for centimeters, in for inches, or pt for points (1/72 inch).

You can use commas to separate the values or to separate the X and Y coordinates in each point, or to separate the points:

Points="100 50, 200 50, 200 150, 100 150" 


If necessary, the string can run to multiple lines.

Although both Polygon and Polyline draw a series of straight lines, one common use of these classes is drawing curves. The trick is to make the individual lines very short and use plenty of them. Any curve that you can define mathematically you can draw using Polygon or Polyline. Don't hesitate to use hundreds or thousands of points. These classes were made for that purpose.

Of course, specifying thousands of points in a Polyline is only feasible in C# code. The following program draws a sine curve with a positive and negative amplitude of one inch and a period of four inches, with a total horizontal dimension of 2,000 device-independent units, and that's how many points there are.

SineWave.cs

[View full width]

//----------------------------------------- // SineWave.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.Shapes; namespace SineWave { public class SineWave : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new SineWave()); } public SineWave() { Title = "Sine Wave"; // Make Polyline content of window. Polyline poly = new Polyline(); poly.VerticalAlignment = VerticalAlignment.Center; poly.Stroke = SystemColors .WindowTextBrush; poly.StrokeThickness = 2; Content = poly; // Define the points. for (int i = 0; i < 2000; i++) poly.Points.Add( new Point(i, 96 * (1 - Math .Sin(i * Math.PI / 192)))); } } }



Notice that the Polyline is set as the Content property of the Window and that vertical coordinates of the sine curve are scaled to range from 0 to 192. It's therefore possible for the VerticalAlignment property of Polyline to be set to Center to vertically center the sine curve in the window. If the vertical coordinates ranged from 96 to 96 (which can be accomplished by removing the 1 and the minus sign in front of Math.Sin), the curve is treated as if it had a vertical dimension of 96 rather than 192. In effect, negative coordinates are ignored in computing the height of the element for alignment purposes. The result is that the curve is actually positioned above the center of the window.

The SineWave program demonstrates one approach to filling up the Points property of Polyline. I suspect this isn't the most efficient approach. As more points are added, the PointCollection object undoubtedly needs to allocate more space. If you want to fill the Points property like this, it's preferable to indicate the number of points before the for loop begins:

poly.Points = new PointCollection(2000); 


An altogether better approach to defining the Points property is demonstrated in the Spiral program, which uses parametric equations to draw a spiral. In this program, an array of Point objects is allocated, and that array becomes the argument to the PointCollection constructor.

Spiral.cs

[View full width]

//--------------------------------------- // Spiral.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.Shapes; namespace Spiral { public class Spiral : Window { const int revs = 20; const int numpts = 1000 * revs; Polyline poly; [STAThread] public static void Main() { Application app = new Application(); app.Run(new Spiral()); } public Spiral() { Title = "Spiral"; // Make Canvas content of window. Canvas canv = new Canvas(); canv.SizeChanged += CanvasOnSizeChanged; Content = canv; // Make Polyline child of Canvas. poly = new Polyline(); poly.Stroke = SystemColors .WindowTextBrush; canv.Children.Add(poly); // Define the points. Point[] pts = new Point[numpts]; for (int i = 0; i < numpts; i++) { double angle = i * 2 * Math.PI / (numpts / revs); double scale = 250 * (1 - (double) i / numpts); pts[i].X = scale * Math.Cos(angle); pts[i].Y = scale * Math.Sin(angle); } poly.Points = new PointCollection(pts); } void CanvasOnSizeChanged(object sender, SizeChangedEventArgs args) { Canvas.SetLeft(poly, args.NewSize .Width / 2); Canvas.SetTop(poly, args.NewSize .Height / 2); } } }



The Spiral program also demonstrates a different approach to centering the object within the client area. The spiral is drawn on a Canvas but centered around the point (0, 0). Whenever the size of the Canvas changes, the CanvasOnSizeChanged event handler sets the Left and Top attached properties of the Polyline to the center of the Canvas, and the spiral is repositioned.

Polyline is intended to draw a series of connected straight lines and Polygon is intended to define a closed area for filling and possibly outlining. Yet the two classes are more similar than they might seem. Here's a Polygon element that includes two brushes to outline the object and fill it:

<Polygon Points="100 50, 200 50, 200 150, 100 150"          Stroke="Red" Fill="Blue" /> 


You'll notice that the Points collection isn't explicitly closed. You could include a final point of (100, 50) but you don't need to. The Polygon class will automatically construct another line from the end point to the beginning pointin this case a line from (100, 150) to (100, 150)so that it closes the square and defines an enclosed area. The lines defining the square (including the last added line) are colored with the Stroke brush, and the square is filled with the Fill brush.

Although the documentation for Polyline indicates that "setting the Fill property on a Polyline has no effect," this is not true. Given a non-null Fill property, Polyline will fill the same area as Polygon. The only real difference is that Polyline does not automatically add the last line that Polygon adds to the figure, although it fills the area as if it did.

Here's a XAML file that draws my portrait (if I actually happened to have an egg-shaped head) using a combination of Ellipse, Polygon, and Line.

SelfPortraitSansGlasses.xaml

[View full width]

<!-- === ======================================================= SelfPortraitSansGlasses.xaml (c) 2006 by Charles Petzold ============================================= ============= --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <!-- Head. --> <Ellipse Canvas.Left="96" Canvas.Top="96" Width="144" Height="240" Fill="PeachPuff" Stroke="Black" /> <!-- Ears. --> <Polygon Points="100 192, 84 168, 84 240, 100 216" Fill="SandyBrown" Stroke="Black" /> <Polygon Points="236 192, 252 168, 252 240, 236 216" Fill="SandyBrown" Stroke="Black" /> <!-- Eyes. --> <Ellipse Canvas.Left="120" Canvas.Top="168" Width="36" Height="36" Fill="White" Stroke="Black" /> <Ellipse Canvas.Left="180" Canvas.Top="168" Width="36" Height="36" Fill="White" Stroke="Black" /> <!-- Irises. --> <Ellipse Canvas.Left="129" Canvas.Top="177" Width="18" Height="18" Fill="Brown" Stroke="Black" /> <Ellipse Canvas.Left="189" Canvas.Top="177" Width="18" Height="18" Fill="Brown" Stroke="Black" /> <!-- Nose. --> <Polygon Points="168 192, 158 240, 178 240" Fill="Pink" Stroke="Black" /> <!-- Mouth. --> <Ellipse Canvas.Left="120" Canvas.Top="260" Width="96" Height="24" Fill="White" Stroke="Red" StrokeThickness="8" /> <!-- Beard. --> <Line X1="120" Y1="288" X2="120" Y2="336" Stroke="Black" StrokeThickness="2" /> <Line X1="126" Y1="290" X2="126" Y2="338" Stroke="Black" StrokeThickness="2" /> <Line X1="132" Y1="292" X2="132" Y2="340" Stroke="Black" StrokeThickness="2" /> <Line X1="138" Y1="294" X2="138" Y2="342" Stroke="Black" StrokeThickness="2" /> <Line X1="144" Y1="296" X2="144" Y2="344" Stroke="Black" StrokeThickness="2" /> <Line X1="150" Y1="297" X2="150" Y2="345" Stroke="Black" StrokeThickness="2" /> <Line X1="156" Y1="298" X2="156" Y2="346" Stroke="Black" StrokeThickness="2" /> <Line X1="162" Y1="299" X2="162" Y2="347" Stroke="Black" StrokeThickness="2" /> <Line X1="168" Y1="300" X2="168" Y2="348" Stroke="Black" StrokeThickness="2" /> <Line X1="174" Y1="299" X2="174" Y2="347" Stroke="Black" StrokeThickness="2" /> <Line X1="180" Y1="298" X2="180" Y2="346" Stroke="Black" StrokeThickness="2" /> <Line X1="186" Y1="297" X2="186" Y2="345" Stroke="Black" StrokeThickness="2" /> <Line X1="192" Y1="296" X2="192" Y2="344" Stroke="Black" StrokeThickness="2" /> <Line X1="198" Y1="294" X2="198" Y2="342" Stroke="Black" StrokeThickness="2" /> <Line X1="204" Y1="292" X2="204" Y2="340" Stroke="Black" StrokeThickness="2" /> <Line X1="210" Y1="290" X2="210" Y2="338" Stroke="Black" StrokeThickness="2" /> <Line X1="216" Y1="288" X2="216" Y2="336" Stroke="Black" StrokeThickness="2" /> </Canvas>



Because Shape derives from FrameworkElement, you can use Style elements to define common properties of these objects. For example, I could have eliminated the repetitious assignment of the Stroke and StrokeThickness attributes in the Line elements by including a Style like this:

<Style TargetType="{x:Type Line}">     <Setter Property="Stroke" Value="Black" />     <Setter Property="StrokeThickness" Value="2" /> </Style> 


Unfortunately, styles probably wouldn't have reduced the overall length of this particular file, but I'll use styles in many of the upcoming programs in this chapter.

The image drawn by SelfPortraitSansGlasses.xaml has a fixed size. In some cases, you might want the size of a graphical image to scale itself up or down depending on the size of the program's window. You can easily accomplish this feat by putting a fixed-size Canvas inside a Viewbox. The Viewbox automatically resizes its content to its own size. The following rather simpler face demonstrates this technique.

ScalableFace.xaml

[View full width]

<!-- =============================================== ScalableFace.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" Background="White"> <Viewbox> <Canvas Width="100" Height="100"> <Ellipse Canvas.Left="5" Canvas.Top="5" Width="90" Height="90" Stroke="Black" /> <!-- Eyes. --> <Ellipse Canvas.Left="25" Canvas.Top="30" Width="10" Height="10" Stroke="Black" /> <Ellipse Canvas.Right="25" Canvas.Top="30" Width="10" Height="10" Stroke="Black" /> <!-- Eyebrows. --> <Polyline Points="25 25, 30 20, 35 25" Stroke="Black" /> <Polyline Points="65 25, 70 20, 75 25" Stroke="Black" /> <!-- Nose. --> <Polyline Points="50 40, 45 60, 55 60, 50 40" Stroke="Black" /> <!-- Mouth. --> <Polyline Points="25 70 50 80 75 70" Stroke="Black" /> </Canvas> </Viewbox> </Page>



Notice that Viewbox scales everything in the image up and down, including the width of the lines. When drawing such an image, you want to keep the overall size of the image and the line widths fairly consistent. You can use a very large or very small Canvas to draw the figure, but the overall image might look a bit strange unless you also set the StrokeThickness to a commensurate value.

By default, the Viewbox scales equally both horizontally and vertically so that no distortion of the image is introduced. If you don't mind a little distortion and prefer that the image occupy as much space as allowed for it, set the Stretch property to Fill.

Sometimes the lines defining a particular Polyline or Polygon overlap each other, and then issues arise about which enclosed areas are filled and which are not. Both Polyline and Polygon define a property named FillRule that indicates how enclosed areas are to be filled. The two options are the enumeration values FillRule.EvenOdd (the default) and FillRule.NonZero. The classic example to show the difference is a five-pointed star, and that's what the next XAML file displays.

To avoid a lot of repetition, the TwoStars.xaml file begins with a Style definition for the polygons that render the five-pointed star. The setters include the four Polygon properties Points, Fill, Stroke, and StrokeThickness. The coordinates used to define the star are based on a center at the point (0, 0) and a radius of one inch.

TwoStars.xaml

[View full width]

<!-- =========================================== TwoStars.xaml (c) 2006 by Charles Petzold =========================================== --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" TextBlock.FontSize="16"> <Canvas.Resources> <!-- Define properties common to both figures. --> <Style x:Key="star"> <Setter Property="Polygon.Points" Value="0 -96, 56, 78, -91 -30, 91 -30, -56 78" /> <Setter Property="Polygon.Fill" Value="Blue" /> <Setter Property="Polygon.Stroke" Value="Red" /> <Setter Property="Polygon.StrokeThickness" Value="3" /> </Style> </Canvas.Resources> <!-- Draw first figure with "EvenOdd" FillRule . --> <TextBlock Canvas.Left="48" Canvas.Top="24" Text="FillRule = EvenOdd" /> <Polygon Style="{StaticResource star}" FillRule="EvenOdd" Canvas.Left="120" Canvas.Top="168" /> <!-- Draw second figure with "NonZero" FillRule. --> <TextBlock Canvas.Left="288" Canvas.Top="24" Text="FillRule = NonZero" /> <Polygon Style="{StaticResource star}" FillRule="NonZero" Canvas.Left="360" Canvas.Top="168" /> </Canvas>



The file displays two TextBlock elements identifying the FillRule and two renditions of the five-pointed star using Polygon elements. (Notice that the start tag of the root element sets the TextBlock.FontSize property to 12 points.) The first five-pointed star has its FillRule set to "EvenOdd," and the second has FillRule set to "NonZero." The two stars are offset from the location implied by the Points collection using the Canvas.Left and Canvas.Top attached properties.

With the default FillRule of EvenOdd, you can imagine a line drawn from a point within an enclosed area to infinity. The enclosed area is filled only if that imaginary line crosses an odd number of boundary lines. That's why the points of the star are filled but the center is not.

That this algorithm really works is demonstrated a little more forcibly by the EvenOddDemo .xaml file. This file is very similar to TwoStars.xaml except that the figure it draws is a bit more elaborate and results in enclosed areas separated from infinity by up to six lines.

EvenOddDemo.xaml

[View full width]

<!-- ============================================== EvenOddDemo.xaml (c) 2006 by Charles Petzold ============================================= = --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" TextBlock.FontSize="16"> <Canvas.Resources> <!-- Define properties common to both figures. --> <Style x:Key="figure"> <Setter Property="Polygon.Points" Value=" 0 0, 0 144, 144 144, 144 24, 24 24, 24 168, 168 168, 168 48, 48 48, 48 192, 192 192, 192 72, 72 72, 72 216, 216 216, 216 96, 96 96, 96 240, 240 240, 240 120, 120 120, 120 264, 264 264, 264, 0" /> <Setter Property="Polygon.Fill" Value="Blue" /> <Setter Property="Polygon.Stroke" Value="Red" /> <Setter Property="Polygon.StrokeThickness" Value="3" /> </Style> </Canvas.Resources> <!-- Draw first figure with "EvenOdd" FillRule . --> <TextBlock Canvas.Left="48" Canvas.Top="24" Text="FillRule = EvenOdd" /> <Polygon Style="{StaticResource figure}" FillRule="EvenOdd" Canvas.Left="48" Canvas.Top="72" /> <!-- Draw second figure with "NonZero" FillRule. --> <TextBlock Canvas.Left="360" Canvas.Top="24" Text="FillRule = NonZero" /> <Polygon Style="{StaticResource figure}" FillRule="NonZero" Canvas.Left="360" Canvas.Top="72" /> </Canvas>



By now you might have concluded that the NonZero rule simply results in the filling of all enclosed areas. That's usually the case, but the algorithm is a bit more complex. Keep in mind that a polygon is defined by a series of connected pointspt1, pt2, pt3, pt4, and so forthand these lines are conceptually drawn in a particular direction: from pt1 to pt2, from pt2 to pt3, from pt3 to pt4, and so forth.

To determine whether an enclosed area is filled with the NonZero rule, you again imagine a line drawn from a point in that area to infinity. If the imaginary line crosses an odd number of boundary lines, that area is filled just as with the EvenOdd rule. But if the imaginary line crosses an even number of boundary lines, the area is filled only if the number of boundary lines going in one direction (relative to the imaginary line) is not equal to the number of boundary lines going in the other direction. In other words, an area is filled if the difference between the number of boundary lines going in one direction and the number of boundary lines going in the other direction is "nonzero," which is the name of the rule.

With a little thought, it's possible to come up with a simple figure that tests the rule, and here it is. The arrows show the direction in which the lines are drawn:

With both the EvenOdd and NonZero rules, the three L-shaped areas numbered 1 to 3 are always filled. The two smaller interior areas labeled 4 and 5 are not filled with the EvenOdd rule because an even number of boundary lines separate these areas from infinity. But with the NonZero rule, area number 5 is filled because you must cross two lines going in the same direction to get from the inside of that area to the outside of the figure. Area number 4 is not filled. You must again cross two lines, but the two lines go in opposite directions.

Here's a XAML file that draws two versions of that figure:

TwoFigures.xaml

[View full width]

<!-- ============================================= TwoFigures.xaml (c) 2006 by Charles Petzold ============================================= --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" TextBlock.FontSize="16"> <Canvas.Resources> <!-- Define properties common to both figures. --> <Style x:Key="figure"> <Setter Property="Polygon.Points" Value="0 48, 0 144, 96 144, 96 0, 192 0, 192 96, 48 96, 48 192, 144 192 144 48" /> <Setter Property="Polygon.Fill" Value="Blue" /> <Setter Property="Polygon.Stroke" Value="Red" /> <Setter Property="Polygon.StrokeThickness" Value="3" /> </Style> </Canvas.Resources> <!-- Draw first figure with "EvenOdd" FillRule . --> <TextBlock Canvas.Left="48" Canvas.Top="24" Text="FillRule = EvenOdd" /> <Polygon Style="{StaticResource figure}" FillRule="EvenOdd" Canvas.Left="48" Canvas.Top="72" /> <!-- Draw second figure with "NonZero" FillRule. --> <TextBlock Canvas.Left="288" Canvas.Top="24" Text="FillRule = NonZero" /> <Polygon Style="{StaticResource figure}" FillRule="NonZero" Canvas.Left="288" Canvas.Top="72" /> </Canvas>



When defining the appearances of the outlines of rectangles, ellipses, and polygons, so far I've been using only two Shape properties: The Stroke property to specify the brush used for coloring the lines, and StrokeThickness for the width of the lines.

The System.Windows.Media namespace includes a Pen class that defines eight properties related to drawing lines. Although the Pen is used elsewhere in WPF graphics, Shape does not define a Pen property. It obviously uses a Pen internally but Shape defines all its own propertiesall beginning with the word Strokethat parallel the Pen properties. Here's a chart of the Pen properties and corresponding Shape properties.

Pen Property

Type

Shape Property

Brush

Brush

Stroke

Thickness

Double

StrokeThickness

StartLineCap

PenLineCap

StrokeStartLineCap

EndLineCap

PenLineCap

StrokeEndLineCap

LineJoin

PenLineJoin

StrokeLineJoin

MiterLimit

Double

StrokeMiterLimit

DashStyle

DashStyle

StrokeDashArray(DoubleCollection)

StrokeDashOffset(double)

DashCap

PenLineCap

StrokeDashCap


The definition of these properties by Shape obviously makes life easier for those of us who code XAML by hand. With the Stroke property defined by Shape, you can define a line color like this:

<Ellipse Stroke="Blue" ... /> 


If Shape instead defined a Pen property of type Pen, you'd have to do it like this:

<!-- Not the way we have to do it! --> <Ellipse ... >     <Ellipse.Pen>         <Pen Brush="Blue" .../>     </Ellipse.Pen> </Ellipse> 


The StrokeThickness property defined by Shape (corresponding to the Thickness property of the Pen) indicates the width of the line in device-independent units. As a line gets wider, the appearances of the two ends of the lines start to become more evident. By default, a thick line (shown in gray) straddles the geometric line that defines its start and end points (shown in black), and ends abruptly at those points:

The appearance of the beginning and end of the line is known as a line cap and is specified with members of the PenLineCap enumeration. The default is PenLineCap.Flat. The other options are Square, Round, and Triangle, all of which effectively increase the length of the line beyond its geometrical start and end points.

The following little stand-alone XAML file demonstrates the four line caps. As in the illustration, I've superimposed a thin black line that indicates the geometric start and end of the line.

LineCaps.xaml

[View full width]

<!-- =========================================== LineCaps.xaml (c) 2006 by Charles Petzold =========================================== --> <StackPanel xmlns="http://schemas.microsoft.com /winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" Orientation="Horizontal"> <StackPanel.Resources> <Style TargetType="{x:Type Canvas}"> <Setter Property="Width" Value="150" /> <Setter Property="Margin" Value="12" /> </Style> <Style x:Key="thin"> <Setter Property="Line.X1" Value="00" /> <Setter Property="Line.Y1" Value="50" /> <Setter Property="Line.X2" Value="100" /> <Setter Property="Line.Y2" Value="50" /> <Setter Property="Line.Stroke" Value="Black" /> </Style> <Style x:Key="thick" BasedOn="{StaticResource thin}"> <Setter Property="Line.Stroke" Value="LightGray" /> <Setter Property="Line .StrokeThickness" Value="25" /> </Style> </StackPanel.Resources> <!-- PenLineCap.Flat. --> <Canvas> <TextBlock Text="PenLineCap.Flat" /> <Line Style="{StaticResource thick}" StrokeStartLineCap="Flat" StrokeEndLineCap="Flat" /> <Line Style="{StaticResource thin}" /> </Canvas> <!-- PenLineCap.Square. --> <Canvas> <TextBlock Text="PenLineCap.Square" /> <Line Style="{StaticResource thick}" StrokeStartLineCap="Square" StrokeEndLineCap="Square" /> <Line Style="{StaticResource thin}" /> </Canvas> <!-- PenLineCap.Round. --> <Canvas> <TextBlock Text="PenLineCap.Round" /> <Line Style="{StaticResource thick}" StrokeStartLineCap="Round" StrokeEndLineCap="Round" /> <Line Style="{StaticResource thin}" /> </Canvas> <!-- PenLineCap.Triangle. --> <Canvas> <TextBlock Text="PenLineCap.Triangle" /> <Line Style="{StaticResource thick}" StrokeStartLineCap="Triangle" StrokeEndLineCap="Triangle" /> <Line Style="{StaticResource thin}" /> </Canvas> </StackPanel>



A related issue exists for polylines and involves the juncture where one straight line joins another. This is known as a line join. You set the StrokeLineJoin property of the Shape object to a member of the PenLineJoin enumeration, which has members Bevel, Miter, and Round. Both the Bevel and the Miter joins imply that a union of two connected lines results in a sharp, arrow-like point. The Bevel join shaves off the point, while the Miter join does not (at least to a certain extent).

The StrokeLineJoin property affects Rectangle as well as Polyline and Polygon, as this stand-alone XAML program demonstrates.

LineJoins.xaml

[View full width]

<!-- ============================================ LineJoins.xaml (c) 2006 by Charles Petzold ============================================ --> <StackPanel xmlns="http://schemas.microsoft.com /winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" Orientation="Horizontal"> <StackPanel.Resources> <Style TargetType="{x:Type TextBlock}"> <Setter Property="Canvas.Left" Value="25" /> </Style> <Style TargetType="{x:Type Canvas}"> <Setter Property="Width" Value="150" /> <Setter Property="Margin" Value="12" /> </Style> <Style TargetType="{x:Type Rectangle}"> <Setter Property="Width" Value="100" /> <Setter Property="Height" Value="100" /> <Setter Property="Canvas.Top" Value="50" /> <Setter Property="Canvas.Left" Value="25" /> <Setter Property="Stroke" Value="Black" /> <Setter Property="StrokeThickness" Value="25" /> </Style> </StackPanel.Resources> <!-- PenLineJoin.Bevel. --> <Canvas> <TextBlock Text="PenLineJoin.Bevel" /> <Rectangle StrokeLineJoin="Bevel" /> </Canvas> <!-- PenLineJoin.Round. --> <Canvas> <TextBlock Text="PenLineJoin.Round" /> <Rectangle StrokeLineJoin="Round" /> </Canvas> <!-- PenLineJoin.Miter. --> <Canvas> <TextBlock Text="PenLineJoin.Miter" /> <Rectangle StrokeLineJoin="Miter" /> </Canvas> </StackPanel>



There's a particular problem with the Miter join that's only revealed when the two lines join at a very small angle. To further explore these issues, you might want to experiment with the PenProperties.xaml file when setting line ends and joins.

PenProperties.xaml

[View full width]

<!-- ================================================ PenProperties.xaml (c) 2006 by Charles Petzold ============================================= === --> <Grid xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <!-- Draw the Polyline in the single-cell Grid . --> <Polyline Margin="0.5in, 1.5in, 0, 0" Points="0 0, 500 25, 0 50" VerticalAlignment="Center" Stroke="Blue" StrokeThickness="{Binding ElementName=sliderThickness, Path=Value }" StrokeStartLineCap="{Binding ElementName=lstboxStartLineCap, Path=SelectedItem.Content}" StrokeEndLineCap="{Binding ElementName=lstboxEndLineCap, Path=SelectedItem.Content}" StrokeLineJoin="{Binding ElementName=lstboxLineJoin, Path=SelectedItem.Content}" StrokeMiterLimit="{Binding ElementName=sliderMiterLimit, Path=Value }" /> <!-- Create a horizontal StackPanel in the same cell of the Grid. --> <StackPanel Grid.Column="0" Margin="0, 12, 0, 0" Orientation="Horizontal" > <!-- Define a style for the five "user-interface" groups. --> <StackPanel.Resources> <Style x:Key="uigroup"> <Setter Property="StackPanel .VerticalAlignment" Value="Top" /> <Setter Property="StackPanel.Width" Value="100" /> <Setter Property="StackPanel.Margin" Value="12, 0, 12, 0" /> </Style> </StackPanel.Resources> <!-- A Slider for the StrokeThickness property. --> <StackPanel Style="{StaticResource uigroup}"> <Label Content="_Thickness" /> <Slider Name="sliderThickness" Minimum="0" Maximum="100" Value="24" /> </StackPanel> <!-- A ListBox for the StrokeStartLineCap property. --> <StackPanel Style="{StaticResource uigroup}"> <Label Content="_StartLineCap" /> <ListBox Name="lstboxStartLineCap"> <ListBoxItem Content="{x:Static PenLineCap.Flat}" /> <ListBoxItem Content="{x:Static PenLineCap.Square}" /> <ListBoxItem Content="{x:Static PenLineCap.Round}" /> <ListBoxItem Content="{x:Static PenLineCap.Triangle}" /> </ListBox> </StackPanel> <!-- A ListBox for the StrokeEndLineCap property. --> <StackPanel Style="{StaticResource uigroup}"> <Label Content="_EndLineCap" /> <ListBox Name="lstboxEndLineCap"> <ListBoxItem Content="{x:Static PenLineCap.Flat}" /> <ListBoxItem Content="{x:Static PenLineCap.Square}" /> <ListBoxItem Content="{x:Static PenLineCap.Round}" /> <ListBoxItem Content="{x:Static PenLineCap.Triangle}" /> </ListBox> </StackPanel> <!-- A ListBox for the StrokeLineJoin property. --> <StackPanel Style="{StaticResource uigroup}"> <Label Content="_LineJoin" /> <ListBox Name="lstboxLineJoin"> <ListBoxItem Content="{x:Static PenLineJoin.Bevel}" /> <ListBoxItem Content="{x:Static PenLineJoin.Round}" /> <ListBoxItem Content="{x:Static PenLineJoin.Miter}" /> </ListBox> </StackPanel> <!-- A Slider for the StrokeMiterLimit property. --> <StackPanel Style="{StaticResource uigroup}"> <Label Content="_MiterLimit" /> <Slider Name="sliderMiterLimit" Minimum="0" Maximum="100" Value="10" /> </StackPanel> </StackPanel> </Grid>



The program draws a simple two-segment polyline that looks like an elongated greater than sign. It also creates Slider controls and ListBox controls that let you set five properties of the Polyline.

The Polyline is elongated to demonstrate a problem that could arise when you choose a StrokeEndJoin property of LineEndJoin.Miter. For example, a one-inch-thick polyline joined at an angle of 1 degree would have a miter join that extended more than 4.5 feet! (Let w be the width of the line and a be the join angle. It's easy to show that the extension of the miter tip past the actual join point is (w/2)/sin(α/2).)

For this reason, a StrokeMiterLimit property is intended to limit the extent of a miter join by shaving off the tip. You can see the StrokeMiterLimit property kick in when you select a Miter join and increase the width of the line with the first Slider control. Past a certain point, the join is shaved off. You can increase the StrokeMiterLimit with the second Slider control.

The two remaining Pen properties in the preceding table are DashStyle and DashCap. These properties let you draw styled lines, which are lines composed of dots or dashes or combinations of dots and dashes. The DashStyle property is an object of type DashStyle, a class with two properties: Dashes (an object of type DoubleCollection) and Offset (a Double). The elements of the Dashes collection indicate an on/off pattern for drawing the line.

Shape, however, doesn't go anywhere near the DashStyle class and instead defines two properties that take its place named StrokeDashArray and StrokeDashOffset. This XAML file sets the StrokeDashArray property of a Line element to "1 2 2 1".

OneTwoTwoOne.xaml

[View full width]

<!-- =============================================== OneTwoTwoOne.xaml (c) 2006 by Charles Petzold ============================================= == --> <Canvas xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Line X1="48" Y1="48" X2="1000" Y2="48" Stroke="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}" StrokeThickness="12" StrokeDashArray="1 2 2 1" /> </Canvas>



When you execute this XAML file, you can see that the line begins with a dash the same length as the thickness of the line, followed by space that is twice the line thickness, then a dash that is twice the line thickness, and a space of the line thickness. The pattern repeats indefinitely.

The StrokeDashOffset value is an offset into the dash pattern. For example, in OneTwoTwoOne.xaml you can include an attribute to set StrokeDashOffset to 1:

StrokeDashOffset="1" 


Now the line begins with the first two-unit space.

The StrokeDashArray in the OneTwoTwoOne.xaml file is actually problematic. The Shape class defines a StrokeDashCap property (corresponding to the DashCap property of Pen) of type PenLineCap, the same enumeration that you use with the StrokeStartLineCap and StrokeEndLineCap properties. The four members of the enumeration, you'll recall, are Flat, Square, Round, and Triangle. The default is Flat, which means that the dashes and spaces are precisely the lengths implied by the StrokeDashArray values. However, the other dash cap values effectively increase the length of the dashes. Try adding the following attribute to the XAML file to see the problem:

StrokeDashCap="Round" 


Now the dots no longer look like dotsthey look like little sausages. You'll probably want to change the StrokeDashArray to "0 3 1 2" to get something that looks similar to the earlier pattern.

The following stand-alone XAML program displays the four different dash caps using a StrokeDashArray pattern of "2 2."

DashCaps.xaml

[View full width]

<!-- =========================================== DashCaps.xaml (c) 2006 by Charles Petzold =========================================== --> <Grid xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Grid.Resources> <Style TargetType="{x:Type TextBlock}"> <Setter Property="FontSize" Value="16" /> <Setter Property="Margin" Value="24" /> <Setter Property="VerticalAlignment" Value="Center" /> </Style> <Style TargetType="{x:Type Line}"> <Setter Property="Grid.Column" Value="1" /> <Setter Property="Y1" Value="30" /> <Setter Property="X2" Value="400" /> <Setter Property="Y2" Value="30" /> <Setter Property="StrokeThickness" Value="25" /> <Setter Property="Stroke" Value="Black" /> <Setter Property="StrokeDashArray" Value="2 2" /> <Setter Property="StrokeStartLineCap" Value="{Binding RelativeSource={RelativeSource self}, Path=StrokeDashCap}" /> <Setter Property="StrokeEndLineCap" Value="{Binding RelativeSource={RelativeSource self}, Path=StrokeDashCap}" /> </Style> </Grid.Resources> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <!-- PenLineCap.Flat. --> <TextBlock Grid.Row="0" Text="PenLineCap.Flat" /> <Line Grid.Row="0" /> <!-- PenLineCap.Square. --> <TextBlock Grid.Row="1" Text="PenLineCap .Square" /> <Line Grid.Row="1" StrokeDashCap="Square" /> <!-- PenLineCap.Round. --> <TextBlock Grid.Row="2" Text="PenLineCap.Round" /> <Line Grid.Row="2" StrokeDashCap="Round" /> <!-- Triangle.Triangle. --> <TextBlock Grid.Row="3" Text="PenLineCap .Triangle" /> <Line Grid.Row="3" StrokeDashCap="Triangle" /> </Grid>



Notice that the Style definition for the Line defines a binding from the StrokeDashCap property to both the StrokeStartLineCap and StrokeEndLineCap properties. The lines really look the most "natural" when these three properties are set to the same value.

Only the Flat style appears to be alternating between a 2-unit dash and a 2-unit space. The others can also appear that way when the StrokeDashArray is set to "1 3."

The System.Windows.Media namespace includes both a DashStyle class (which has the properties Dashes and Offset) and a DashStyles class (notice the plural). The DashStyles class has five static properties named Solid, Dot, Dash, DashDot, and DashDotDot of type DashStyle. The following table shows the Dashes property for each of the static properties of DashStyles.

DashStyles Static Property

Dashes Property

Solid

 

Dot

0 2

Dash

2 2

DashDot

2 2 0 2

DashDotDot

2 2 0 2 0 2


In the Pen class, the default DashCap property is PenLineCap.Square, which means that the dots and dashes are by default extended by half the thickness of the line. These various Dashes arrays have been chosen with that in mind. These values are not suitable when the DashCap property is PenLineCap.Flat, which is the default value of the StrokeDashCap property that Shape defines. If you set an attribute of

StrokeDashArray="0 2" 


with the default StrokeDashCap property of Flat, you'll see no line at all because the dashes are 0 units long. My recommendation is that you manually set a StrokeDashArray property based on the StrokeDashCap property you choose.

If you set the StrokeDashCap to Square, Round, or Triangle, you can define a binding between StrokeDashArray and one of the static properties of the DashStyles class. The binding requires an x:Static markup extension to access the static property DashStyles.Dot (or whatever style you want to use) and a Path property referencing the Dashes property of DashStyle. Here's a program that uses such a binding to display a dotted line around an ellipse.

EllipseWithStyledLines.xaml

[View full width]

<!-- === ====================================================== EllipseWithStyledLines.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" WindowTitle="Ellipse with Styled Lines"> <Ellipse Margin="0.5in" Fill="Blue" Stroke="Red" StrokeDashArray="{Binding Source={x :Static DashStyles.Dot}, Path=Dashes, Mode=OneTime}" StrokeThickness="36pt" StrokeDashCap="Round"> </Ellipse> </Page>



The resultant image is actually quite interesting. Because the StrokeDashCap has been set to Round, the dots that comprise the line appear as actual dots and seem like a series of round balls that surround the ellipse. There's obviously been some special coding for this situation because there's no awkward point at which a partial dot appears.

In this chapter I have discussed all the classes that derive from Shape except Path. A graphics path is a collection of straight lines and curves. Beyond the properties defined by Shape, the Path class adds just one more: a property named Data of type Geometry, which opens a powerful array of graphics capabilities. Geometries and paths are the subject of the next chapter.




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