Saturday, November 28, 2009

WPF Skinning

I was planning to learn about WPF skinning for this week. Here we go.

image image

Create 3 sets of “skin” – WPF resource dictionaries:

Default:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   
    <LinearGradientBrush x:Key="bgbrush">
        <GradientStop Color="White" Offset="0" />
        <GradientStop Color="Blue" Offset="1" />
    </LinearGradientBrush>
   
</ResourceDictionary>

Green:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   
    <LinearGradientBrush x:Key="bgbrush">
        <GradientStop Color="White" Offset="0" />
        <GradientStop Color="Green" Offset="1" />
    </LinearGradientBrush>
   
</ResourceDictionary>

Red:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   
    <LinearGradientBrush x:Key="bgbrush">
        <GradientStop Color="Cyan" Offset="0" />
        <GradientStop Color="Red" Offset="1" />
    </LinearGradientBrush>
   
</ResourceDictionary>

In App.xaml, use the default skin:

<Application.Resources>
   
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Skins\Default.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
    
</Application.Resources>

I want to use a menu to switch the skins, and put a check mark at the current skin. This sounds like a property binding. So create a dependency property on the main window class and perform the skin switching when property changed:

public static readonly DependencyProperty SkinProperty = DependencyProperty.Register(
    "Skin", typeof(string), typeof(Window1), new PropertyMetadata("Default", OnSkinChanged));

public string Skin
{
    get { return (string)GetValue(SkinProperty); }
    set { SetValue(SkinProperty, value); }
}

private static void OnSkinChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    string skin = e.NewValue as string;
    ResourceDictionary res = (ResourceDictionary)Application.LoadComponent(
        new Uri(string.Format(@"Skins\{0}.xaml", skin), UriKind.Relative));
    Application.Current.Resources = res;
}

I want a menu item to be checked if the current Skin matches the menu item. Looks like I need a value converter check Equals… I can’t find an existing one. On the other hand, when a menu item becomes checked, I want to update the Skin property. I end up creating a value converter:

public class SkinEqualsConverter : IValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter,
        CultureInfo culture)
    {
        return object.Equals(value, parameter);
    }

    public object ConvertBack(object value, Type targetType, object parameter,
        CultureInfo culture)
    {
        return (bool)value ? parameter : DependencyProperty.UnsetValue;
    }

    #endregion
}

Now if a menu item is clicked, we only need to set its IsChecked to true and let data binding update the Skin. Setting MenuItem.IsCheckable to true almost satisfies my requirements. When clicked and IsChecked is turned on, everything works. However, I can’t prevent a user to click it again and uncheck the menu item. In that case Skin is not changed, but the check mark is gone. Finally I decide to handle the click manually:

private void SkinItem_Click(object sender, RoutedEventArgs e)
{
    MenuItem menu = (MenuItem)sender;
    menu.IsChecked = true;
}

The main window xaml is like the following. When a menu item is clicked, the above handler would turn on IsChecked, the binding will invoke the converter to get the parameter skin name, and bind that name to the above Skin property, causing the skin switch. Other menu items will uncheck because the binding invokes the converter and check if the current Skin equals to the converter parameter skin. In the UI I use DynamicResource to refer to skin resources so that UI refreshes dynamically when skin resources are replaced at runtime.

<Window x:Class="SkinApp.Window1"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="clr-namespace:SkinApp"
      x:Name="mainWindow"       
      Title="Window1" Height="300" Width="300">

    <Window.Resources>
        <local:SkinEqualsConverter x:Key="skinEquals" />
    </Window.Resources>

    <DockPanel x:Name="framePanel" LastChildFill="True">
        <Menu IsMainMenu="True" DockPanel.Dock="Top">
            <MenuItem Header="_Skins">
                <MenuItem Header="_Default" Click="SkinItem_Click"
                        IsChecked="{Binding ElementName=mainWindow, Path=Skin,
                              Converter={StaticResource skinEquals},
                              ConverterParameter=Default}"/>
                <MenuItem Header="_Green" Click="SkinItem_Click"
                        IsChecked="{Binding ElementName=mainWindow, Path=Skin,
                              Converter={StaticResource skinEquals},
                              ConverterParameter=Green}"/>
                <MenuItem Header="_Red" Click="SkinItem_Click"
                        IsChecked="{Binding ElementName=mainWindow, Path=Skin,
                              Converter={StaticResource skinEquals},
                              ConverterParameter=Red}"/>
            </MenuItem>
        </Menu>

        <Grid Background="{DynamicResource bgbrush}">
            <TextBlock
              Text="{Binding ElementName=mainWindow, Path=Skin}"
              HorizontalAlignment="Center"
              VerticalAlignment="Center"/>
        </Grid>
    </DockPanel>

</Window>

No comments: