Chapter 18. Drawing with Shapes

What You’ll Learn in This Hour:

  • Using Shapes

  • Drawing with the Path element

  • Understanding stroke and fill

  • Working with complex shapes

Just like the rest of WPF, the API for 2D drawing is both extensive and powerful. Our goal in this hour is to equip you with the basics you need to start working with WPF quickly, as well as providing a foundation for learning if your projects have a need to go deeper.

Drawing Basic Shapes

The WPF has a number of basic shapes built in. The shapes are

  • Line

  • Polyline

  • Polygon

  • Rectangle

  • Ellipse

  • Path

All these classes live in the namespace System.Windows.Shapes.

Lines and Strokes

The best way to understand how these shapes work is to see them in action. Let’s create a simple project for the purpose of understanding these classes.

  1. Create a new project in Visual Studio called LearningShapes.

  2. Open Window1.xaml and add the following markup inside the Grid.

    <Line X1="0" Y1="0" X2="100" Y2="100" Stroke="Red" />
  3. Make sure that you are using the Split Pane view to view Window1.xaml. Visual Studio’s live preview will display the XAML without your having to run the application. This makes it very easy for us to play with the various shapes. Figure 18.1 shows what Window1.xaml looks like at this point.

    Previewing a simple line in the IDE.

    Figure 18.1. Previewing a simple line in the IDE.

As you can see, Line draws a line between two points. For Line to render something visible, you need to provide at least one of the coordinate pairs and the stroke. The X and Y properties default to zero, so technically we could have omitted X1 and Y1 in the preceding example.

The coordinates are specified with respect to the upper-left corner of the element that contains the line. So when Y1 is 25, it means that the point is 25 pixels from the top. Increasing Y1 would move the point downward. Likewise, increasing X1 would move the point toward the right.

You can provide values for the coordinates that go beyond the bounds of the container. You can even provide negative values if you need to.

The Stroke property tells WPF what color the line should be. (Actually, it’s a not a color, but a brush. Brushes are a lot more interesting than simple colors. We’ll go into that in the next hour.) The Stroke property is common to all the shapes classes.

Let’s explore some of the other properties related to the stroke. We’ll draw two lines on top of each other for comparison.

  1. In Window1.xaml, replace the line with the following markup:

    <Line X1="20" Y1="20"
          X2="200" Y2="100"
          Stroke="Black"
          StrokeThickness="30"/>
  2. This draws a very thick black line. The StrokeThickness tells WPF how thick the line should be in pixels. Now, we’ll draw a brightly colored line on top of it. Add this markup immediately after the previous Line:

    <Line X1="20" Y1="20"
          X2="200" Y2="100"
          Stroke="Fuchsia"
          StrokeThickness="20"
          StrokeStartLineCap="Round"
          StrokeEndLineCap="Square"
          StrokeDashCap="Triangle"
          StrokeDashArray="1.6 1.3"/>
  3. The resulting lines are shown in Figure 18.2.

    Demonstrating the stroke-related properties.

    Figure 18.2. Demonstrating the stroke-related properties.

We have specified line caps for the fuchsia line using the StrokeStartLineCap and StrokeEndLineCap. A line cap tells WPF how to draw the ends of the line. Notice that the upper-left point of the black line is flat, and that the corresponding point on the fuchsia line is rounded. The StrokeStartLineCap defines the cap for the point of the line designated by X1 and Y1. StrokeEndLineCap handles the point designated by X2 and Y2.

The preceding example actually demonstrates all the possible values for line caps and how they are rendered. The default value for a line cap is Flat. Note that all line caps except for Flat extend beyond the endpoint of the line. That’s why the ends of the fuchsia line overlap the ends of the black line, even though they are the same length.

StrokeDashArray is a property that allows us to define the dashes and gaps in a line. The values in the array alternate between the length of the dash and the length of the gap. The values are also relative to the thickness of the stroke. So a value of 1.0 means that the length of the corresponding dash or gap is equal to the width of the stroke.

