Skins

Skinning refers to the act of changing an application’s appearance (or skin) on the fly, typically by third parties. WPF doesn’t have a distinct concept called a skin, nor does it have a formal notion of skinning, but it doesn’t need one. You can easily write an application or a component that supports dynamic skinning by using WPF’s dynamic resource mechanism (described in Chapter 12, “Resources”) combined with Styles and/or templates.

To support skinning in an application, one of the first things you need to do is decide on a data format. Whereas it might make sense to invent a format for Win32 or Windows Forms applications, XAML is a no-brainer data format for skins in WPF applications if you are okay with loading arbitrary code into your process. (Loading someone’s XAML is like loading someone’s add-in; it has the power to invoke unrelated code and may therefore do something malicious. See the FAQ at the end of this section for more information.)

But what should such XAML files look like?

Often, the initial instinct is to load an entire Window or Page dynamically from a loose XAML file and hook it up to the appropriate logic (using the technique shown at the end of Chapter 2, “XAML Demystified”). Loading your entire user interface on the fly gives complete flexibility, but, in most cases, it’s probably too much flexibility. Authors of such XAML files would need a lot of discipline to include all the right elements with all the right names and all the right event handlers, and so on. (Either that or the code to connect the user interface to the application logic needs to be extremely forgiving.) Visual Studio follows this approach with its XAML-based Start Page. By loading an arbitrary Page, authors can plug in something completely different. If all they want to do is reskin what’s there, they need to start by copying the existing Page and tweak it from there.

For environments in which you don’t want to encourage complete user interface replacement, the best approach is to make ResourceDictionary the root of a skin representation. ResourceDictionary makes a great extensibility point in general because of the ease with which it can be swapped in and out or merged with others. When defining a skin, it makes sense for the ResourceDictionary to contain Styles and/or templates.

To demonstrate skinning, the following Window is a hypothetical progress dialog, shown in Figure 14.13:

<Window x:Class="WindowsApplication1.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Please Wait" Height="200" Width="300" ResizeMode="NoResize">
  <Grid>
    <StackPanel Style="{DynamicResource DialogStyle}">
      <Label Style="{DynamicResource HeadingStyle}">Loading...</Label>
      <ProgressBar Value="35" MinHeight="20" Margin="20"/>
      <Button Style="{DynamicResource CancelButtonStyle}" Width="70"
        Click="Cancel_Click">Cancel</Button>
    </StackPanel>
  </Grid>
</Window>

Image

FIGURE 14.13 A dialog box, shown with its default skin.

Notice that most of the Window’s elements are given explicit Styles. This is not a requirement for skinning, but it’s often a nice touch for giving skin authors more control over the visual experience. For example, suppose you want to give a specific look to a Cancel Button that’s different from the look you want for all other Buttons. Marking all Cancel Buttons with an explicit CancelButtonStyle allows you to do just that. Referencing the explicit Styles as dynamic resources is critical to enable them to be updated at arbitrary times.


Tip

When giving an element a Style that you expect to be reskinned dynamically, don’t forget to reference it as a dynamic resource!


So that it will have the look shown in Figure 14.13, the preceding Window is paired with the following App.xaml file that provides a default definition of each Style resource:

<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Window1.xaml">
    <Application.Resources>
      <Style x:Key="DialogStyle" TargetType="{x:Type StackPanel}">
        <Setter Property="Margin" Value="20"/>
      </Style>
      <Style x:Key="HeadingStyle" TargetType="{x:Type Label}">
        <Setter Property="FontSize" Value="16"/>
        <Setter Property="FontWeight" Value="Bold"/>
      </Style>
      <Style x:Key="CancelButtonStyle" TargetType="{x:Type Button}"/>
    </Application.Resources>
</Application>

Notice that CancelButtonStyle is empty, so applying it to a Button has no effect. This is perfectly valid because the expectation is that a skin might replace this Style with something more meaningful.

With this in place, a skin file could simply look like the following:

<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Style x:Key="DialogStyle" TargetType="{x:Type StackPanel}">
    ...
  </Style>
  <Style x:Key="HeadingStyle" TargetType="{x:Type Label}">
    ...
  </Style>
  <Style x:Key="CancelButtonStyle" TargetType="{x:Type Button}">
    ...
  </Style>
  Any additional styles...
</ResourceDictionary>

Then all the host application needs to do is dynamically load the skin XAML file and assign it as the new Application.Resources dictionary. The following code does this for a .xaml file sitting in the current directory:

