Windows Presentation Foundation 中控件开发的模板威力






4.83/5 (4投票s)
演示了WPF中的UI建模,利用模板。
引言
WPF是一个非常强大的框架,它在定义UI的视觉特性方面具有极大的灵活性。WPF的视觉设计方面允许开发者和设计者协作(并分离关注点)。在本教程中,我将以一副纸牌游戏的实际例子为例,说明如何在WPF中建模一副扑克牌,并定义模板以根据牌的花色和数值来定义牌的视觉外观。
评估开发选项
我想建模一副扑克牌,使其具有高度的可重用性和主题化,以便其他开发人员和设计师可以轻松使用。基本而言,一副牌必须包含其数值和类型,并且是可选的。WPF提供了几种开发选项供我选择
- 采用现有的WPF控件并为其设置主题以表示扑克牌,并使用附加属性设置其数值和牌面类型
- 定义一个自定义控件并编写所有上述功能,并为其设置主题以表示扑克牌
- 结合以上两种方法 - 从现有的WPF控件派生,并为其定义模板和附加属性
第一种方法不允许我在现有控件上声明可发现的牌面属性。虽然附加属性很棒,但它们有点难发现。牌面属性非常显眼且常用,所以我选择了一个更开箱即用、自包含的控件。
第二种方法需要我从头开始开发一个自定义控件。因此,这种方法也被排除了。
我更喜欢第三种方法,即我可以利用现有WPF控件的某些功能,并在其之上定义我自己的自定义属性,以提供开箱即用的体验。由于这将是一个自定义控件,我可以为此控件定义一个开箱即用的默认主题。由于一副扑克牌可以被选中/取消选中,我选择从提供选择功能的ToggleButton
控件派生。
模板支持
扑克牌支持多种主题,具体取决于牌的类型和数值。此信息包含在CardTemplate
类中,其中一系列CardTemplate
对象被定义为CardTemplateCollection
。
public sealed class CardTemplate
{
public ControlTemplate Template { get; set; }
public CardValue Value { get; set; }
}
public sealed class CardTemplateCollection : Collection<CardTemplate>
{
}
从Collection<T>
派生允许本机XAML序列化,并且我们可以轻松地将对象包含在集合标签中。
PlayingCard
类包含CardTemplates
依赖属性,类型为CardTemplateCollection
。
private static DependencyProperty CardTemplatesProperty =
DependencyProperty.Register( "CardTemplates",
typeof(CardTemplateCollection),
typeof(PlayingCard),
new PropertyMetadata(new CardTemplateCollection(),
UpdateTemplate));
注意CardTemplateCollection
是如何设置为默认属性值的。这是因为每当我们从Collection<T>
派生并利用XAML序列化功能时,我们需要一个有效的对象才能在XAML中为其赋值。
PlayingCard
还包含一个UpdateTemplate
方法,该方法根据牌的数值选择正确的模板。
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;
}
以下是PlayingCard
类的完整列表
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();
}
}
请注意,每当CardValue
或CardTemplates
属性更改时,我们都会调用UpdateTemplate
方法。
下一步是定义和填充PlayingCard
控件默认主题中的所有模板。但在我们开始之前,我们需要两件视觉信息才能正确地为牌设置主题
- 牌的花色 - 根据牌的类型,我们需要正确的符号出现在模板中。这通过
CardTypeToImageConverter
实现。基本上,我们使用一个类型为CardImageTypeCollection
的属性来定义此转换器,该属性包含类型为CardImageType
的对象。CardImageType
仅包含CardType
枚举(表示牌的类型:梅花、方块、红心、黑桃)以及相关图像的字符串URI。
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();
}
}
可以看到,此转换器以PlayingCard
作为输入,并通过搜索其ImageCollection
来返回符号图像的URI。
PlayingCard
的数值为Ace、King、Queen和Jack,我们需要在模板中访问正确的角色图像。这如下委托给CardTypeToCharacterImageConverter
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;
}
}
}
因此,根据角色牌的类型和数值,此转换器会返回牌的正确图像URI。
现在我们拥有了定义PlayingCard
控件模板所需的一切。
首先,我们在主题将使用的资源字典中定义共享转换器CardTypeToImageConverter
<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>
然后,我们定义共享转换器CardTypeToCharacterImageConverter
<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>
我们还需要在牌的下半部分垂直翻转牌的花色,因此我们将其捕获在一个名为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>
太好了!现在我们拥有了可以用于牌模板的所有工具。因此,让我们定义每个可能的牌数值和角色牌的模板,从角色牌模板开始
<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>
您可以看到,在模板中使用的Image
的Source
属性中,我们使用CardTypeToCharacterImageConverter
(如上所述声明为共享转换器),并将模板父项(PlayingCard
)作为相对源传递。
现在,对于每个数值,我们声明模板。我不会在此处涵盖完整的模板列表(您可以在源代码“主题”文件夹的Generic.xaml中查看),但我将提及数值为“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>
如您所见,我们使用了CardTypeToImageConverter
来获取花色图像,并使用VerticalFlipStyle
垂直翻转底部符号,以获得类似于下图1的牌。
最后,我们像下面一样声明PlayingCard
的默认样式和模板
<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>
请注意,所有模板都作为CardTemplate
值添加。UpdateTemplate
方法将根据牌的数值分配正确的模板。我们还使用触发器定义PlayingCard
的选中视觉状态,并渲染渐变背景。我们还使用触发器来定义钻石或红心牌的前景色为红色。这就是让我们的PlayingCard
正常工作所需的一切。为了演示,我构建了一个完整的纸牌牌组,并将其渲染成一个圆形面板(功劳:我从Microsoft Expression Blend附带的颜色样本中获取了这个面板)。此外,我通过设置Z-Index将选中的牌带到前面。
以下是渲染纸牌牌组的MainWindow
的代码隐藏
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);
}
}
private PlayingCard _selectedCard;
}
最后的话
我希望我能够通过这个例子提供一些关于UI建模的力量和简单性的见解。WPF是一个非常丰富且灵活的架构,它允许我们通过模板和样式进行奇妙的操作。同时它也可以很有趣,并且是一种致命的上瘾!如果您有任何意见或建议,请随时留言。我也有我的博客,网址是http://akaila.serveblog.net和http://ashishkaila.blogspot.com。谢谢!