In our example, we are telling WPF to draw a dash that is 1.6 times the width of the stroke followed by a gap 1.3 times the width of the stroke. The array is not limited to two values; you can make something much more complicated if you like.

Listing 18.1 contains markup demonstrating some possible lines. Notice that the common properties of the lines have been moved into the resources.

Example 18.1. Several Lines in Window1.xaml

<Window x:Class="LearningShapes.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300">
    <StackPanel>
        <StackPanel.Resources>
            <Style TargetType="{x:Type Line}">
                <Setter Property="Stroke" Value="Black" />
                <Setter Property="StrokeThickness" Value="10" />
                <Setter Property="X2" Value="260" />
                <Setter Property="Margin" Value="10" />
            </Style>
        </StackPanel.Resources>

        <Line />
        <Line StrokeDashArray="1 1" />
        <Line StrokeDashArray="0.5 1.5"/>
        <Line StrokeDashArray="1 2 0.1 0.2 0.3 0.4"/>

        <!-- a dotted line-->
        <Line StrokeDashArray="0 1.5"
              StrokeDashCap="Round"/>

        <Line StrokeDashArray="0 1.5"
              StrokeDashCap="Triangle"/>

    </StackPanel>
</Window>

Sometimes the preview isn’t accurate when displaying the results of your XAML. Figure 18.3 shows what Listing 18.1 looks like when executed.

Examples of lines with dashes.

Figure 18.3. Examples of lines with dashes.

Simple Shapes and Fills

Polyline allows you to draw a line with multiple segments. It has a property, Points, that is similar to the StrokeDashArray. It takes a series of values that alternate between x and y coordinates. The following XAML snippets are equivalent:

<Polyline Points="10 10 200 10" Stroke="Red"/>
<Line X1="10" Y1="10" X2="200" Y2="10" Stroke="Red" />

They both draw a red line from 10,10 to 200,10. With Polyline you can continue to add segments. We could draw a square with the following snippet:

<Polyline Points="10,10 200,10 200,210 10,210 10,10" Stroke="Red"/>

Notice that in this snippet I have included commas to help group the x and y pairs. The commas are ignored by WPF; occasionally, you will see them used to delimit the pairs themselves, such as in this snippet:

<Polyline Points="10 10,200 10,200 210,10 210,10 10" Stroke="Red"/>

A Polyline can handle thousands of points. However, it’s difficult to manage that many points in XAML. In those cases, you will very likely be adding the points in code.

The Polygon shape is nearly identical to the Polyline. The primary difference is that a Polyline is open and a Polygon is closed. For example, we could create a square with the following snippet:

<Polygon Points="10 10 50 10 50 60 10 60" Stroke="Green"/>

Notice that our polygon has one less set of x,y values. This is because a Polygon automatically connects the end point with the start point.

