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

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

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

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

The first type of trigger observes conditions for dependency properties on a type. When defining triggers for dependency properties, you will do so by defining the trigger using <Trigger> elements. The second type of trigger accounts for “normal” .NET property types and is defined within a <DataTrigger> element. This flavor of trigger can be helpful for data binding operations. We will look at the <Trigger> element later in this chapter during our examination of styles and themes.

The final type of trigger relevant for the current example is an event trigger (defined within an <EventTrigger> element), which is used when building WPF animations. Here, our <EventTrigger> is connected to the Loaded event of the Label. When this event fires, the action to take is to execute the <DoubleAnimation> sequence on the Label’s Height property.

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

The Role of Animation Key Frames

The final aspect of the WPF animation system we will examine is the use of key frames. The System. Windows.Media.Animation namespace also contains a number of members that end with the

AnimationUsingKeyFrames suffix (ByteAnimationUsingKeyFrames, ColorAnimationUsingKeyFrames, DoubleAnimationUsingKeyFrames, In32AnimationUsingKeyFrames, etc.). Each of these types provides a

Duration property, which controls how long the entire animation sequence should take. However, it is up to the key frames themselves to inform the animation system when they are up for duty.

Unlike the Animation-suffixed types, which can only move between a starting point and an ending point, the key frame counterparts allow us to create a collection of specific values for an animation that should take place at specific times. For example, the AnimationUsingKeyFrames-suffixed types could allow us to create an animation that bounces a circle around window, causes a custom image to move around the outline of a geometric shape, or cycles the colors within a text box over a time slice.

Within the scope of an AnimationUsingKeyFrames-suffixed element, you may add a collection of three different key frame types, each of which controls that frame of movement of the animated item:

LinearXXXKeyFrame: The linear key frame types are used to move an item between points on a straight line.

SplineXXXKeyFrame: The spline key frame types are used to move an item along a Bezier curve, using the KeySpline property.

DiscreteXXXKeyFrame: The discrete key frame types do not provide a transition between key frames. For example, this can be useful when “animating” string data that grows in size or a border that “animates” between various colors.

Following a similar pattern, the exact name of the subelements will be based on the type of item you are animating (doubles, colors, bools, etc.). Here, XXX is obviously being used as a placeholder. The real names of any of these three key frame types would be along the lines of

LinearDoubleKeyFrame, SplineDoubleKeyFrame, and DiscreteDoubleKeyFrame.

Animation Using Discrete Key Frames

To illustrate the use of a discrete key frame type, assume you wish to build a Button type that animates its content in such a way that over the course of three seconds, the value “OK!” appears one character at a time. Also assume that this behavior should happen as soon as the button has loaded into memory, and it should repeat continuously. To see this behavior firsthand, author the following XAML within the SimpleXamlApp.exe program you created in Chapter 28:

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

1143

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<Grid>

<Button Name="myButton" Height="40"

FontSize="16pt" FontFamily="Verdana" Width = "100"> <Button.Triggers>

<EventTrigger RoutedEvent="Button.Loaded"> <BeginStoryboard>

<Storyboard>

<StringAnimationUsingKeyFrames RepeatBehavior = "Forever" Storyboard.TargetName="myButton" Storyboard.TargetProperty="Content" Duration="0:0:3" FillBehavior="HoldEnd">

<DiscreteStringKeyFrame Value="" KeyTime="0:0:0" /> <DiscreteStringKeyFrame Value="O" KeyTime="0:0:1" /> <DiscreteStringKeyFrame Value="OK" KeyTime="0:0:1.5" /> <DiscreteStringKeyFrame Value="OK!" KeyTime="0:0:2" />

</StringAnimationUsingKeyFrames>

</Storyboard>

</BeginStoryboard>

</EventTrigger>

</Button.Triggers>

</Button>

</Grid>

</Window>

Notice first of all that we have defined an event trigger for our button to ensure that our storyboard executes when the button has loaded. The StringAnimationUsingKeyFrames type is in charge of changing the content of our button, via the Storyboard.TargetName and Storyboard.

