Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp 2008 And The .NET 3.5 Platform [eng]-1

.pdf
Скачиваний:
34
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

1132 CHAPTER 30 WPF 2D GRAPHICAL RENDERING, RESOURCES, AND THEMES

Recall that a <GeometryDrawing> consists of a brush, pen, and any of the WPF geometry types. Here, we have indirectly defined a light blue SolidColorBrush using the Brush property in the open element. The Pen type is declared using property-element syntax, to define a blue brush with a specific thickness. Here, we are making use of a <RectangleGeometry> as the value assigned to the

Geometry property of the <GeometryDrawing> type.

If you attempt to author this XAML directly within a <Page> scope using xamlpad.exe (or within any ContentControl-derived type), you will generate a markup error that essentially tells you that <GeometryDrawing> does not extend the UIElement base class and therefore cannot be used as a value to the Content property.

This brings up an interesting aspect of working with the Drawing-derived types: they do not have any user interface in and of themselves! Types such as <GeometryDrawing> simply describe how a 2D element would look if placed into a suitable container. WPF provides three different hosting objects for Drawing objects: DrawingImage, DrawingBrush, and DrawingVisual.

Containing Drawing Types in a DrawingImage

The DrawingImage type allows you to plug your drawing geometry into a WPF <Image> control. Thus, if you wish to render the previous <GeometryDrawing>, you would need to wrap it as so:

<Image>

<Image.Source>

<DrawingImage>

<DrawingImage.Drawing> <GeometryDrawing Brush ="LightBlue">

...

</GeometryDrawing>

</DrawingImage.Drawing>

</DrawingImage>

</Image.Source>

</Image>

Notice how the Source property of the Image type is assigned a Drawing-derived type.

Containing Drawing Types in a DrawingBrush

If you instead wrap a <GeometryDrawing> using a DrawingBrush type, you have essentially created a complex custom brush, given that DrawingBrush is actually one of the Brush-derived types (more information on brushes in the next section). You could then use it anywhere a Brush type is required, such as the Background property of the <Window> type:

<Window x:Class="FunWithDrawingAndGeometries.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="FunWithDrawingAndGeometries" Height="190" Width="224">

<!-- Set the background of this window to a custom DrawingBrush -->

<Window.Background>

<DrawingBrush>

<DrawingBrush.Drawing> <GeometryDrawing Brush ="LightBlue">

<GeometryDrawing.Pen>

<Pen Brush ="Blue" Thickness ="5"/> </GeometryDrawing.Pen>

CHAPTER 30 WPF 2D GRAPHICAL RENDERING, RESOURCES, AND THEMES

1133

<GeometryDrawing.Geometry> <RectangleGeometry Rect="0,0,100,50"/>

</GeometryDrawing.Geometry>

</GeometryDrawing>

</DrawingBrush.Drawing>

</DrawingBrush>

</Window.Background>

</Window>

A More Complex Drawing Geometry

A DrawingImage object can be composed of multiple individual Drawing objects, placed in a <DrawingGroup> in order to build much more elaborate 2D images. Consider the following Image type, which uses a <DrawingImage> as its source:

<Image>

<Image.Source>

<DrawingImage>

<DrawingImage.Drawing>

<!-- A group of various geometries -->

<DrawingGroup>

<GeometryDrawing>

<GeometryDrawing.Geometry>

<GeometryGroup>

<RectangleGeometry Rect="0,0,20,20" /> <RectangleGeometry Rect="160,120,20,20" />

<EllipseGeometry Center="75,75" RadiusX="50" RadiusY="50" /> <LineGeometry StartPoint="75,75" EndPoint="180,0" />

</GeometryGroup>

</GeometryDrawing.Geometry>

<!-- A custom pen to draw the borders -->

<GeometryDrawing.Pen>

<Pen Thickness="10" LineJoin="Round" EndLineCap="Triangle" StartLineCap="Round">

<Pen.Brush>

<LinearGradientBrush>

<GradientStop Offset="0.0" Color="Red" /> <GradientStop Offset="1.0" Color="Green" />

</LinearGradientBrush>

</Pen.Brush>

</Pen>

</GeometryDrawing.Pen>

</GeometryDrawing>

</DrawingGroup>

</DrawingImage.Drawing>

</DrawingImage>

</Image.Source>

</Image>