In addition to Stroke, all the shapes also have a Fill property. Fill is analogous to the Background property that you have already seen on controls. Let’s try drawing a few shapes and adding some fills.

  1. Create a new window in your LearningShapes project named Poly.xaml. Replace the Grid with a WrapPanel. Set the Background of the Window to Pink to add some contrast.

  2. First, we’ll draw a Polyline. Add the following inside the WrapPanel:

    <Polyline Points="10 10 50 10 50 60 10 60"
              Stroke="Red"
              StrokeThickness="4"
              Fill="Orange"/>

    Notice that the left side of the square does not contain a stroke.

  3. Add a Polygon to the WrapPanel with this markup:

    <Polygon Points="10 10 50 10 50 60 10 60"
             Fill="Yellow"
             Stroke="Green"
             StrokeThickness="6"
             StrokeLineJoin="Round"/>

    This Polygon has the same points as the preceding Polyline, but there is a stroke on the left side. We’ve also added the StrokeLineJoin property. It tells the stroke how to render at the vertices or corners of the Polygon. In this case we are rounding the corners. It’s hard to notice the effect unless the stroke has some thickness.

  4. Our next Polygon demonstrates mitered corners. We need some angles that aren’t perpendicular to see this effect.

    <Polygon Points="10 10, 30 20, 50 10, 50 60, 30 50, 10 60"
             Stroke="Green"
             StrokeThickness="6"
             StrokeLineJoin="Miter"
             StrokeMiterLimit="1"
             Fill="Yellow" />

    The StrokeMiterLimit determines how much of the corners are cut off. Values less than 1 are meaningless. You can experiment here by changing the property to something like 1.2. StrokeThickness also affects how visible the miter is.

  5. A Polygon can intersect itself as well. This next one crosses itself twice:

    <Polygon Points="10 10, 30 60, 50 10, 50 60, 30 10, 10 60"
             Fill="Yellow" />
  6. Here’s the markup for a star:

    <Polygon Margin="10 10 0 0"
             Points="25 0, 33 17, 50 19, 38 32, 40 50,
             25 42, 10 50, 12 32, 0 19, 17 17"
       Fill="Yellow" />

    We included the margin on the star to add some spaces; all our previous shapes are already offset by 10,10. It’s also useful to note that our collection of points can span multiple lines. The extra whitespace is ignored.

  7. Finally, put the entire WrapPanel element inside of a ViewBox. The complete markup for Poly.xaml is shown in Listing 18.2. Set Poly.xaml to be the StartupUri in App.xaml and run the application. Resize the window and you can get a better look at the behavior of the strokes. Figure 18.4 shows the application.

Example 18.2. Polyline, Polygons, and Fills

<Window x:Class="LearningShapes.Poly"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Poly" Height="300" Width="300"
        Background="Pink">
    <Viewbox>
        <WrapPanel>
            <Polyline Points="10 10 50 10 50 60 10 60"
                      Stroke="Red"
                      StrokeThickness="4"
                      Fill="Orange"/>
            <Polygon Points="10 10 50 10 50 60 10 60"
                     Fill="Yellow"
                     Stroke="Green"
                     StrokeThickness="6"
                     StrokeLineJoin="Round"/>
            <Polygon Points="10 10, 30 20, 50 10, 50 60, 30 50, 10 60"
                     Stroke="Green"
                     StrokeThickness="6"
                     StrokeLineJoin="Miter"
                     StrokeMiterLimit="1"
                     Fill="Yellow" />
            <Polygon Points="10 10, 30 60, 50 10, 50 60, 30 10, 10 60"
                     Fill="Yellow" />
            <Polygon Margin="10 10 0 0"
                     Points="25 0, 33 17, 50 19, 38 32, 40 50,
                     25 42, 10 50, 12 32, 0 19, 17 17"
                     Fill="Yellow" />
        </WrapPanel>
    </Viewbox>
</Window>

The polyline and polygons.

Figure 18.4. The polyline and polygons.

By the Way

Both Polygon and Polyline have a property called FillRule. FillRule controls which portions of a Polygon or Polyline are filled in. FillRule is meaningful only under certain conditions when you have a complicated shape that intersects with itself multiple times. If the fill of a shape is not what you expect, looking up this property in the documentation is a good place to start.

Ellipses and Rectangles

Often you will find that Line, Polyline, and Polygon are overkill. The two classes that you are likely to use frequently are Ellipse and Rectangle.

They are similar to the shapes we’ve already encountered. Fill and all the stroke properties we discussed are still applicable. However, with both Ellipse and Rectangle you do not need to specify any points. Instead, you provide the desired Width and Height. For circles and squares, set Width and Height to the same value.

By default, Ellipse and Rectangle are positioned at 0,0 with respect to their parent element. To relocate them you have to use Margin. The exception is when they are placed in a Canvas. In that case, you use the attached properties Canvas.Left and Canvas.Top. As it turns out, Canvas is the container you’ll most commonly place shapes in.

A Header for the Media Viewer

