Avalon Skinning

[Note (05/19/2005): I see that I've broken the images below. I'll try to fix this tomorrow. Sorry about that!]

Yesterday I posted about Terrarium at PDC 2003. I mentioned that one of the features I liked best was the use of Xaml to provide skinning capabilities to the application. It is really easy to do, so tonight I’ll show how Terrarium did it.

 

First, let’s take a look at a couple of screen shots. They show the two different styles that were available at the Hands-On Lab. The first one was the default one, called “Toon”. The second was called “Xbox”. I know, I’m not the most creative when it comes to names. J

 

 

 

A couple of things to point out. All of the buttons for the application are just regular Avalon buttons that have had their Visual Tree replaced to provide custom drawn buttons completely in Xaml. The ability to due custom, or owner, drawn controls via mark-up is a really powerful feature of Avalon. Another thing to notice is that the message center area in the upper right is itself a button “at heart”. It is a button that includes a couple of Panels to help layout a Text and Image control. This is an example of compositing existing controls to create a new control. Again, all in Xaml. The last thing to notice, and it is a bit hard to see in these screenshots is that even the mini-map is styled, providing a custom “where am I” rectangle that fits in with the theme.

 

So how did we do this? Like I mention earlier, it was rather easy, much simpler than I thought it would be. There are 3 steps to enable skinning in an application such as Terrarium:

 

 

  • Create the Style sheets

The style sheets themselves are implemented as Avalon resources defined in Xaml files. You need to figure out exactly what areas of your application you want to style and create the various items you will use to give your application the look you desire. Common items are Brushes used for Backgrounds, the Font settings used for text, Colors, etc. You can also define custom drawn controls here by replacing the default Visual Tree. You also need to be sure to name your various resources so that they can be referenced later. Here is a portion of the tool_style.xaml file:

 

 

<DockPanel xmlns="https://schemas.microsoft.com/2003/xaml/" xmlns:def="Definition">

 

<DockPanel.Resources>

 

<!--

*** MAIN BAR ***

-->

<LinearGradientBrush def:Name="MainBackground" StartPoint="0,0" EndPoint="0,1">

<LinearGradientBrush.GradientStops>

<GradientStopCollection>

<GradientStop Offset="0.0" Color="Maroon" />

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

</GradientStopCollection>

</LinearGradientBrush.GradientStops>

</LinearGradientBrush>

 

<Style def:Name="MainBar">

<Canvas Background="{MainBackground}"/>

</Style>

<!--

*** BUTTON STYLES ***

-->

 

<LinearGradientBrush def:Name="MainButtonOver" StartPoint="0,0" EndPoint="0,1">

<LinearGradientBrush.GradientStops>

<GradientStopCollection>

<GradientStop Offset="0.0" Color="Orange" />

<GradientStop Offset="0.5" Color="Yellow" />

<GradientStop Offset="1.0" Color="Orange" />

</GradientStopCollection>

</LinearGradientBrush.GradientStops>

</LinearGradientBrush>

 

<LinearGradientBrush def:Name="MainButtonPressed" StartPoint="0,0" EndPoint="0,1">

<LinearGradientBrush.GradientStops>

<GradientStopCollection>

<GradientStop Offset="0.0" Color="LightBlue" />

<GradientStop Offset="1.0" Color="SteelBlue" />

</GradientStopCollection>

</LinearGradientBrush.GradientStops>

</LinearGradientBrush>

<Style def:Name="LargeButton">

<Button Foreground="Orange" FontWeight="Bold" Opacity="1.0"/>

<Style.VisualTree>

<Canvas Width="*Alias(Target=Width)" Height="*Alias(Target=Height)">

<Rectangle def:StyleID="MainRect" RadiusX="8" RadiusY="8"

Fill="White" Stroke="Black" StrokeThickness="4" Width="100%" Height="100%"/>

<FlowPanel Width="100%" Height="100%" VerticalAlignment="Center" HorizontalAlignment="Center">

<ContentPresenter ContentControl.Content="*Alias(Target=Content)" Margin="4,4,4,4"/>

</FlowPanel>

</Canvas>

</Style.VisualTree>

<Style.VisualTriggers>

<PropertyTrigger Property="IsMouseOver" Value="true">

<Set PropertyPath="Shape.Fill" Value="{MainButtonOver}" Target="MainRect"/>

<Set PropertyPath="Foreground" Value="Maroon"/>

</PropertyTrigger>

 

<PropertyTrigger Property="Pressed" Value="true">

<Set PropertyPath="Shape.Fill" Value="{MainButtonPressed}" Target="MainRect"/>

<Set PropertyPath="Foreground" Value="Yellow"/>

</PropertyTrigger>

</Style.VisualTriggers>

</Style>

</DockPanel.Resources>

</DockPanel>

 

 

 

  • Create your Application UI

The next step is to create your application’s main user interface. We used Xaml to define the main window and dialogs for Terrarium. As you are defining your controls, be sure to apply the Style attribute to them and specify which resource they should use. In some cases, you are simply supplying something like a Brush for the Background property rather than supplying a Style. Both of these are possible and can reference items defined in the Skin resource. Here is a portion of the MainWindow.xaml file:

 

 