The <DrawingImage> is composed of a <DrawingGroup> that contains a <GeometryGroup> to build an image consisting of two rectangles, an ellipse, and a line. The borders of our images are rendered using a custom pen type, which is in turn composed of a custom LinearGradientBrush. The end result can be seen in Figure 30-7.

1134 CHAPTER 30 WPF 2D GRAPHICAL RENDERING, RESOURCES, AND THEMES

Figure 30-7. An image consisting of a DrawingGroup

If we were to update our Pen type to make use of a DashStyle such as DashStyles.DashDotDot (seen previously)

<Pen Thickness="10" LineJoin="Round" EndLineCap="Triangle" StartLineCap="Round"

DashStyle = "{x:Static DashStyles.DashDotDot}" > <Pen.Brush>

<LinearGradientBrush>

<GradientStop Offset="0.0" Color="Red" /> <GradientStop Offset="1.0" Color="Green" />

</LinearGradientBrush>

</Pen.Brush>

</Pen>

we would now find the rendering shown in Figure 30-8.

Figure 30-8. A Pen with a DashStyle setting of dash-dot-dot

Source Code The FunWithDrawingGeometries.xaml file can be found under the Chapter 30 subdirectory.

CHAPTER 30 WPF 2D GRAPHICAL RENDERING, RESOURCES, AND THEMES

1135

The Role of UI Transformations

Before moving on to the topic of animation services, allow me to wrap up our look at 2D graphic rendering by examining the topic of transformations. WPF ships with numerous types that extend the Transform abstract base class, which can be applied to any FrameworkElement (e.g., descendents of Shape as well as UI elements such as Buttons, TextBoxes, etc.). Using these types, you are able to render a FrameworkElement at a given angle, skew the image across a surface, and expand or shrink the image in a variety of ways.

Transform-Derived Types

Table 30-5 documents many of the key out-of-the-box Transform types.

Table 30-5. Key Descendents of the System.Windows.Media.Transform Type

Type

Meaning in Life

MatrixTransform

Creates an arbitrary matrix transformation that is used to manipulate

 

objects or coordinate systems in a 2D plane

RotateTransform

Rotates an object clockwise about a specified point in a 2D (x, y) coordinate

 

system

ScaleTransform

Scales an object in the 2D (x, y) coordinate system

SkewTransform

Skews an object in the 2D (x, y) coordinate system

TransformGroup

Represents a composite Transform composed of other Transform objects

 

 

Once you create a Transform-derived object, you can apply it to two properties provided by the FrameworkElement base class. The LayoutTransform property is helpful in that the transformation occurs before elements are rendered into a panel. The RenderTransform property, on the other hand, occurs after the items are in their container, and therefore it is quite possible that elements can be transformed in such a way that they overlap each other. As well, types that extend UIElement can also assign a value to the RenderTransformOrigin property, which simply specifies an (x, y) position to begin the transformation.

Applying Transformations

Assume we have a <Grid> containing a single row with four columns. Within each cell, we will rotate, skew, and scale various UIElements, for example:

<!-- A Rectangle with a rotate transformation -->

<Rectangle Height ="100" Width ="40" Fill ="Red" Grid.Row="0" Grid.Column="0"> <Rectangle.LayoutTransform>

<RotateTransform Angle ="45"/> </Rectangle.LayoutTransform>

</Rectangle>

<!-- A Button with a skew transformation -->

<Button Content ="Click Me!" Grid.Row="0" Grid.Column="1" Width="95" Height="40"> <Button.RenderTransform>

<SkewTransform AngleX ="20" AngleY ="20"/> </Button.RenderTransform>

</Button>

1136 CHAPTER 30 WPF 2D GRAPHICAL RENDERING, RESOURCES, AND THEMES

<!-- An Ellipse that has been scaled by 20% -->

<Ellipse Fill ="Blue" Grid.Row="0" Grid.Column="2" Width="5" Height="5"> <Ellipse.RenderTransform>

<ScaleTransform ScaleX ="20" ScaleY ="20"/> </Ellipse.RenderTransform>

</Ellipse>

<!-- A Button that has been skewed, rotated, and skewed again -->

<Button Content ="Me Too!" Grid.Row="0" Grid.Column="3" Width="50" Height="40"> <Button.RenderTransform>

<TransformGroup>