Let’s use a few Ellipse elements to create a logo for Media Viewer.

  1. Open the MediaViewer project in Visual Studio.

  2. We’ll begin by defining a palette that we’ll use to theme the application. Add a new resource dictionary under the Resources folder named ColorsAndBrushes.xaml.

  3. Add the following brushes to our new resource dictionary:

    <SolidColorBrush x:Key="yellowBrush" Color="#FFFFB03B"/>
    <SolidColorBrush x:Key="orangeBrush" Color="#FFB64926"/>
    <SolidColorBrush x:Key="redBrush" Color="#FF8E2800"/>
  4. Let’s also go ahead and create a resource dictionary for all styles that we will use. Add it under Resources and name it DefaultStyles.xaml. We’ll use both the styles and the brushes applicationwide, so let’s merge these dictionaries into the main dictionary for the application. Open App.xaml.

  5. Inside Application.Resources add the following:

    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="ResourcesColorsAndBrushes.xaml" />
            <ResourceDictionary Source="ResourcesDefaultStyles.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
  6. Now we are ready to start the real work. Open MainWindow.xaml. We’re going to add a header to our application. It will contain the Home button that returns to the main menu, the logo and title of the application, and it will be accessible from each of the views in the application.

  7. Replace the existing Button element with the following:

    <StackPanel DockPanel.Dock="Top"
                Orientation="Horizontal"
                Margin="0 0 0 8">
    
        <Button Content="Return to Main Menu"
                Click="Header_Click"/>
    
        <TextBlock Text="Media Viewer"
                   VerticalAlignment="Center"/>
    </StackPanel>
  8. Our logo for the application is very simple. It’s three overlapping circles of different colors. In between the Button and the TextBlock, add the following:

    <Canvas>
        <Ellipse Fill="{StaticResource redBrush}"
                 Width="20" Height="20"
                 Canvas.Top="5"/>
        <Ellipse Fill="{StaticResource orangeBrush}"
                 Width="20" Height="20"
                 Canvas.Top="5" Canvas.Left="6"/>
        <Ellipse Fill="{StaticResource yellowBrush}"
                 Width="20" Height="20"
                 Canvas.Top="5" Canvas.Left="12"/>
    </Canvas>

    Look at the live preview in the design pane and notice that the TextBlock is on top of the Canvas and that our ellipses extend beyond the bounds of the StackPanel. This isn’t how we want the header to look. We’d like to have the logo completely inside the StackPanel, and we’d like to have the TextBlock to the right of the logo. You can see this illustrated in Figure 18.5.

    The logo isn’t quite right.

    Figure 18.5. The logo isn’t quite right.

    The problem is that the Canvas element has no Width or Height. Unlike other panels, it does not expand to contain its children. They spill outside of its bounds. If we provide a Width of 35 and a Height of 25, the elements align as we expect.

  9. Instead of plain text, let’s use a Polygon to draw the silhouette of a house inside our home button. We’ll remove the Content property and add a Polygon element. The result will be this:

    <Button Background="{StaticResource redBrush}"
            Click="Header_Click">
        <Polygon Fill="{StaticResource yellowBrush}"
                 Points="0,10 11,0 22,10 18,10 18,20 4,20 4,10" />
    </Button>

    If you are trying to follow the points, the Polygon begins at the left-most edge of the roof.

  10. While we are working on the header, let’s style the title a bit. First, we’ll add a brush for coloring text. We’ll add it to ColorsAndBrushes.xaml:

    <SolidColorBrush x:Key="textBrush" Color="#FF000000"/>
  11. Inside DefaultStyles.xaml, we need to add a reference to ColorAndBrushes.xaml.

    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="ColorsAndBrushes.xaml" />
    </ResourceDictionary.MergedDictionaries>
  12. We’ll name the style title. Place this in DefaultStyles.xaml:

    <Style x:Key="title" TargetType="{x:Type TextBlock}">
        <Setter Property="FontSize" Value="20" />
        <Setter Property="FontWeight" Value="Bold" />
        <Setter Property="Foreground" Value="{StaticResource textBrush}" />
    </Style>
  13. Return to MainWindow.xaml. Locate the TextBlock and point the style to title.

    <TextBlock Style="{StaticResource title}"
               Text="Media Viewer"
               VerticalAlignment="Center"/>

    The application now looks like Figure 18.6.

    The header with a logo and a fancy Home button.

    Figure 18.6. The header with a logo and a fancy Home button.