ResourceDictionary resources = null;
using (FileStream fs = new FileStream("CustomSkin.xaml", FileMode.Open,
  FileAccess.Read))
{
  // Get the root element, which must be a ResourceDictionary
  resources = (ResourceDictionary)XamlReader.Load(fs);
}
Application.Current.Resources = resources;

You could alternatively use code like the following to retrieve a skin file from the Internet at an arbitrary URL:

ResourceDictionary resources = null;
System.Net.WebClient client = new System.Net.WebClient();
using (Stream s = client.OpenRead("http://adamnathan.net/wpf/CustomSkin.xaml"))
{
  // Get the root element, which must be a ResourceDictionary
  resources = (ResourceDictionary)XamlReader.Load(s);
}
Application.Current.Resources = resources;

Because assigning a dictionary to Application.Current.Resources code wipes out the current dictionary, you should also store the default ResourceDictionary if you want to restore it later!


Image FAQ: What happens if a skin doesn’t define a named Style expected by the application?

If you take the approach of completely replacing the current Application.Resources dictionary with a new ResourceDictionary, and if the new dictionary is missing Styles, the affected controls will silently revert to their default appearance. This is true of any dynamic resource that gets removed while the application is running. The dynamic resource mechanism does emit a debug trace, however, much like how data binding reports errors. For example, applying a skin missing a Style called CancelButtonStyle causes the following message to be emitted inside a debugger:

System.Windows.ResourceDictionary Warning: 9 : Resource not found;
ResourceKey='CancelButtonStyle'

To avoid this, another approach would be to iterate through the new resource dictionary and individually set each key/value pair in the application’s resource dictionary.


The progress dialog sample (whose full source code is included with the rest of the book’s code at http://informit.com/title/9780672336973) switches the skin when you click the Cancel Button for demonstration purposes, but for a real application, this action would likely be taken when a user initiates it from some skin-choosing user interface.

In this book’s source code, you’ll find two alternative skins for the progress dialog in Figure 14.13. Figure 14.14 shows the dialog with these two skins.

Image

FIGURE 14.14 Two alternate skins for the dialog.

Notice that the “electric” skin restyles the ProgressBar (using the pie chart template from the previous section) even though the application didn’t give it an explicit Style. It does this by making it a typed Style that applies to all ProgressBars. Fortunately, any additions of, removals of, or changes to typed styles in a ResourceDictionary are automatically reflected the same way as explicit dynamic resources. The skin’s CancelButtonStyle uses a TranslateTransform to reposition it next to the ProgressBar rather than below it. It also does something quite unique for the Label’s Style: It uses a template to send the Label’s content through a “jive translator” web service. (This, of course, works only if the Label contains text.)

The “light and fluffy” skin has its own set of fairly radical changes. Listing 14.12 shows the complete source for this skin.

LISTING 14.12 The “Light and Fluffy” Skin


<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <!-- Make the background a simple gradient -->
  <Style x:Key="DialogStyle" TargetType="{x:Type StackPanel}">
    <Setter Property="Margin" Value="0"/>
    <Setter Property="Background">
    <Setter.Value>
      <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
        <GradientStop Offset="0" Color="LightBlue"/>
        <GradientStop Offset="1" Color="White"/>
      </LinearGradientBrush>
    </Setter.Value>
    </Setter>
  </Style>
  <!-- Rotate and move the main text -->
  <Style x:Key="HeadingStyle" TargetType="{x:Type Label}">
    <Setter Property="Foreground" Value="White"/>
    <Setter Property="FontSize" Value="30"/>
    <Setter Property="FontFamily" Value="Segoe Print"/>
    <Setter Property="RenderTransform">
    <Setter.Value>
      <TransformGroup>
        <RotateTransform Angle="-35"/>
        <TranslateTransform X="-19" Y="55"/>
      </TransformGroup>
    </Setter.Value>
    </Setter>
    <Setter Property="Effect">
    <Setter.Value>
      <DropShadowEffect ShadowDepth="2"/>
    </Setter.Value>
    </Setter>
  </Style>

  <!-- Remove the Cancel button -->
  <Style x:Key="CancelButtonStyle" TargetType="{x:Type Button}">
    <Setter Property="Visibility" Value="Collapsed"/>
  </Style>

  <!-- Wrap the ProgressBar in an Expander -->
  <Style TargetType="{x:Type ProgressBar}">
    <Setter Property="Height" Value="100"/>
    <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type ProgressBar}">
        <Expander Header="More Details" ExpandDirection="Left">
          <ProgressBar Style="{x:Null}"
            Height="30" Value="{TemplateBinding Value}"
            Minimum="{TemplateBinding Minimum}"
            Maximum="{TemplateBinding Maximum}"
            IsEnabled="{TemplateBinding IsEnabled}"
            IsIndeterminate="{TemplateBinding IsIndeterminate}"/>
        </Expander>
      </ControlTemplate>
    </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>