TargetProperty values. Within the scope of our <StringAnimationUsingKeyFrames> element, we have defined three DiscreteStringKeyFrame types, which change the button’s content over the course of two seconds (note that the duration established by StringAnimationUsingKeyFrames is a total of three seconds, so we will see a slight pause between the final “!” and looping “O”).

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

Animation Using Linear Key Frames

To see the use of a linear key frame at work, consider the following XAML, which spins a Button in a complete circle using the center of the button as the point of origin. Once the 360-degree rotation has completed, the button will then flip itself upside down (and then right side up again). Assume this XAML markup is defined within a <Grid> type:

<!-- This button will rotate in a circle, then flip, when clicked -->

<Button Name="myAnimatedButton" Width="120" Height = "40" RenderTransformOrigin="0.5,0.5" Content = "OK">

<Button.RenderTransform> <RotateTransform Angle="0"/>

</Button.RenderTransform>

<!-- The animation is triggered when the button is clicked -->

<Button.Triggers>

<EventTrigger RoutedEvent="Button.Click">

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

<BeginStoryboard>

<Storyboard>

<DoubleAnimationUsingKeyFrames

Storyboard.TargetName="myAnimatedButton"

Storyboard.TargetProperty= "(Button.RenderTransform).(RotateTransform.Angle)" Duration="0:0:2" FillBehavior="Stop">

<DoubleAnimationUsingKeyFrames.KeyFrames> <LinearDoubleKeyFrame Value="360" KeyTime="0:0:1" /> <DiscreteDoubleKeyFrame Value="180" KeyTime="0:0:1.5" />

</DoubleAnimationUsingKeyFrames.KeyFrames>

</DoubleAnimationUsingKeyFrames>

</Storyboard>

</BeginStoryboard>

</EventTrigger>

</Button.Triggers>

</Button>

This Button’s definition begins by specifying a value to the RenderTransformOrigin property, which ensures the rotation occurs using the dead center of the button as the turning point. Next, we establish the initial starting value for the rendering transformation, using the nested <Button. RenderTransform> scope (note the starting angle is zero). Finally, we define an event trigger to ensure our storyboard will execute when the end user clicks the Button widget.

With these initial settings complete, we create a <Storyboard> scope that makes use of the DoubleAnimationUsingKeyFrames type. Notice that the target of this storyboard is our Button instance (myAnimatedButton) and the property we are targeting on this object is ultimately the Angle property. However, notice the new bit of XAML syntax that we must use when assigning a dependency property to a property value:

<DoubleAnimationUsingKeyFrames

Storyboard.TargetName="myAnimatedButton"

Storyboard.TargetProperty="(Button.RenderTransform).(RotateTransform.Angle)"

Duration="0:0:2" FillBehavior="Stop">

As you can see, we must wrap dependency properties within parentheses; therefore, the single bold line of code allows us to say in effect, “I am animating the Angle property of the RotateTransform object exposed by the button.” This point aside, the total time allowed for this animation is set to two seconds.

Within the scope of our DoubleAnimationUsingKeyFrames type, we add two key frame types. The first (LinearDoubleKeyFrame) will rotate the button 360 degrees over a one-second time period. Approximately half a second later, the DiscreteDoubleKeyFrame flips the button 180 degrees (turning the button upside down). Finally, once the animation expires (again, half a second later, given our Duration property of the DoubleAnimationUsingKeyFrames type), the button flips right side up again, as the DoubleAnimationUsingKeyFrames type has a FillBehavior value of Stop (which returns the item to the initial state).

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

That wraps up our look at the basic animation services (and 2D rendering techniques) that are baked into WPF. Both of these topics could easily require a book of their own to cover all of the bells and whistles. Nevertheless, at this time you should be in a solid position for further exploration.

Next up, we will turn our attention to how to package application resources.

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

1145

Understanding the WPF Resource System

Our next task is to examine the seemingly unrelated topic of embedding and accessing application resources. WPF supports two flavors of resources, the first of which is binary resources, which represents what most programmers consider a “resource” in the traditional sense of the word (bitmap files, icons, string tables, etc.).

The second flavor of resources, termed object resources or logical resources, represents any type of .NET object that is named and embedded within an assembly. As you will see, logical resources are extremely helpful when working with graphical data of any sort, given that you can define commonly used graphic primitives (brushes, pens, animations, etc.) within a resources dictionary.