<SkewTransform AngleX ="20" AngleY ="20"/> <RotateTransform Angle ="45"/> <SkewTransform AngleX ="5" AngleY ="20"/> </TransformGroup>

</Button.RenderTransform>

</Button>

Our first type, the <Rectangle>, makes use of the RotateTransform type that renders the UI item at a 45-degree angle via the Angle property. The first <Button> type uses a SkewTransform object, which slants the rendering of the widget based on (at minimum) the AngleX and AngleY properties. The <ScaleTransform> type used by the <Ellipse> grows the height and width of the circle quite a bit. Notice, for example, that the Height and Width properties of the <Ellipse> are set to 5, while the rendered output is much larger. Last but not least, the final <Button> type makes use of the <TransformGroup> type to apply a skew and a rotation. Figure 30-9 shows the rendered output.

Figure 30-9. Applying transformations

Source Code The FunWithTransformations.xaml file can be found under the Chapter 30 subdirectory.

Understanding WPF’s Animation Services

In addition to providing a full-fledged API to support 2D (and 3D) graphical rendering, WPF supplies a programming interface to support animation services. The term “animation” may bring to mind visions of spinning company logos, a sequence of rotating image resources (to provide the illusion of movement), text bouncing across the screen, or specific types of programs such as video games or multimedia applications.

While WPF’s animation APIs could certainly be used for such purposes, animation can be used anytime you wish to give an application additional flair. For example, you could build an animation

CHAPTER 30 WPF 2D GRAPHICAL RENDERING, RESOURCES, AND THEMES

1137

for a button on a screen that magnifies slightly in size when the mouse cursor hovers within its boundaries (and shrinks back once the mouse cursor moves beyond the boundaries). Perhaps you wish to animate a window so that it closes using a particular visual appearance (such as slowly fading into transparency). The short answer is that WPF’s animation support can be used within any sort of application (business application, multimedia programs/video games, etc.) whenever you wish to provide a more engaging user experience.

Like many other aspects of WPF, the notion of building animations is nothing new in and of itself. However, unlike other APIs you may have used in the past (including GDI+), developers are not required to author the necessary infrastructure by hand. Under WPF, we have no need to create the background threads (or timers) used to advance the animation sequence, define custom types to represent the animation, or bother with numerous mathematical calculations.

Like other aspects of WPF, we are able to build an animation entirely using XAML, entirely using C# code, or a combination of the two. Furthermore, Microsoft Expression Blend (mentioned a few times within these WPF-centric chapters) can be used to design an animation using integrated tools and wizards without seeing a bit of C# or XAML in the foreground. This approach is ideal for graphic artists, who may not feel comfortable viewing such details.

The Role of Animation-Suffixed Types

To understand WPF’s animation support, we must begin by examining the core animation types within the System.Windows.Media.Animation namespace of PresentationCore.dll. Here you will find a number of class types that all take the Animation suffix (ByteAnimation, ColorAnimation, DoubleAnimation, In32Animation, etc.). Obviously, these types are not used to somehow provide an animation sequence directly to a variable to a particular data type (after all, how exactly could we animate the value “9” using an Int32Animation?). Rather, these Animation-suffixed types can be connected to any dependency property of a given type that matches the underlying types.

Note Allow me to repeat this key point: Animation-suffixed types can only work in conjunction with dependency properties, not normal CLR properties (see Chapter 29). If you attempt to apply animation objects to CLR properties, you will receive a compile-time error.

For example, consider the Label type’s Height and Width properties, both of which are dependency properties wrapping a double. If you wish to define an animation that would increase the height of a label over a time span, you could connect a DoubleAnimation object to the Height property and allow WPF to take care of the details of performing the actual animation itself. By way of another example, if you wish to transition the color of a brush type from green to yellow, you could do so using the ColorAnimation type.

Regardless of which Animation-suffixed type you wish to make use of, they all define a handful of key properties that control the starting and ending values used to perform the animation:

To: This property represents the animation’s ending value.

From: This property represents the animation’s starting value.

By: This property represents the total amount by which the animation changes its starting value.

Despite the fact that all Animation-suffixed types support the To, From, and By properties, they do not receive them via virtual members of a base class. The reason for this is that the underlying types wrapped by these properties vary greatly (integers, colors, Thickness objects, etc.), and representing all possibilities using a System.Object would cause numerous boxing/unboxing penalties for stack-based data.