<Window

xmlns="https://schemas.microsoft.com/2003/xaml"

xmlns:def="Definition"

xmlns:wfci="wfci"

xmlns:wfc="wfc"

xmlns:anim="anim"

xmlns:media="media"

xmlns:terr="terr"

def:Class="Terrarium.Avalon.MainWindowBase"

def:SubClass="Terrarium.Avalon.MainWindow"

def:CodeBehind="MainWindow.xaml.cs"

Text="Avalon" Visible="False"

Closing="MainWindow_Closing"

Closed="MainWindow_Closed"

MinWidth="320"

MinHeight="240">

 

<DockPanel ID="RootCanvas" Width="100%" Height="100%" Background="Silver">

<!--

TOP PANEL

-->

<DockPanel ID="TopPanel" Width="100%" Height="136" DockPanel.Dock="Top">

 

<FlowPanel Style="{MainBar}" Width="100%" Height="104" DockPanel.Dock="Top" HorizontalAlignment="Center" VerticalAlignment="Center">

<FlowPanel HorizontalAlignment="Center" VerticalAlignment="Center" Width="40%" Height="100%" ClipToBounds="True">

<Button ID="StatsButton" Style="{LargeButton}" ToolTip="View Local Statistics" Height="64" Width="64" IsEnabled="True" Margin="4">

<Image Source="resources/chart_32.png" />

</Button>

<Button ID="PropertiesButton" Style="{LargeButton}" ToolTip="View Organism Properties" Height="64" Width="64" IsEnabled="True" Margin="4">

<Image Source="resources/srch_32.png" />

</Button>

<Button Visibility="Visible" ID="TraceButton" Style="{LargeButton}" ToolTip="View Trace Data" Height="64" Width="64" IsEnabled="True" Margin="4">

<Image Source="resources/notep_32.png" />

</Button>

<Button ID="StyleButton" Style="{LargeButton}" ToolTip="Change Visual Style" Height="64" Width="64" IsEnabled="True" Margin="4">

<Image Source="resources/paint_32.png" />

</Button>

</FlowPanel>

</FlowPanel>

</DockPanel>

</Window>

 

 

 

  • Load and Apply the Styles

The last step is to implement the code that will load and apply the styles to the application. For Terrarium PDC, I wanted the ability to pick up changes to styles without having to restart the application, a very big advantage when first learning styles and Xaml. J I also wanted the ability to drop new style files in and have the application use it. Every time the “Change Style” button was clicked, the application would look for files that ended with _style.xaml and store the file names it finds. The meat of this operation is the ApplyStyle( string styleName ) function, specifically these 3 lines:

 

 

Stream stream = File.OpenRead(styleName);

 

FrameworkElement resourceElement = (FrameworkElement)Parser.LoadXml(stream);

 

resourceElement.Resources.Seal();

 

this.Application.Resources = resourceElement.Resources;

 

stream.Close();

 

 

Here is the complete code used to discover, cycle and load the styles for Terrarium PDC:

 

 

namespace Terrarium.Avalon

{

public partial class MainWindow : Terrarium.Avalon.MainWindowBase

{

protected string[] _styleList;

protected int _styleIndex = 0;

 

protected void DiscoverStyles()

{

_styleList = Directory.GetFiles( System.AppDomain.CurrentDomain.BaseDirectory, "*_style.xaml");

}

 

protected void StyleButton_Click(object sender, ClickEventArgs args)

{

// To detect new styles...

this.DiscoverStyles();

 

if ( this._styleList == null || this._styleList.Length == 0 )

return;

 

this._styleIndex++;

if ( this._styleIndex > this._styleList.Length - 1 )

this._styleIndex = 0;

this.ApplyStyle( this._styleIndex );

}

 

protected void ApplyStyle(string styleName)

{

try

{

if (styleName.IndexOf("_style.xaml") == -1)

{

styleName += "_style.xaml";

styleName = AppDomain.CurrentDomain.BaseDirectory + "\\" + styleName;

}

 

 

if (File.Exists(styleName) == false)

{

return;

}

 

Stream stream = File.OpenRead(styleName);

FrameworkElement resourceElement = (FrameworkElement)Parser.LoadXml(stream);

 

resourceElement.Resources.Seal();

 

this.Application.Resources = resourceElement.Resources;

 

stream.Close();

}

catch (Exception ex)

{

ErrorLog.LogHandledException(ex);

}

}

 

protected void ApplyStyle(int styleIndex)

{

try

{

this.ApplyStyle(this._styleList[styleIndex]);

}

catch { }

}

 

protected void ApplyStyle()

{

this.ApplyStyle(this._styleIndex);

}

 

}

}

 

 

 

And that’s it! See, it really is easy to implement skinning in your Avalon based applications. I can’t wait to get started on “Terrarium Next” to really see what is possible using Xaml and Avalon for creating rich, customizable UIs.

 

[DISCLAIMER: This code and functionality was built using the PDC 2003 build of Windows “Longhorn”. This is all subject to change. ]