Working with Binary Resources

As mentioned, binary resources are the auxiliary bits used by a .NET application, such as string tables, icons, and image files (e.g., company logos, images for an animation, etc.). If you are creating a WPF application using Visual Studio, you are able to instruct the compiler to embed an external resource into the assembly simply by specifying the Resource build option. To illustrate, create a new WPF Windows Application named FunWithResources. Update your initial XAML definition for the main window with a three-column <Grid>, where the first cell contains an Image widget:

<Window x:Class="FunWithResources.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="FunWithResources" Height="207" Width="612" WindowStartupLocation="CenterScreen">

<Grid>

<Grid.ColumnDefinitions>

<ColumnDefinition/>

<ColumnDefinition/>

<ColumnDefinition/>

</Grid.ColumnDefinitions>

<Image Grid.Column ="0" Name ="companyLogo"/> </Grid>

</Window>

The first goal is to embed an image file into our application as a binary resource, which will be used to set the Source property of the companyLogo Image control. Using your image file of choice, add it into your current project using the Project Add Existing Item menu option of Visual Studio (here, I am assuming a file named IntertechBlue.gif).

The Resource Build Action

Once you have added an external image file to your application, you should now see it listed within Solution Explorer. When selected, the Properties can now be used to instruct the compiler how to process these external files using the Build Action option (see Figure 30-11).

If you select the default setting, Resource, the compiler will embed the data into the .NET assembly, and therefore these external files do not need to be shipped with the completed application. Assuming you have set the Build Action of your image file to Resources, you can update the XAML definition of the Image control as so (notice you refer to the name of the binary resource by name):

<Image Grid.Column ="0" Name ="companyLogo" Source ="IntertechBlue.gif"/>

When you now compile and run your program, you should see your image file stretched within the first cell of your <Grid>.

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

Figure 30-11. Options for packaging binary resources

Note Be careful that your WPF applications use the Resource option to embed resources, which is a WPFaware option. The tempting-sounding Embedded Resource option is used for Windows Forms application.

If you load your compiled application into reflector.exe (see Chapter 2), you can view the embedded resource directly, as shown in Figure 30-12.

Figure 30-12. An embedded binary resource

The Content Build Action

It is also possible to set the Build Action of a related external file to Content, rather than Resource. When you do so, your assembly is compiled in such a way that it is aware of the relative location of the external file, but does not actually contain the binary data. This setting can be helpful when you deploy an application that contains a subfolder (or two) containing external resources that need to be replaced from time to time, or when the location of external resources is on a network share point.

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

1147

In these cases, the WPF resource management system defines a number of additional URI formats (including a syntax to load resources embedded in an external assembly) beyond a simple file name. If you are interested in examining the various URI formats that can be used with local resources not compiled into the current assembly, look up the topic “Pack URIs in WPF” within the

.NET Framework 3.5 SDK documentation.

The Role of Object (a.k.a. Logical) Resources

WPF resources really come into their own when you embed custom .NET objects into an assembly for use within your application. At first glance, this may seem like a very odd thing to do. However, by doing so you can define commonly used graphical elements (brushes, pens, etc.) for use by multiple areas of your program. This technique is often used when creating custom themes and styles for your WPF applications, which we will examine next.

Defining and Applying Styles for WPF Controls

When you are building the UI of a WPF application, it is not uncommon for a family of widgets to all have a shared look and feel. For example, you may wish to ensure that all button types have the same height, width, background color, and font size for their string content. While you could do so by setting each button’s individual properties to identical values, this approach certainly makes it difficult to implement changes down the road, as you would need to reset the same set of properties on multiple objects for every change.

Thankfully, WPF offers numerous ways to change the look and feel of UI elements (styles, templates, skins, etc.) with minimal fuss and bother. As you will see, building such styles entails the use of each topic presented thus far in this chapter (2D graphics, animations, and resources). To get the ball rolling, let’s begin by examining the use of styles.

Working with Inline Styles