1138 CHAPTER 30 WPF 2D GRAPHICAL RENDERING, RESOURCES, AND THEMES

You might also wonder why .NET generics were not used to define a single animation class with a single type parameter (e.g., Animate<T>). Again, given that there are so many underlying data types (colors, vectors, ints, strings, etc.) used by animated dependency properties, it would not be as clean a solution as you might expect (not to mention XAML has only limited support for generic types).

The Role of the Timeline Base Class

Although a single base class was not used to define virtual To, From, and By properties, the Animation- suffixed types do share a common base class: System.Windows.Media.Timeline. This type provides a number of additional properties that control the pacing of the animation, as described in Table 30-6.

Table 30-6. Key Members of the Timeline Base Class

Properties

Meaning in Life

AccelerationRatio, DecelerationRatio,

These properties can be used to control the overall

SpeedRatio

pacing of the animation sequence.

AutoReverse

This property gets or sets a value that indicates whether

 

the timeline plays in reverse after it completes a

 

forward iteration.

BeginTime

This property gets or sets the time at which this

 

timeline should begin. The default value is 0, which

 

begins the animation immediately.

Duration

This property allows you to set a duration of time to

 

play the timeline.

FillBehavior, RepeatBehavior

These properties are used to control what should

 

happen once the timeline has completed (e.g., repeat

 

the animation, do nothing, etc.).

 

 

Authoring an Animation in C# Code

Our first look at WPF’s animation services will make use of the DoubleAnimation type to control various properties of various Label types on a main window. Create a new WPF Windows Application named AnimatedLabel, and design a <Grid> consisting of two rows and two columns. Into the first column place a Button type in each cell and handle the Click event for each widget. In the second column, place a Label type into each cell (named lblHeight and lblTransparency). Figure 30-10 shows one possible UI.

Figure 30-10. The initial UI of the AnimatedLabel application

CHAPTER 30 WPF 2D GRAPHICAL RENDERING, RESOURCES, AND THEMES

1139

Now, within each Click event handler, author the following code:

public partial class MainWindow : System.Windows.Window

{

public MainWindow()

{

InitializeComponent();

}

protected void btnAnimatelblMessage_Click(object sender, RoutedEventArgs args)

{

// This will grow the height of the label.

DoubleAnimation dblAnim = new DoubleAnimation(); dblAnim.From = 40;

dblAnim.To = 60; lblHeight.BeginAnimation(Label.HeightProperty, dblAnim);

}

protected void btnAnimatelblTransparency_Click(object sender, RoutedEventArgs args)

{

// This will change the opacity of the label.

DoubleAnimation dblAnim = new DoubleAnimation(); dblAnim.From = 1.0;

dblAnim.To = 0.0; lblTransparency.BeginAnimation(Label.OpacityProperty, dblAnim);

}

}

Notice in each Click event handler we set the From and To values of the DoubleAnimation type to represent the beginning and ending value. After this point, we call BeginAnimation() on the correct Label object, passing in the correct dependency property field of the related widget (again, the Label) followed by the Animation-suffixed object used to perform the animation.

Note Recall from our examination of dependency properties in Chapter 29 that public read-only static fields of type DependencyObject are used to represent a given dependency property exposed by the (optional) CLR property wrapper.

If you now run your application and click each button, you will find that the lblHeight label will grow in size, while the lblTransparency button will slowly fade from view.

Controlling the Pacing of an Animation

By default, an animation will take approximately one second to transition between the values assigned to the From and To properties. For example, if you were to modify the Click event handler that grows the Height of the Label from 40 to 200 (a larger increase than what we currently have in place), it would still take approximately one second to do so.

If you wish to define a custom amount of time for an animation’s transition, you may do so via the Duration property, which can be set to an instance of a Duration object. Typically, the time span is established by passing a TimeSpan object to the Duration’s constructor. Consider the following update to the current Click handlers, which will grow the label’s height over four seconds and fade the other label from view over the course of ten seconds:

1140 CHAPTER 30 WPF 2D GRAPHICAL RENDERING, RESOURCES, AND THEMES

protected void btnAnimatelblMessage_Click(object sender, RoutedEventArgs args)

