Background
WPF is very powerful framework in terms of flexibility in defining visual characteristics of the UI. The visual designing aspect of WPF allows for developer – designer collaboration (and separation of concerns). In this tutorial I will take a practical example of a card game and illustrate how to model a playing card in WPF and define templates for defining visual aspect of the card as per the sign and the value of the card.
Source Code
Complete source code of the article and be found here
Evaluating development options
I wanted to model a playing card so it is highly reusable and theme-able so other developers and designers can consume it easily. At a basic level a card must contain its value and type and be selectable. There were several development options available in WPF I could opt for:
- Take an existing WPF control and theme it to represent a card and use attached properties to set its value and card type
- Define a custom control and code in all the above functionality and define its theme to represent a card
- Combine the above two methods - derive from an existing WPF control and define templates and additional properties on it
First approach does not allow me to declare discoverable properties of a card on existing control. While attached properties are great, they are bit hard to discover. Card’s properties are very prominent and frequently used, so I opted for a more out of the box – self contained control.
Second approach requires me to develop a custom control from scratch. So this approach is ruled out as well.
I prefer the third approach where I can leverage some capabilities of an existing WPF control and define my own custom properties on top to provide an out of box experience. And since this will be a custom control, I can define out of the box – default theme for this control. Since a playing card can be selected – deselected I choose to derive from ToggleButton control that provides with selection capabilities.
Template support
Playing card supports multiple theme depending on the type and value of the card. This information is captured within CardTemplate class a collection of which is defined as CardTemplateCollection.
public sealed class CardTemplate
{
public ControlTemplate Template { get; set; }
public CardValue Value { get; set; }
}
public sealed class CardTemplateCollection : Collection<CardTemplate>
{
}
Deriving from Collection<T> allows for native XAML serialization and one can enclose objects easily within collection tag.
PlayingCard class contains CardTemplateCollection as its CardTemplates dependency property.
private static DependencyProperty CardTemplatesProperty = DependencyProperty.Register(
"CardTemplates",
typeof(CardTemplateCollection),
typeof(PlayingCard),
new PropertyMetadata(new CardTemplateCollection(), UpdateTemplate));
Notice how a new CardTemplateCollection is set as a default property value. This is so because whenever we derive from Collection<T> and leverage XAML serialization capability, we need a valid object before it is assigned in XAML.
PlayingCard also contains a method UpdateTemplate which picks the right template for the card depending upon card value.
private void UpdateTemplate()
{
CardTemplateCollection templates = CardTemplates;
ControlTemplate selectedTemplate = null;
if (templates != null)
{
foreach (CardTemplate template in templates)
{
if (template.Value == CardValue)
{
selectedTemplate = template.Template;
break;
}
}
}
Template = selectedTemplate;
}
Following is the complete listing of PlayingCard class:
public class PlayingCard : ToggleButton
{
private static DependencyProperty CardTypeProperty = DependencyProperty.Register("CardType",
typeof(CardType),
typeof(PlayingCard),
new PropertyMetadata(CardType.Club));
private static DependencyProperty CardValueProperty = DependencyProperty.Register("CardValue",
typeof(CardValue),
typeof(PlayingCard),
new PropertyMetadata(CardValue.Two, UpdateTemplate));
private static DependencyProperty CardTemplatesProperty = DependencyProperty.Register("CardTemplates",
typeof(CardTemplateCollection),
typeof(PlayingCard),
new PropertyMetadata(new CardTemplateCollection(), UpdateTemplate));
static PlayingCard()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(PlayingCard), new FrameworkPropertyMetadata(typeof(PlayingCard)));
}
public CardType CardType
{
get { return (CardType)GetValue(CardTypeProperty); }
set { SetValue(CardTypeProperty, value); }
}
public CardValue CardValue
{
get { return (CardValue)GetValue(CardValueProperty); }
set { SetValue(CardValueProperty, value); }
}
public CardTemplateCollection CardTemplates
{
get { return (CardTemplateCollection)GetValue(CardTemplatesProperty); }
set { SetValue(CardTemplatesProperty, value); }
}
private void UpdateTemplate()
{
CardTemplateCollection templates = CardTemplates;
ControlTemplate selectedTemplate = null;
if (templates != null)
{
foreach (CardTemplate template in templates)
{
if (template.Value == CardValue)
{
selectedTemplate = template.Template;
break;
}
}
}
Template = selectedTemplate;
}
private static void UpdateTemplate(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
PlayingCard card = d as PlayingCard;
card.UpdateTemplate();
}
}
Notice we call UpdateTemplate method whenever CardValue or CardTemplates properties are changed.
Next step is to define and populate all the templates within the default theme of the PlayingCard control. But before we embark on that there are two pieces of visual information we would need in order to theme the card right:
- The sign of the card – Depending on the type of the card we need the right symbol to appear in the template. This is achieved by CardTypeToImageConverter. Basically we define this converter with a property of type CardImageTypeCollection which contains objects of type CardImageType. CardImageType just contains CardType enum (that denotes type of card: Club, Diamond, Heart, Spade) and string uri of related image
public sealed class CardTypeImage
{
public CardType CardType { get; set; }
public string ImageSource { get; set; }
}
public sealed class CardTypeImageCollection : Collection<CardTypeImage>
{
}
public sealed class CardTypeToImageConverter : IValueConverter
{
public CardTypeToImageConverter()
{
ImageCollection = new CardTypeImageCollection();
}public CardTypeImageCollection ImageCollection { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
PlayingCard card = value as PlayingCard;
if ((card != null) && (ImageCollection != null))
{
foreach (CardTypeImage cardTypeImage in ImageCollection)
{
if (card.CardType == cardTypeImage.CardType)
{
return cardTypeImage.ImageSource;
}
}
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
As can be seen this converter takes in PlayingCard as input and returns the uri of the symbol image from searching its ImageCollection.
- Image of the character if card represents a character – If PlayingCard is of value Ace, King, Queen and Jack we need access to the right character image in the template. This is delegated to CardTypeToCharacterImageConverter as follows:
public sealed class CardCharacterImage
{
public CardType CardType { get; set; }
public CardValue CardValue { get; set; }
public string ImageSource { get; set; }
}public sealed class CardCharacterImageCollection : Collection<CardCharacterImage>
{
}public sealed class CardTypeToCharacterImageConverter : IValueConverter
{
public CardTypeToCharacterImageConverter()
{
ImageCollection = new CardCharacterImageCollection();
}public CardCharacterImageCollection ImageCollection { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
PlayingCard card = value as PlayingCard;
if ((card != null) && (IsCharacterCard(card)))
{
foreach (CardCharacterImage cardCharacterImage in ImageCollection)
{
if ((card.CardValue == cardCharacterImage.CardValue) &&
(card.CardType == cardCharacterImage.CardType))
{
return cardCharacterImage.ImageSource;
}
}
}
return null;
}public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}private bool IsCharacterCard(PlayingCard card)
{
switch (card.CardValue)
{
case CardValue.Two:
case CardValue.Three:
case CardValue.Four:
case CardValue.Five:
case CardValue.Six:
case CardValue.Seven:
case CardValue.Eight:
case CardValue.Nine:
case CardValue.Ten:
return false;
break;
default:
return true;
break;
}
}
}Hence depending on the type and value of the character card, this converter returns the right image uri for the card.
Now we have all we need to define our templates for the PlayingCard control.
First let’s define CardTypeToImageConverter shared converter in resource dictionary that themes will use:
<Converters:CardTypeToImageConverter x:Key="CardTypeToImageConverter">
<Converters:CardTypeToImageConverter.ImageCollection>
<Converters:CardTypeImageCollection>
<Converters:CardTypeImage CardType="Club"
ImageSource="/CardDeckSample;component/Resources/Club.png"/>
<Converters:CardTypeImage CardType="Diamond"
ImageSource="/CardDeckSample;component/Resources/Diamond.png"/>
<Converters:CardTypeImage CardType="Heart"
ImageSource="/CardDeckSample;component/Resources/Heart.png"/>
<Converters:CardTypeImage CardType="Spade"
ImageSource="/CardDeckSample;component/Resources/Spade.png"/>
</Converters:CardTypeImageCollection>
</Converters:CardTypeToImageConverter.ImageCollection>
</Converters:CardTypeToImageConverter>
Then let’s define CardTypeToCharacterImageConverter shared converter:
<Converters:CardTypeToCharacterImageConverter x:Key="CardTypeToCharacterImageConverter">
<Converters:CardTypeToCharacterImageConverter.ImageCollection>
<Converters:CardCharacterImage
CardType="Club"
CardValue="Ace"
ImageSource="/CardDeckSample;component/Resources/AceClub.png"/>
<Converters:CardCharacterImage
CardType="Diamond"
CardValue="Ace"
ImageSource="/CardDeckSample;component/Resources/AceDiamond.png"/>
<Converters:CardCharacterImage
CardType="Heart"
CardValue="Ace"
ImageSource="/CardDeckSample;component/Resources/AceHeart.png"/>
<Converters:CardCharacterImage
CardType="Spade"
CardValue="Ace"
ImageSource="/CardDeckSample;component/Resources/AceSpade.png"/>
<Converters:CardCharacterImage
CardType="Club"
CardValue="Jack"
ImageSource="/CardDeckSample;component/Resources/JackClub.png"/>
<Converters:CardCharacterImage
CardType="Diamond"
CardValue="Jack"
ImageSource="/CardDeckSample;component/Resources/JackDiamond.png"/>
<Converters:CardCharacterImage
CardType="Heart"
CardValue="Jack"
ImageSource="/CardDeckSample;component/Resources/JackHeart.png"/>
<Converters:CardCharacterImage
CardType="Spade"
CardValue="Jack"
ImageSource="/CardDeckSample;component/Resources/JackSpade.png"/>
<Converters:CardCharacterImage
CardType="Club"
CardValue="King"
ImageSource="/CardDeckSample;component/Resources/KingClub.png"/>
<Converters:CardCharacterImage
CardType="Diamond"
CardValue="King"
ImageSource="/CardDeckSample;component/Resources/KingDiamond.png"/>
<Converters:CardCharacterImage
CardType="Heart"
CardValue="King"
ImageSource="/CardDeckSample;component/Resources/KingHeart.png"/>
<Converters:CardCharacterImage
CardType="Spade"
CardValue="King"
ImageSource="/CardDeckSample;component/Resources/KingSpade.png"/>
<Converters:CardCharacterImage
CardType="Club"
CardValue="Queen"
ImageSource="/CardDeckSample;component/Resources/QueenClub.png"/>
<Converters:CardCharacterImage
CardType="Diamond"
CardValue="Queen"
ImageSource="/CardDeckSample;component/Resources/QueenDiamond.png"/>
<Converters:CardCharacterImage
CardType="Heart"
CardValue="Queen"
ImageSource="/CardDeckSample;component/Resources/QueenHeart.png"/>
<Converters:CardCharacterImage
CardType="Spade"
CardValue="Queen"
ImageSource="/CardDeckSample;component/Resources/QueenSpade.png"/>
</Converters:CardTypeToCharacterImageConverter.ImageCollection>
</Converters:CardTypeToCharacterImageConverter>
We also need to flip card signs in the bottom half of cards vertically so we capture this in a common style called VerticalFlipStyle:
<Style TargetType="{x:Type FrameworkElement}" x:Key="VerticalFlipStyle">
<Setter Property="RenderTransform">
<Setter.Value>
<TransformGroup>
<RotateTransform Angle="180"/>
</TransformGroup>
</Setter.Value>
</Setter>
<Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
</Style>
Great now we have all the tools that we can make use of in our card templates. So let’s define templates for each of the possible card values and character cards starting with character card template:
<ControlTemplate TargetType="{x:Type local:PlayingCard}"
x:Key="CharacterCardTemplate">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<Image Source="{Binding Converter={StaticResource CardTypeToCharacterImageConverter}, RelativeSource={RelativeSource TemplatedParent}}"/>
</Border>
</ControlTemplate>
You can see in Source attribute of Image used in template we use CardTypeToCharacterImageConverter (declared as shared converter above) and pass in templated parent (PlayingCard) as relative source.
Now for each of the values we declare templates. I will not cover entire template list here (you can check them out in Generic.xaml in Themes folder of source code) but I will mention template for a card of value “2”
<ControlTemplate TargetType="{x:Type local:PlayingCard}"
x:Key="TwoTemplate">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<Grid Margin="4">
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="32"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="2"
FontSize="26.667"/>
<Image Source="{Binding Converter={StaticResource CardTypeToImageConverter}, RelativeSource={RelativeSource TemplatedParent}}"
Grid.Row="1"
Grid.Column="0"/>
<Image Source="{Binding Converter={StaticResource CardTypeToImageConverter}, RelativeSource={RelativeSource TemplatedParent}}"
Grid.Row="3"
Grid.Column="2"
Style="{StaticResource VerticalFlipStyle}"/>
<TextBlock Grid.Row="4"
Grid.Column="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="2"
FontSize="26.667"
Style="{StaticResource VerticalFlipStyle}"/>
<Grid Grid.Row="2"
Grid.Column="1"
Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Image Source="{Binding Converter={StaticResource CardTypeToImageConverter}, RelativeSource={RelativeSource TemplatedParent}}"
Grid.Row="0"
Height="32"
Width="32"/>
<Image Source="{Binding Converter={StaticResource CardTypeToImageConverter}, RelativeSource={RelativeSource TemplatedParent}}"
Grid.Row="1"
Height="32"
Width="32"
Style="{StaticResource VerticalFlipStyle}"/>
</Grid>
</Grid>
</Border>
</ControlTemplate>
As you can see we have used both CardTypeToImageConverter to obtain sign image and used VerticalFlipStyle to flip bottom symbols vertically to get a card similar to Figure 1 below.
Figure 1 – Templated spade card of value 2
Finally we declare default style and template of PlayingCard as below:
<Style TargetType="{x:Type local:PlayingCard}">
<Setter Property="Width" Value="300"/>
<Setter Property="Height" Value="450"/>
<Setter Property="BorderBrush" Value="Gray"/>
<Setter Property="Background" Value="White"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CardTemplates">
<Setter.Value>
<local:CardTemplateCollection>
<local:CardTemplate Template="{StaticResource CharacterCardTemplate}" Value="Ace"/>
<local:CardTemplate Template="{StaticResource CharacterCardTemplate}" Value="King"/>
<local:CardTemplate Template="{StaticResource CharacterCardTemplate}" Value="Queen"/>
<local:CardTemplate Template="{StaticResource CharacterCardTemplate}" Value="Jack"/>
<local:CardTemplate Template="{StaticResource TwoTemplate}" Value="Two"/>
<local:CardTemplate Template="{StaticResource ThreeTemplate}" Value="Three"/>
<local:CardTemplate Template="{StaticResource FourTemplate}" Value="Four"/>
<local:CardTemplate Template="{StaticResource FiveTemplate}" Value="Five"/>
<local:CardTemplate Template="{StaticResource SixTemplate}" Value="Six"/>
<local:CardTemplate Template="{StaticResource SevenTemplate}" Value="Seven"/>
<local:CardTemplate Template="{StaticResource EightTemplate}" Value="Eight"/>
<local:CardTemplate Template="{StaticResource NineTemplate}" Value="Nine"/>
<local:CardTemplate Template="{StaticResource TenTemplate}" Value="Ten"/>
</local:CardTemplateCollection>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="BorderBrush" Value="#FF0C1A89"/>
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush EndPoint="0.5,1" MappingMode="RelativeToBoundingBox" StartPoint="0.5,0">
<GradientStop Color="#FFF1EDED" Offset="0"/>
<GradientStop Color="White" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="CardType" Value="Diamond">
<Setter Property="Foreground" Value="#FFd40000"/>
</Trigger>
<Trigger Property="CardType" Value="Heart">
<Setter Property="Foreground" Value="#FFd40000"/>
</Trigger>
</Style.Triggers>
</Style>
Notice all the templates added as CardTemplates values. UpdateTemplate method will assign the right template based on the card value. We also define selected visual state of the PlayingCard using triggers and render a gradient background. We also use triggers to define foreground color of red if card is of type Diamond or Heart. That is all we need to get our PlayingCard working. For demonstration purpose I build a complete deck of cards as a card fan and render it in a circular panel (credit: I took this panel from color swatch sample that ships with Microsoft Expression Blend). Also I bring selected card to the front by setting its Z-Index.
Figure 2 – Complete card deck with 5 of diamond selected (notice selection background gradient)
Following is the code behind for MainWindow that renders the card deck.
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
foreach (CardType type in Enum.GetValues(typeof(CardType)))
{
foreach (CardValue value in Enum.GetValues(typeof(CardValue)))
{
PlayingCard card = new PlayingCard();
card.CardType = type;
card.CardValue = value;
Deck.Children.Add(card);
}
}
Deck.AddHandler(ToggleButton.CheckedEvent, new RoutedEventHandler(OnCardSelected));
}
private void OnCardSelected(object sender, RoutedEventArgs args)
{
if (_selectedCard != null)
{
_selectedCard.IsChecked = false;
Canvas.SetZIndex(_selectedCard, 0);
}
_selectedCard = args.OriginalSource as PlayingCard;
if (_selectedCard != null)
{
_selectedCard.IsChecked = true;
Canvas.SetZIndex(_selectedCard, 1);
}
}
PlayingCard _selectedCard;
}
Last Words
I hope I was able to provide some insight into power and simplicity of modeling UI from this example. WPF is very rich and flexible architecture that allows one to perform wonders with things like templates and styles. It could also be fun at the same time and so a very fatal addiction! If you have any comments or suggestions please do not hesitate to leave a note. Thanks !
No comments:
Post a Comment