The first way in which you can change the look and feel of a WPF widget is through styles. A style is a kindred spirit to a web-based style sheet, in that styles do not have a UI of their own, but simply establish a number of property settings that other UI elements can adopt. Any descendent of Control has the ability to support styles (via the Style property), including, of course, the Window itself. When you wish to author a style, one possible approach is to make use of property-element syntax that allows you to assign a style “inline.”

To illustrate, update your current <Grid> definition of the FunWithResources project to define a Button within the remaining two cells. Now, consider the following markup, which establishes a custom style for a button named btnClickMe (but not the second button, btnClickMeToo):

<Window x:Class="FunWithResources.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="FunWithResources" Height="207" Width="612" WindowStartupLocation="CenterScreen">

<Grid>

<Grid.ColumnDefinitions>

<ColumnDefinition/>

<ColumnDefinition/>

<ColumnDefinition/>

</Grid.ColumnDefinitions>

<Image Grid.Column ="0" Name ="companyLogo" Source ="IntertechBlue.gif"/>

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

<!-- This button has an inline style! -->

<Button Grid.Column ="1" Name="btnClickMe" Height="80" Width = "100" Content ="Click Me">

<Button.Style>

<Style>

<Setter Property ="Button.FontSize" Value ="20"/> <Setter Property ="Button.Background">

<Setter.Value>

<LinearGradientBrush StartPoint="0,0" EndPoint="1,1"> <GradientStop Color="Green" Offset="0" /> <GradientStop Color="Yellow" Offset="0.25" /> <GradientStop Color="Pink" Offset="0.75" /> <GradientStop Color ="Red" Offset="1" />

</LinearGradientBrush>

</Setter.Value>

</Setter>

</Style>

</Button.Style>

</Button>

<!-- No style for this button! -->

<Button Grid.Column ="2" Name="btnClickMeToo" Height="80" Width = "100" Content ="Me Too"/> </Grid>

</Window>

As you can see, a WPF style is defined using the <Style> element. Within this scope, we define any number of <Setter> elements, which are used to establish the name/value pairs of the properties we wish to set. Here we have established a FontSize property of the Button to be 20, and the Background property of the Button type via a LinearGradientBrush type that is composed of four interconnected colors.

Note If necessary, you can programmatically establish a style in your code file. Simply set the Style property on the control-derived type.

While this approach to building a style is syntactically correct, one obvious limitation is that inline styles are bound to a specific instance of a UI type (btnClickMe in our example), not each button within the scope. In Figure 30-13, notice that the second Button type, btnClickMeToo, is unaffected by the style assigned to btnClickMe.

Figure 30-13. Inline styles are bound to the control that defined them.

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

1149

Working with Named Styles

To define a style that can be used by multiple UI elements of the same type (e.g., all Buttons, all ListBoxes, etc.), you may define the style within a container’s resource dictionary, thereby defining an object (a.k.a. logical) resource. For example, you could add a named style to the <Window>’s resource dictionary and identify it by name through the Key property. That way, the same theme can be referenced everywhere in your XAML document. Consider the following update:

<Window x:Class="FunWithResources.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="FunWithResources" Height="207"

Width="612" WindowStartupLocation="CenterScreen">

<!-- Add a logical resource to the window's resource dictionary -->

<Window.Resources>

<Style x:Key ="MyFunkyStyle">

<Setter Property ="Button.FontSize" Value ="20"/> <Setter Property ="Button.Background">

<Setter.Value>

<LinearGradientBrush StartPoint="0,0" EndPoint="1,1"> <GradientStop Color="Green" Offset="0" /> <GradientStop Color="Yellow" Offset="0.25" /> <GradientStop Color="Pink" Offset="0.75" /> <GradientStop Color ="Red" Offset="1" />

</LinearGradientBrush>

</Setter.Value>

</Setter>

</Style>

</Window.Resources>

<Grid>

<Grid.ColumnDefinitions>

<ColumnDefinition/>

<ColumnDefinition/>

<ColumnDefinition/>

</Grid.ColumnDefinitions>

<Image Grid.Column ="0" Name ="companyLogo" Source ="IntertechBlue.gif"/>

<!-- Both buttons now use the same style -->

<Button Grid.Column ="1" Name="btnClickMe" Height="80" Width = "100"