{

// Take 4 seconds to complete the animation.

DoubleAnimation dblAnim = new DoubleAnimation(); dblAnim.From = 40;

dblAnim.To = 200;

dblAnim.Duration = new Duration(TimeSpan.FromSeconds(4)); lblHeight.BeginAnimation(Label.HeightProperty, dblAnim);

}

protected void btnAnimatelblTransparency_Click(object sender, RoutedEventArgs args)

{

// This will change the opacity of the label

DoubleAnimation dblAnim = new DoubleAnimation(); dblAnim.From = 1.0;

dblAnim.To = 0.0;

dblAnim.Duration = new Duration(TimeSpan.FromSeconds(10)); lblTransparency.BeginAnimation(Label.OpacityProperty, dblAnim);

}

Note The BeginTime property of an Animation-suffixed type also takes a TimeSpan object. Recall this property can be set to establish a wait time before starting an animation sequence.

Reversing and Looping an Animation

You can also tell Animation-suffixed types to play an animation in reverse at the completion of the animation sequence by setting the AutoReverse property to true. For example, the following update to our Click event handler will cause the Label to grow from 40 to 100 pixels in height, after which it will “shrink” from 100 back to 40 (over the course of eight seconds, four seconds each “direction”):

// Reverse when done. dblAnim.AutoReverse = true;

If you wish to have an animation repeat some number of times (or to never stop once activated), you can do so using the RepeatBehavior property, which is common to all Animation-suffixed types. The RepeatBehavior property is set to an object of the same name. If you pass in a simple numerical value to the constructor, you can specify a hard-coded number of times to repeat. On the other hand, if you pass in a TimeSpan object to the constructor, you can establish an amount of time the animation should repeat. Finally, if you wish an animation to loop ad infinitum, you can simply specify RepeatBehavior.Forever. Consider the following ways we could change the height of our Label object:

// Loop forever.

dblAnim.RepeatBehavior = RepeatBehavior.Forever;

// Loop three times.

dblAnim.RepeatBehavior = new RepeatBehavior(3);

// Loop for 30 seconds.

dblAnim.RepeatBehavior = new RepeatBehavior(TimeSpan.FromSeconds(30));

CHAPTER 30 WPF 2D GRAPHICAL RENDERING, RESOURCES, AND THEMES

1141

Source Code The AnimatedLabel project can be found under the Chapter 30 subdirectory.

Authoring an Animation in XAML

When you need to dynamically interact with the state of an animation, your best approach is to do so in procedural code, as we have just done. However, if you have a “fixed” animation that is predefined and will not require runtime interaction, you can author your entire animation sequence in XAML. For the most part, this process is identical to what you have already seen; however, the various Animation-suffixed types are wrapped within storyboard types. The storyboard types in turn are associated to an event trigger.

Let’s walk through a complete example of an animation defined in terms of XAML, followed by a detailed breakdown. Our goal is to represent the eternally growing and shrinking Label of the previous example using XAML. Consider the following markup, which will be examined in the next several sections:

<Label Content = "Interesting..."> <Label.Triggers>

<EventTrigger RoutedEvent = "Label.Loaded"> <EventTrigger.Actions>

<BeginStoryboard>

<Storyboard TargetProperty = "Height">

<DoubleAnimation From ="40" To = "200" Duration = "0:0:4" RepeatBehavior = "Forever"/>

</Storyboard>

</BeginStoryboard>

</EventTrigger.Actions>

</EventTrigger>

</Label.Triggers>

</Label>

The Role of Storyboards

Working from the innermost element outward, we first encounter the <DoubleAnimation> type, which is making use of the same properties we set in procedural code (To, From, Duration, and RepeatBehavior). As mentioned, Animation-suffixed types are placed within a <Storyboard> type, which is used to map the animation to a given property on the parent type via the TargetProperty property (which again in this case is Height).

The reason for this level of indirection is that XAML does not support a syntax to invoke methods on objects, such as the necessary BeginAnimation() method of the Label. Essentially a <Storyboard> and the <BeginStoryboard> parent are the XAML-centric version of specifying the following procedural code:

// This necessary animation logic is represented with storyboards in XAML. lblHeight.BeginAnimation(Label.HeightProperty, dblAnim);

The Use of <EventTrigger>

Once the <Storyboard> has been defined, we next need to define an event trigger to contain it. WPF supports three different types of triggers that allow you to define a set of actions that will occur when a given routed event is raised.