The complete markup for MainWindow.xaml is shown in Listing 18.3.

Example 18.3. MainWindow.xaml with Some Styling

<Window x:Class="MediaViewer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Media Viewer"
        Height="600"
        Width="800"
        Loaded="MainWindow_Loaded">
    <DockPanel>
        <StackPanel DockPanel.Dock="Top"
                    Orientation="Horizontal"
                    Margin="0 0 0 8">
            <Button Background="{StaticResource redBrush}"
                    Click="Header_Click">
                <Polygon Fill="{StaticResource yellowBrush}"
                         Points="0,10 11,0 22,10 18,10 18,20 4,20 4,10" />
            </Button>
            <Canvas Width="35" Height="25">
                <Ellipse Fill="{StaticResource redBrush}"
                         Width="20" Height="20"
                         Canvas.Top="5"/>
                <Ellipse Fill="{StaticResource orangeBrush}"
                         Width="20" Height="20"
                         Canvas.Top="5" Canvas.Left="6"/>
                <Ellipse Fill="{StaticResource yellowBrush}"
                         Width="20" Height="20"
                         Canvas.Top="5" Canvas.Left="12"/>
            </Canvas>
            <TextBlock Style="{StaticResource title}"
                       Text="Media Viewer"
                       VerticalAlignment="Center"/>
        </StackPanel>
        <ContentControl x:Name="currentView" />
    </DockPanel>
</Window>

Styling the Media Controls

Let’s turn our attention to MediaPlayer.xaml. This user control contains the stop, play/pause, and mute buttons used in our application. Ultimately, we want the user control to look like Figure 18.7.

MediaPlayer.xaml after it is completely styled.

Figure 18.7. MediaPlayer.xaml after it is completely styled.

To do this, we’re going to need to modify the layout somewhat. This brings up an important aspect of WPF applications. We can significantly modify the layout of elements in the application without breaking the application.

  1. Open MediaPlayer.xaml.

  2. Locate the Grid element that is just inside the DockPanel. Modify the opening tag so that it looks like this:

    <Grid DockPanel.Dock="Bottom"
          HorizontalAlignment="Center"
          Width="400" Height="50"
          Background="{StaticResource yellowBrush}">
  3. If you look back at Figure 18.7, you will notice that the control is divided into thirds. The first third is empty, the middle third contains the three buttons, and the final third contains the slider for adjusting the volume. To achieve this, we need to group the three buttons together, as well as reduce the number of columns in our outermost Grid to three. After making these changes, our Grid now looks like this:

    <Grid DockPanel.Dock="Bottom"
          HorizontalAlignment="Center"
          Width="400" Height="50"
          Background="{StaticResource yellowBrush}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid Grid.Column="1"
              HorizontalAlignment="Center">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <Button Content="Stop"
                    Click="Stop_Click" />
            <Button Grid.Column="1"
                    Content="Play"
                    Click="Play_Click" />
            <ToggleButton Grid.Column="2"
                          Content="Mute"
                          IsChecked="{Binding ElementName=mediaElement,
                                              Path=IsMuted}" />
        </Grid>
        <Slider Grid.Column="2"
                Minimum="0"
                Maximum="1"
                Value="{Binding ElementName=mediaElement, Path=Volume}"/>
    </Grid>

    On both Grid elements, we set the HorizontalAlignment to Center both to center the element in its parent and to make it collapse to the size of its content. To see the difference, remove the alignment on the innermost Grid. You’ll see in the live preview that it expands to fill all the space in the center column of its parent Grid.

    We set the Width of the column in the outer Grid to * because we want them to all be the same size, whereas we set the column in the inner grid to Auto so that they will collapse to the size of their content.

  4. Now, we’ll use a Rectangle to make the Stop button look like the traditional Stop buttons on media players. Replace the Stop button with this:

    <Button Click="Stop_Click">
        <Rectangle Width="18" Height="20"
                   Fill="{StaticResource redBrush}"
                   RadiusX="1" RadiusY="1"
                   Stroke="{StaticResource orangeBrush}"
                   StrokeThickness="1"/>
    </Button>

    The Rectangle element behaves just like the Ellipse. The Rectangle includes RadiusX and RadiusY properties. These allow you to round the corners on a Rectangle. They are similar to CornerRadius on Border, but provide finer-grained control. Try changing their values and observing the change in the preview before we move on. The current value is very subtle.

  5. The Play button is a little more complicated. We want the content of the button to change depending on the state of the player. We’ll actually implement this when we discuss triggers in Hour 22. In the meantime, we add the content for both play and pause, and we’ll just hide the pause content.

    The icon for play is a simple triangle that we’ll draw with a Polygon. For the Pause button, we want two parallel bars. There’s actually several ways to achieve this. We’ll use two Rectangle elements inside a Canvas for the time being.

    Also, a Button can have only one child, so we’ll place both of these inside a Grid. The new markup for the play button is the following:

    <Button Grid.Column="1"
            Click="Play_Click">
        <Grid>
            <Polygon Fill="{StaticResource yellowBrush}"
                     Stroke="Gray"
                     Points="0,0 18,10 0,18" />
            <Canvas Visibility="Hidden">
                <Rectangle Height="18"
                           Fill="{StaticResource yellowBrush}"
                           Stroke="{StaticResource orangeBrush}"
                           Width="6"/>
                <Rectangle Height="18"
                           Fill="{StaticResource yellowBrush}"
                           Stroke="{StaticResource orangeBrush}"
                           Width="6"
                           Canvas.Left="8"/>
            </Canvas>
        </Grid>
    </Button>
  6. Go ahead and run the application to see how the styling looks.