Style ="{StaticResource MyFunkyStyle}" Content = "Click Me"/> <Button Grid.Column ="2" Name="btnClickMeToo" Height="80" Width = "100"

Style ="{StaticResource MyFunkyStyle}" Content = "Me Too"/>

</Grid>

</Window>

This time, note that the style has been defined within the scope of a <Window.Resources> element and has been assigned the name MyFunkyStyle via the Key attribute. Beyond these points, the style declaration itself is identical to the previous style we created inline. Also notice that when we want to apply a style (as we do within the <Button> definitions), we do so using the StaticResource markup extension (see Chapter 28). With this update, each button takes on the same look and feel, as shown in Figure 30-14.

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

Figure 30-14. Named styles can be used by multiple UI elements in the same scope.

Overriding Style Settings

It is important to point out that when a UI element adopts a particular style (either an inline style or a named style) it has the freedom to “override” a property setting. For example, assume you want the second Button type to make use of the Background setting established by MyFunkyStyle, but you prefer a smaller font. To do so, simply assign a new value to the property you wish to change within the opening XAML element:

<Button Grid.Column ="2" Name="btnClickMeToo" Height="80" Width = "100" Style ="{StaticResource MyFunkyStyle}" FontSize = "10" Content = "Me Too"/>

Subclassing Existing Styles

It is also possible to build new styles using an existing style, via the BasedOn property, provided the style you are extending has been given a specific name via the Key property. For example, the following NewFunkyStyle style (which is added as a new child element of the <Window.Resources> scope) gathers the font size and background color of MyFunkyStyle, but rotates the UI element 20 degrees:

<Style x:Key ="NewFunkyStyle" BasedOn = "{StaticResource MyFunkyStyle}"> <Setter Property = "Button.Foreground" Value = "Blue"/>

<Setter Property = "Button.RenderTransform"> <Setter.Value>

<RotateTransform Angle = "20"/> </Setter.Value>

</Setter>

</Style>

Figure 30-15 shows the new style in action when applied to each button.

Figure 30-15. Using a derived style

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

1151

Widening Styles

Moving a style into a resource dictionary is a step in the right direction to be sure. However, what if you want to use the same style for multiple UI elements? Currently, MyFunkyStyle can only be applied to button widgets, given that the style explicitly references the Button type using propertyelement syntax.

One of the very interesting aspects of WPF styles is that the values assigned within a <Setter> scope honor the concept of inheritance. Thus, if we set properties on the common parent of all UI elements (System.Windows.Controls.Control) within our style, we can effectively define a style that is common to all WPF controls. For example, the following style update:

<Window.Resources>

<Style x:Key ="MyFunkyStyle">

<Setter Property ="Control.FontSize" Value ="20"/> <Setter Property ="Control.Background">

<Setter.Value>

<LinearGradientBrush StartPoint="0,0" EndPoint="1,1"> <GradientStop Color="Green" Offset="0" /> <GradientStop Color="Yellow" Offset="0.25" /> <GradientStop Color="Pink" Offset="0.75" /> <GradientStop Color ="Red" Offset="1" />

</LinearGradientBrush>

</Setter.Value>

</Setter>

</Style>

...

</Window.Resources>

allows us to apply MyFunkyStyle to TextBox types as well as Button types (or to any item extending Control, for that matter). Assume the following new UI element added within a new column of the current <Grid>:

<TextBox Grid.Column ="3" Name="txtAndMe" Height="40" Width = "100"

Style ="{StaticResource MyFunkyStyle}" Text = "And me!"/>

Note When you are building a style that is making use of a base class type, you needn’t be concerned if you assign a value to a dependency property not supported by derived types. If the derived type does not support a given dependency property, it is ignored.

Narrowing Styles

If you wish to define a style that can be applied only to certain types of UI elements (e.g., only Buttons and nothing else), you can do so by setting the TargetType property on the style’s opening element. This property expects a metadata description of the target widget, so you will make use of the x:Type markup extension (see Chapter 28). By way of illustration, we could update MyFunkyStyle as follows. With this update, it would now be a markup error for the previous TextBox to attempt to apply this style.

<Style x:Key ="MyFunkyStyle" TargetType = "{x:Type Button}">

...

</Style>