65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (4投票s)

2010年8月2日

CPOL

6分钟阅读

viewsIcon

24115

downloadIcon

339

演示了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();
    }
}

请注意,每当CardValueCardTemplates属性更改时,我们都会调用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>

您可以看到,在模板中使用的ImageSource属性中,我们使用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的牌。

图1 - 数值为2的模板化黑桃牌

最后,我们像下面一样声明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将选中的牌带到前面。

图2 - 完整的纸牌牌组,选中了钻石5(注意选中背景渐变)

以下是渲染纸牌牌组的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.nethttp://ashishkaila.blogspot.com。谢谢!

© . All rights reserved.