The customized DialogStyle and HeadingStyle are pretty straightforward (although the latter uses a slick drop shadow effect introduced in the next chapter). But this skin, to keep a minimalistic user interface, uses CancelButtonStyle to completely hide the Cancel Button! In this case, doing so is appropriate (assuming that closing the Window behaves the usual way). In other cases, users might not appreciate a skin that hides pieces of the user interface!

The typed Style for ProgressBar also performs an interesting trick for the purpose of simplifying the user interface. It defines a custom template to wrap the ProgressBar inside an Expander (that’s collapsed by default)! The wrapped ProgressBar has several TemplateBindings to keep its display in sync with the templated parent. Notice that this inner ProgressBar is given a null Style. This is necessary to avoid a nasty recursion problem. Without the explicit Style, the inner ProgressBar gets the default typed Style that it’s a part of, making it an Expander inside an Expander inside an Expander, and so on.


Image FAQ: How can I prevent a user-contributed skin from acting maliciously?

There is no built-in mechanism to do this. It might be tempting to try to write your own logic to examine a user-supplied ResourceDictionary and remove things that you consider to be malicious, but this is basically a futile task. For example, if you want to prevent a skin from hiding elements, you can pretty easily remove Setters that operate on Visibility. But what about a skin that makes text the same color as the background? Or a skin that gives controls an empty-looking template? There’s more than one way to skin a cat! (Pun intended.)

And making your user interface unusable is the least of your concerns. Imagine a skin that finds a way to send private information displayed by the application back to a web server. There’s an inherent risk whenever arbitrary code (or XAML!) is executed inside a full-trust application. Loading the XAML in a separate process is one workaround but is probably too cumbersome for most scenarios.

If you’re concerned about this issue, you should probably define your own skin data format that is much more limited in expressiveness. But if you provide an easy way for a user to remove a “malicious skin,” then perhaps you don’t need to worry about this in the first place.


Themes

Whereas skins are application specific, themes generally refer to visual characteristics of the operating system that are reflected in user interface elements of all programs. For example, changing your Windows theme to Windows Classic gives buttons and scrollbars a flat and rectangular look. On Windows XP, switching the default theme’s color scheme between Blue, Olive Green, and Silver affects the color and sheen of standard controls. To maintain consistency with the user’s chosen Windows theme, the built-in WPF controls have a separate control template for each theme.

Consistency with the operating system theme is important for the default control templates. But when somebody creates custom control templates, they typically do so to avoid consistency with the rest of the operating system! Nevertheless, it can still be a nice touch to incorporate elements of the user’s operating system theme to prevent the customized controls from sticking out like a sore thumb. It’s also important to understand how theming works if you create your own custom controls that should blend in with the operating system theme by default.

This section examines how easy it is to create Styles and templates (and, therefore, skins) that adapt to the current theme. There are basically two ways to do this. The first is simple but not as powerful, and the second is a bit more work but completely flexible.

Using System Colors, Fonts, and Parameters

The properties exposed by the SystemColors, SystemFonts, and SystemParameters classes automatically get updated when the Windows theme changes. Therefore, incorporating these into your Styles and templates is an easy way to blend them in with the user’s theme.

The following updated ProgressBar pie chart Style makes use of the SystemColors class to control the colors in its default fill (using the technique explained in Chapter 12):

<Style TargetType="{x:Type ProgressBar}">
<Style.Resources>
  <LinearGradientBrush x:Key="foregroundBrush" StartPoint="0,0" EndPoint="1,1">
    <GradientStop Offset="0"
      Color="{DynamicResource {x:Static SystemColors.InactiveCaptionColorKey}}"/>
    <GradientStop Offset="0.5"
      Color="{DynamicResource {x:Static SystemColors.InactiveCaptionColorKey}}"/>
    <GradientStop Offset="1"
      Color="{DynamicResource {x:Static SystemColors.ActiveCaptionColorKey}}"/>
  </LinearGradientBrush>
</Style.Resources>
<Setter Property="Foreground" Value="{StaticResource foregroundBrush}"/>
<Setter Property="Background"
  Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
...
</Style>

Figure 14.15 shows how the appearance of this Style subtly changes when the user switches Windows themes.

Image

FIGURE 14.15 The same control with the same Style, viewed under two different themes.

Per-Theme Styles and Templates