Paths and Complex Drawings

Even with all the classes we’ve discussed so far, limitations still exist. For example, we said earlier that we would like the mute button to look like Figure 18.8.

The mute button after styling.

Figure 18.8. The mute button after styling.

However, we haven’t yet discussed a way of drawing a shape that consists of straight lines and curves. In fact, the curves we’ve seen so far are ellipses.

WPF has a shape class called Path that allows you to define any sort of shape you can imagine. It is a powerful tool, and has many features. We are only going to scratch the surface of what you can do with it.

Path is not as intuitive as the other shapes. It may be easier to think of a Path element as a series of commands telling WPF what to draw. (Yes, that’s more imperative than declarative.) You can think of the commands as moving a virtual pen around the display. For example, you might have a set of commands like this:

  • Move the pen to point A.

  • Draw a line to point B.

  • Draw another line to point C.

  • Draw a curved line to point D.

  • Close the shape with a line back to point A.

Figure 18.9 helps illustrate this idea. Realize that you are not telling WPF to draw a line from A to B; rather, the pen is already at A, and you are drawing a line to B. That automatically makes B the beginning of the next segment.

Moving and drawing with the pen.

Figure 18.9. Moving and drawing with the pen.

Path has Fill and the same stroke properties as the previous shapes. However, it has a property called Data. You can set Data using a PathGeometry element or using a special syntax we’ll discuss later in the hour. I’ll show you some examples of each, but a thorough coverage of the subject is beyond the scope of this book.