Many of the built-in WPF controls differ from theme to theme in richer ways than just colors, fonts, and simple measurements. For example, they’re generally simple and flat in the Windows 8 theme, shiny in the Windows 7 Aero theme, and darker with bevels in Windows Classic. This is accomplished by having a separate control template for each theme.

The ability to define your own styles and templates that differ in interesting ways based on the current theme can be quite useful. For example, it could be argued that the Windows Classic version of ProgressBar in Figure 14.15 is too pretty! Someone who uses the Windows Classic theme probably isn’t going to appreciate fancy gradients and other effects!

If you want to create your own per-theme styles and templates, you could programmatically load and swap them whenever the theme changes (using the techniques discussed in the “Skins” section). WPF doesn’t expose a theme-changing event, however, so this would involve intercepting the Win32 WM_THEMECHANGE message (the same way WM_DWMCOMPOSITIONCHANGED is intercepted in Chapter 8, “Exploiting Windows Desktop Features”). Fortunately, WPF does expose a theming mechanism built on top of the low-level Win32 APIs, enabling you to provide per-theme resources with almost no procedural code.

The first step is to organize your theme-specific resources into distinct resource dictionary XAML files (one per theme) that are compiled into your assembly. You can then designate each resource dictionary as a theme dictionary by placing it in a themes subfolder (which must be in the root of your project!) and naming it ThemeName.ThemeColor.xaml (case-insensitive). A theme dictionary can be loaded and applied automatically by WPF when your application launches and whenever the theme changes. Styles inside a theme dictionary are called theme styles.

The following are themes that Microsoft has created, along with their corresponding valid theme dictionary URIs:

Image The Aero2 theme (Windows 8): themesAero2.NormalColor.xaml

Image The Aero theme (Windows Vista and Windows 7): themesAero.NormalColor.xaml

Image The default Windows XP theme: themesLuna.NormalColor.xaml

Image The olive green Windows XP theme: themesLuna.Homestead.xaml

Image The silver Windows XP theme: themesLuna.Metallic.xaml

Image The Windows XP Media Center Edition 2005 and Windows XP Tablet PC Edition 2005 theme: themesRoyale.NormalColor.xaml

Image The Windows Classic theme: themesClassic.xaml

Image The Zune Windows XP theme: themesune.NormalColor.xaml

Image The AeroLite theme (included in Windows 8, but not enabled by default): themesAeroLite.NormalColor.xaml

Note that Windows Classic is a bit special, as it doesn’t have the ThemeColor part of the URI.


Tip

Be sure to provide a generic dictionary whenever you create theme dictionaries. This enables you to provide a consistent experience when encountering an unexpected theme.


Furthermore, you can specify a fallback resource dictionary that gets used if you don’t have a dictionary corresponding to the current theme and color. This fallback dictionary, often called the generic dictionary, must be named themesGeneric.xaml.

With one or more theme dictionaries and/or a generic dictionary in place, you must now opt in to the automatic theming mechanism with an assembly-level ThemeInfoAttribute. This attribute’s constructor takes two parameters of type ResourceDictionaryLocation. The first one specifies where WPF should find the theme dictionaries, and the second one specifies where WPF should find the generic dictionary. Each one can independently be set to the following values:

Image None—Don’t look for a resource dictionary. This is the default value.

Image SourceAssembly—Look for them inside the current assembly.

Image ExternalAssembly—Look for them inside a different assembly, which must be named AssemblyName.ThemeName.dll (where AssemblyName matches the current assembly’s name). WPF uses this scheme for its built-in theme dictionaries, found in PresentationFramework.Aero2.dll, PresentationFramework.Luna.dll, and so on. This is a nice way to avoid having extra copies of resources loaded in memory at all times.

Therefore, a typical use of ThemeInfoAttribute looks like the following:

// Look for the theme dictionaries and the generic dictionary inside this assembly
[assembly:ThemeInfo(ResourceDictionaryLocation.SourceAssembly,
                    ResourceDictionaryLocation.SourceAssembly)]

There’s one final catch to the theming support: It’s designed to provide the default styles for elements. As ThemeInfoAttribute indicates, theme styles must exist in the same assembly defining the target element or a specific companion assembly. Unlike with application-level (or lower) resource dictionaries, you can’t define a typed style for externally defined elements such as Button or ProgressBar in a theme dictionary or generic dictionary in your own application and have it override the default style—unless you use an additional mechanism involving ThemeDictionaryExtension.

ThemeDictionaryExtension is a markup extension that enables you to override the theme styles for any elements. It can reference any assembly containing a set of theme dictionaries, even the current application. You can apply ThemeDictionaryExtension as the Source for a ResourceDictionary to affect everything under its scope. Here’s an example:

<Application ...>
<Application.Resources>
  <ResourceDictionary>
  <ResourceDictionary.MergedDictionaries>
    <ResourceDictionary .../>
    <ResourceDictionary Source="{ThemeDictionary MyApplication}"/>
  </ResourceDictionary.MergedDictionaries>
  </ResourceDictionary>
</Application.Resources>
</Application>

Imagine that you want to make the pie chart style for ProgressBar vary, based on the Windows theme. If the MyApplication assembly contains per-theme styles with a TargetType of {x:Type ProgressBar}, all ProgressBars in this application get the customized per-theme style by default, thanks to the use of ThemeDictionaryExtension.

Another approach for attaching per-theme styles to existing elements is to define a custom subclass. Creating custom controls is the subject of Chapter 20, but creating a custom control (or another element) solely for the purpose of giving it a theme style is pretty simple. For the per-theme pie chart style example, you could create a custom control called ProgressPie, as follows:

public class ProgressPie : ProgressBar
{
  static ProgressPie()
  {
    DefaultStyleKeyProperty.OverrideMetadata(
      typeof(ProgressPie),
      new FrameworkPropertyMetadata(typeof(ProgressPie)));
  }
}

Because ProgressPie derives from ProgressBar, it automatically has all the necessary functionality. But having a unique type gives you the ability to support a new theme style distinct from ProgressBar’s theme style. The only magic incantation is the single line of code in ProgressPie’s static constructor that sets the DefaultStyleKey dependency property. DefaultStyleKey is a protected dependency property on FrameworkElement and FrameworkContentElement that determines the key to use for its default style. (The terms default style and theme style are often used interchangeably.)

WPF’s built-in elements set this property to their own type, so their corresponding theme dictionaries use typed styles. If the preceding code didn’t set a DefaultStyleKey, ProgressPie would inherit the value from ProgressBar, which is typeof(ProgressBar). Therefore, ProgressPie makes typeof(ProgressPie) its DefaultStyleKey.

This book’s source code contains a Visual Studio project that contains the preceding definition of ProgressPie, the preceding usage of ThemeInfoAttribute, and a handful of theme dictionaries that get compiled into the application. Each theme dictionary is a standalone XAML file with the following structure:

<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:ThemedProgressPie">
  <Style TargetType="{x:Type local:ProgressPie}">
    ...
  </Style>
</ResourceDictionary>

Figure 14.16 displays a theme-styled ProgressPie under two different themes. Although you can dig into how each Style was created, the point is that theme styles give you the flexibility to completely change an element’s visuals when the theme changes. Unlike the visuals in Figure 14.15, I think Figure 14.16 succeeds in making the Windows 7 ProgressPie “glassy” and the Windows Classic ProgressPie old-fashioned. Kidding aside, making theme styles too different from each other will probably confuse your users more than help them.

Image

FIGURE 14.16 The same control with its theme style, viewed under two different themes.

Summary

The combination of styles, templates, skins, and themes is very powerful and often confusing to someone learning about WPF. Adding to the confusion is the fact that Styles can (and often do) contain templates, elements in templates all have Styles (whether marked explicitly or inherited implicitly), and theme styles are managed separately from normal Styles (so an element like Button’s Style property is null by default, even though it clearly has a theme style applied).

These mechanisms are so powerful, in fact, that often you can restyle an existing control as an alternative to writing your own custom control. This is great news, as restyling an existing control is significantly easier than writing a custom control, and it can perhaps be done entirely by a graphic designer rather than a programmer. If you do find that you need to write a custom control (the topic of Chapter 20), the lessons learned here about creating robust templates and adapting to themes are still very applicable.


Tip

You can play around with alternate skins for many WPF controls by downloading the WPF Themes .zip file from http://wpf.codeplex.com. These “themes” are what this chapter calls skins; they are just resource dictionaries that define new typed styles for most of WPF’s built-in controls. To use one of them, you can simply reference the resource dictionary as the Resources collection in your Application, Window, or elsewhere:

<Application ...>
<Application.Resources>
  <ResourceDictionary Source="BureauBlack.xaml"/>
</Application.Resources>
</Application>

Unfortunately, these skins don’t include styles for the controls in WPF 4.0 and later such as Ribbon, DataGrid, Calendar, and DatePicker. Figure 14.17 demonstrates the seven included skins applied to several controls.

Image

FIGURE 14.17 Applying skins from the “WPF Themes” download.


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

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