Let’s use the PathGeometry element to begin drawing the mute button.

  1. Open MediaPlayer.xaml.

  2. Locate the ToggleButton that we bound to mediaElement. We’re going to remove the Content attribute and place a Canvas inside the ToggleButton. The Canvas will contain a Path drawing the silhouette of a speaker, a second Path that will be the glossy highlight on the speaker, and a series of Polygon elements that will be the sound coming out of the speaker. The resulting markup looks like this:

    <ToggleButton Grid.Column="2"
                  IsChecked="{Binding ElementName=mediaElement,
                                      Path=IsMuted}">
        <Canvas Width="20" Height="20">
            <!-- the speaker -->
            <Path Fill="{StaticResource yellowBrush}"
                  Stroke="{StaticResource redBrush}"
                  StrokeLineJoin="Round">
                <Path.Data>
                    <PathGeometry>
                        <PathFigure StartPoint="12,5"
                                    IsClosed="True">
                            <LineSegment Point="0,6" />
                            <PolyLineSegment Points="0,14 12,15 17,20" />
                            <ArcSegment Point="17,0"
                                        Size="30,30"/>
                        </PathFigure>
                    </PathGeometry>
                </Path.Data>
            </Path>
            <!-- highlight -->
            <Path Fill="White">
                <Path.Data>
                    <PathGeometry>
                        <PathFigure StartPoint="13,6"
                                    IsClosed="True">
                            <PolyLineSegment Points="1,7 1,11 17.5,11" />
                            <ArcSegment Point="16.5,2"
                                        Size="30,30"/>
                        </PathFigure>
                    </PathGeometry>
                </Path.Data>
            </Path>
            <!-- sound waves -->
            <Polygon Points="20,5 25,0 27,3"
                     Fill="{StaticResource redBrush}" />
            <Polygon Points="20,10 28,9 27,6"
                     Fill="{StaticResource redBrush}" />
            <Polygon Points="20,13 26,17 27,13"
                     Fill="{StaticResource redBrush}" />
            <Polygon Points="20,17 24,21 26,18"
                     Fill="{StaticResource redBrush}" />
        </Canvas>
    </ToggleButton>
  3. The user control should now look like Figure 18.10. Run the application and take a look at the new mute button.

    The MediaPlayer user control so far.

    Figure 18.10. The MediaPlayer user control so far.

Let’s take a closer look at the Path, in particular the PathGeometry element.

<PathGeometry>
    <PathFigure StartPoint="12,5"
                IsClosed="True">
        <LineSegment Point="0,6" />
        <PolyLineSegment Points="0,14 12,15 17,20" />
        <ArcSegment Point="17,0"
                    Size="30,30"/>

    </PathFigure>
</PathGeometry>

The first child element is PathFigure. A path figure is a single shape within the path. You are allowed to have multiple figures in the path. For example, if you wanted two circles in your path that were not touching, you could use two figures to accomplish this. On the figure we set IsClosed to True; this automatically draws a line from the last point to the first point. It’s the same difference that we found between a PolyLine and a Polygon. The StartPoint property is the point where we will begin drawing our figure. This is equivalent to point A in Figure 18.9.

Inside PathFigure, we have a series of segments. The first element, LineSegment, draws a single straight line from 12,5 to 0,6. That’s the top-left line of the speaker. The second element, PolyLineSegment, draws three lines. The first of the three lines goes from 0,6 to 0,14. It’s the right-most line on the speaker. The remaining two lines are the bottom of the speaker. We could easily have incorporated the first LineSegment into the PolyLineSegment, but we left it in for demonstration purposes.

The final segment draws the curved front of the speaker. The Size property indicates the radii of the ellipse that we are taking the arc from. Because we need a gentle curve for the face of the speaker, we used a large ellipse.

There are seven types of segments in all.

  • ArcSegment

  • BezierSegment

  • LineSegment

  • PolyBezierSegment

  • PolyLineSegment

  • PolyQuadraticBezierSegment

  • QuadraticBezierSegment

Covering all these segments is beyond the scope of the book. However, if you are interested in knowing more about computer graphics in general, you should take some time to research these classes as well as the principles underlying them.

ArcSegment has many more features than what we’ve mentioned here. Take a look at the official documentation if you would like to know more about the class.

One important thing to note about PathFigure and the segment classes is that they are available for data binding and animation. However, that they support these features means that they come with extra overhead. There’s a lightweight alternative if you don’t need those features.

Stream Geometry

Stream geometry is an alternative to path geometry. It uses a special syntax or mini-language for defining geometries.

If you use a tool such as Expression Blend or Expression Design to draw shapes in XAML, you will discover that most of the output is Path elements. However, these paths do not use figures. Instead, they employ the special mini-language in the Data attribute. For example, the XAML to produce the speaker that we just examined could also be expressed as the following:

<Path Fill="{StaticResource yellowBrush}"
      Stroke="{StaticResource redBrush}"
      Data="M12,5 L0,6 0,14 12,15 17,20 A30,30,0,0,0,17,0 z" />

This notation is much more concise than the original:

<Path Fill="{StaticResource yellowBrush}"
      Stroke="{StaticResource redBrush}"
      StrokeLineJoin="Round">
    <Path.Data>
        <PathGeometry>
            <PathFigure StartPoint="12,5"
                        IsClosed="True">
                <LineSegment Point="0,6" />
                <PolyLineSegment Points="0,14 12,15 17,20" />
                <ArcSegment Point="17,0"
                            Size="30,30"/>
            </PathFigure>
        </PathGeometry>
    </Path.Data>
</Path>

If you look carefully at the Data attribute, you’ll notice that it is a combination of letters and points. Just like in other places where we have encountered sets of points, the commas are optional. The points themselves are the same points used in the figure segments.

  • M is a command to move to a given point.

  • L indicates that the following points are line segments.

  • A uses the following seven points to draw an arc segment.

  • Z is the command to close the figure.

There are many more commands. Additionally, commands are case sensitive. Uppercase letters work with absolute coordinates, and lowercase letters with relative coordinates. (Relative coordinates are often easier to understand.)

You can use this language to define path geometry as well. It is set to the Figures property on PathGeometry. However, most of the time you encounter it, it will be in the context of stream geometry.

If you are interested in learning more, check the official documentation under the topic of “Path Markup Syntax.”

Summary

This hour has been only an introduction to the topic of drawing in WPF. Nevertheless, a lot can be done just with the material we covered in this hour. Additionally, you are now better equipped to dig deeper into the graphics capabilities of WPF.

Q&A

Q.

Why do we have two separate resource dictionaries in this project?

A.

There is not a technical reason for splitting up the resource dictionaries. In fact, we could store all the resources for styling directly in App.xaml. However, it makes it easier to located resources when they are partitioned into separate files. If we used a single dictionary for all the resources, it would be very large by the time we were finished.

Q.

Are there any drawbacks to having multiple resources dictionaries?

A.

Yes, in fact there are. The primary drawback is that WPF has to load the resources in a particular order. This will become more evident as we progress. However, your resources dictionaries usually end up with a chain of dependencies. For example, DefaultStyles.xaml depends on ColorsAndBrushes.xaml.

Q.

How does the topic of shapes and 2D drawing relate to 3D in WPF?

A.

There is not much of a correlation between what we discussed in this hour and using 3D in WPF. However, there is another means of drawing in 2D. Frequently, it uses Geometry classes, whereas the techniques we studied here use the Shape classes. It has a lot of similarities to the way WPF handles 3D. For more information, look up System.Windows.Media.Geometry on MSDN.

Workshop

Quiz

1.

What’s the difference between a closed path or shape and an open one?

2.

What are two common properties on all the Shape classes?

Answers

1.

Paths that are closed automatically include a line segment connecting their final point to the start point. Open shapes do not connect the start and end points.

2.

There are more than a dozen properties on the base class System.Windows.Shapes.Shape. The properties we mentioned in this hour are

  • Fill

  • Stroke

  • StrokeDashArray

  • StrokeDashCap

  • StrokeEndLineCap

  • StrokeLineJoin

  • StrokeMiterLimit

  • StrokeStartLineCap

  • StrokeThickness

Activities

  1. Look up “Stream Geometry” on MSDN. Gain a better understanding of when it is appropriate to use path geometry over stream geometry.

  2. Research the topic of Bézier curves. You might also find useful information under the topic of “splines.” Build a WPF application that allows you to manipulate the points of a Bézier curve.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.11.227