プログラミング Windows 第6版 第11章 WPF編

この記事では、「プログラミング Windows 第6版」を使って WPF XAML の学習を支援することを目的にしています。この目的から、書籍と併せて読まれることをお勧めします。

第11章 3つのテンプレート

本章では、タイトルにあるように 3種類のテンプレート(コントロール テンプレート、データ テンプレート、アイテム テンプレート)を説明しています。データ テンプレートは、データバインドを行う場合に活用されるもので、ContentControl を継承するクラスで使用されます。コントロール テンプレートは、コントロールの外観を再定義するために使用するもので、マウスなどの動きに応じてコントロールの外観を変化させるアニメーションの定義なども含まれています。最後のアイテム テンプレートは、コレクションをデータ バインドした時に個々のデータに適用されるテンプレートになります。たとえば、リスト ボックスなどのアイテムに適用されるのがアイテム テンプレートです。書籍にも記述がありますが、これらのテンプレートを作成したりカスタマイズするのであれば、Blend for Visual Studio の操作に慣れておくことをお勧めします。その理由は、Blend ではテンプレートをビジュアルに編集することが容易だからです。テンプレートの編集では、Visual Studio のデザイナーは Blend よりも非力になります。

11.1(P494) ボタンのデータ

本節では、Button コントロールの Content プロパティを使ってどのように外観を定義できるかを具体例を使って説明しています。最初に Image オブジェクトを使い、次に Ellipse と LinearGradientBrush を組み合わせてから、テンプレートをリソースに定義することを説明しています。ここまでの説明をしてから、Button に対するスタイル定義を説明するために、SharedStyleWithDataTemplate プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <Style TargetType="Button">
            <Setter Property="HorizontalAlignment" Value="Center" />
            <Setter Property="VerticalAlignment" Value="Center" />
            <Setter Property="ContentTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <Ellipse Width="144"
                                 Height="192"
                                 Fill="{Binding}" />
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Button Grid.Column="0">
            <SolidColorBrush Color="Green" />
        </Button>
        <Button Grid.Column="1">
            <LinearGradientBrush>
                <GradientStop Offset="0" Color="Blue" />
                <GradientStop Offset="1" Color="Red" />
            </LinearGradientBrush>
        </Button>
        <Button Grid.Column="2">
            <ImageBrush ImageSource="https://www.charlespetzold.com/pw6/PetzoldJersey.jpg" />
        </Button>
    </Grid>
</Window>

この XAML は、組み込みのスタイルを除けば WinRT XAML と同じになります。それでは、実行結果を示します。
SharedStyleWithDataTemplate

今度は、時計を表示するボタンの外観を作成するために ClockButton プロジェクトの Clock.cs を示します。

 using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Media;

namespace ClockButton
{
    public class Clock : INotifyPropertyChanged
    {
        bool isEnabled;
        int hour, minute, second;
        int hourAngle, minuteAngle, secondAngle;

        public event PropertyChangedEventHandler PropertyChanged;

        public bool IsEnabled
        {
            set
            {
                if (SetProperty<bool>(ref isEnabled, value, "IsEnabled"))
                {
                    if (isEnabled)
                        CompositionTarget.Rendering += OnCompositionTargetRendering;
                    else
                        CompositionTarget.Rendering -= OnCompositionTargetRendering;
                }
            }
            get
            {
                return isEnabled;
            }
        }

        public int Hour
        {
            set { SetProperty<int>(ref hour, value); }
            get { return hour; }
        }

        public int Minute
        {
            set { SetProperty<int>(ref minute, value); }
            get { return minute; }
        }

        public int Second
        {
            set { SetProperty<int>(ref second, value); }
            get { return second; }
        }

        public int HourAngle
        {
            set { SetProperty<int>(ref hourAngle, value); }
            get { return hourAngle; }
        }

        public int MinuteAngle
        {
            set { SetProperty<int>(ref minuteAngle, value); }
            get { return minuteAngle; }
        }

        public int SecondAngle
        {
            set { SetProperty<int>(ref secondAngle, value); }
            get { return secondAngle; }
        }

        void OnCompositionTargetRendering(object sender, object args)
        {
            DateTime dateTime = DateTime.Now;
            this.Hour = dateTime.Hour;
            this.Minute = dateTime.Minute;
            this.Second = dateTime.Second;
            this.HourAngle = 30 * dateTime.Hour + dateTime.Minute / 2;
            this.MinuteAngle = 6 * dateTime.Minute + dateTime.Second / 10;
            this.SecondAngle = 6 * dateTime.Second + dateTime.Millisecond / 166;        }

        protected bool SetProperty<T>(ref T storage, T value,
                                      [CallerMemberName] string propertyName = null)
        {
            if (object.Equals(storage, value))
                return false;
            storage = value;
            OnPropertyChanged(propertyName);
            return true;
        }

        protected virtual void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

このコードは、WinRT XAML と同じになります。そして、作成した Clock クラスをデータソースとして使用する ClockButton プロジェクトの MainWindow.xaml の抜粋を示します。

 <Grid>
    <Button HorizontalAlignment="Center"
            VerticalAlignment="Center">

        <local:Clock IsEnabled="True" />

        <Button.ContentTemplate>
            <DataTemplate>
                <Grid Width="144" Height="144">
                    <Grid.Resources>
                        <Style TargetType="Polyline">
                            <Setter Property="Stroke"
                                    Value="Black" />
                        </Style>
                    </Grid.Resources>
                    <Polyline Points="72 80, 72 24"
                              StrokeThickness="6">
                        <Polyline.RenderTransform>
                            <RotateTransform Angle="{Binding HourAngle}"
                                             CenterX="72"
                                             CenterY="72" />
                        </Polyline.RenderTransform>
                    </Polyline>
                    <Polyline Points="72 88, 72 12"
                              StrokeThickness="3">
                        <Polyline.RenderTransform>
                            <RotateTransform Angle="{Binding MinuteAngle}"
                                             CenterX="72"
                                             CenterY="72" />
                        </Polyline.RenderTransform>
                    </Polyline>
                    <Polyline Points="72 88, 72 6"
                              StrokeThickness="1">
                        <Polyline.RenderTransform>
                            <RotateTransform Angle="{Binding SecondAngle}"
                                             CenterX="72"
                                             CenterY="72" />
                        </Polyline.RenderTransform>
                    </Polyline>
                </Grid>
            </DataTemplate>
        </Button.ContentTemplate>
    </Button>
</Grid>

この XAML は、組み込みのスタイルを除けば WinRT XAML と同じになります。それでは、実行結果を示します。
ClockButton

書籍には、ボタンのしての動き(たとえば、クリックした場合)などの説明がありますし、外観をどのように変更したかなどの説明もあります。それでも、書籍と併せてここまでを読むだけでも、Button コントロールでも様々な外観にテンプレートを使うことでカスタマイズができることを理解できることでしょう。このカスタマイズ性は、XAML 系 UI 技術の特徴でもあり、Windows Forms には無いものになります。

11.2(P504) 意志決定

本節では、XAML の記述だけでは条件分岐などができないことから、11.1 の ClockButton プロジェクトの Clock クラスを利用して拡張することで条件によって外観を変更することを説明しています。それでは、ConditionalClockButton クラスの TwelveHourColck.cs を示します。

 namespace ConditionalClockButton
{
    public class TwelveHourClock : ClockButton.Clock
    {
        // Initialize for Hour value of 0
        int hour12 = 12;
        bool isAm = true;
        bool isPm = false;

        public int Hour12
        {
            set { SetProperty<int>(ref hour12, value); }
            get { return hour12; }
        }

        public bool IsAm
        {
            set { SetProperty<bool>(ref isAm, value); }
            get { return isAm; }
        }

        public bool IsPm
        {
            set { SetProperty<bool>(ref isPm, value); }
            get { return isPm; }
        }

        protected override void OnPropertyChanged(string propertyName)
        {
            if (propertyName == "Hour")
            {
                this.Hour12 = (this.Hour - 1) % 12 + 1;
                this.IsAm = this.Hour < 12;
                this.IsPm = !this.IsAm;
            }
            base.OnPropertyChanged(propertyName);
        }
    }
}

このコードは、WinRT XAML と同じになります。次に表示を切り替えるために、BooleanToVisibilityConverter.cs を示します。

 using System;
using System.Windows;
using System.Windows.Data;

namespace ConditionalClockButton
{
    public sealed class BooleanToVisibilityConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return (bool)value ? Visibility.Visible : Visibility.Collapsed;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return (Visibility)value == Visibility.Visible;
        }
    }
}

コードは、名前空間とConvert メソッドと ConvertBack メソッドの第4パラメーターを除けば WinRT XAML と同じになります。すでに説明していますが、Windows ストア アプリのプロジェクトと違うので WPF XAML では、サンプルになるようなコンバーターのコードはプロジェクトに含まれていません。それでは、ConditionalClockButton プロジェクトの MainWindow.xaml の抜粋を示します。

 <Grid>
    <Button HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="24">

        <local:TwelveHourClock IsEnabled="True" />

        <Button.ContentTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <StackPanel.Resources>
                        <local:BooleanToVisibilityConverter x:Key="booleanToVisibility" />
                    </StackPanel.Resources>
                    <TextBlock Text="It's after " />
                    <TextBlock Text="{Binding Hour12}" />
                    <TextBlock Text=" o'clock" />
                    <TextBlock Text=" in the morning!"
                               Visibility="{Binding IsAm, 
                                                    Converter={StaticResource booleanToVisibility}}" />
                    <TextBlock Text=" in the afternoon!"
                               Visibility="{Binding IsPm, 
                                                    Converter={StaticResource booleanToVisibility}}" />
                </StackPanel>
            </DataTemplate>
        </Button.ContentTemplate>
    </Button>
</Grid>

このコードは、組み込みのスタイルを除けば WinRT XAML と同じになります。書籍に説明がありますが、今までと異なるのはリソースの定義が StackPanle に定義されていることになります。これは、リソースを扱った章で説明していますが、リソースは様々な要素に定義できることの一例でしかありません。このように定義することで、StackPanel の中だけで使用できるリソースとなります。つまり、リソースのスコープを定義したわけです。それでは、実行結果を示します。
ConditionalClockButton

11.3(P508) コレクション コントロールと DataTemplate の本来の使用方法

本節では、DataTemplate を使用すべき場面を説明しています。そのために、ItemControl を使ってコレクションがどのように表示されるかを説明しています。ここまでの説明が済んでから、DataTemplate の具体例を説明します。そして、DataTemplate の使い方を説明するために ColorItems プロジェクトの MainWindow.xaml の抜粋を示します。

 <Grid>
    <ScrollViewer>
        <ItemsControl x:Name="itemsControl"
                      FontSize="24">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Grid Width="240"
                          Margin="0 12">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="144" />
                            <ColumnDefinition Width="Auto" />
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>
                        <Rectangle Grid.Column="0"
                                   Grid.Row="0"
                                   Grid.RowSpan="4"
                                   Margin="12 0">
                            <Rectangle.Fill>
                                <SolidColorBrush Color="{Binding}" />
                            </Rectangle.Fill>
                        </Rectangle>
                        <StackPanel Grid.Column="1"
                                    Grid.Row="0"
                                    Orientation="Horizontal">
                            <TextBlock Text="A = " />
                            <TextBlock Text="{Binding A}" />
                        </StackPanel>
                        <StackPanel Grid.Column="1"
                                    Grid.Row="1"
                                    Orientation="Horizontal">
                            <TextBlock Text="R = " />
                            <TextBlock Text="{Binding R}" />
                        </StackPanel>
                        <StackPanel Grid.Column="1"
                                    Grid.Row="2"
                                    Orientation="Horizontal">
                            <TextBlock Text="G = " />
                            <TextBlock Text="{Binding G}" />
                        </StackPanel>
                        <StackPanel Grid.Column="1"
                                    Grid.Row="3"
                                    Orientation="Horizontal">
                            <TextBlock Text="B = " />
                            <TextBlock Text="{Binding B}" />
                        </StackPanel>
                    </Grid>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </ScrollViewer>
</Grid>

この XAML は、組み込みのスタイルを除けば WinRT XAML と同じになります。そして、データを設定するための MainWindow.xaml.cs の抜粋を示します。

 public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        IEnumerable<PropertyInfo> properties = typeof(Colors).GetTypeInfo().DeclaredProperties;

        foreach (PropertyInfo property in properties)
        {
            Color clr = (Color)property.GetValue(null);
            itemsControl.Items.Add(clr);
        }
    }
}

このコードは、WinRT XAML と同じになります。Colors 列挙をリフレクションによって、ItemControls の Items コレクションに追加しています。それでは、実行結果を示します。
ColorItems

実行結果には、色の名前が含まれていないことから、これを解決するために Petzold.ProgrammingWindows6.Chapter11 プロジェクトの NamedColor.cs を示します。

 using System.Collections.Generic;
using System.Reflection;
using System.Windows.Media;

namespace Petzold.ProgrammingWindows6.Chapter11
{
    public class NamedColor
    {
        static NamedColor()
        {
            List<NamedColor> colorList = new List<NamedColor>();
            IEnumerable<PropertyInfo> properties = typeof(Colors).GetTypeInfo().DeclaredProperties;

            foreach (PropertyInfo property in properties)
            {
                NamedColor namedColor = new NamedColor
                {
                    Name = property.Name,
                    Color = (Color)property.GetValue(null)
                };
                colorList.Add(namedColor);
            }
            All = colorList;
        }

        public static IEnumerable<NamedColor> All { private set; get; }

        public string Name { private set; get; }

        public Color Color { private set; get; }
    }
}

このコードは、WinRT XAML と同じになります。そして、INotifyPropertyChanged インタフェースを実装していない理由なども書籍では説明しています。この理由は、単純でデータが動的に変化しないためです。そして、次に色の成分値を 16進数に変換するコンバーターである ByteToHexStringConverter.cs を示します。

 using System;
using System.Windows.Data;

namespace Petzold.ProgrammingWindows6.Chapter11
{
    public class ByteToHexStringConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return ((byte)value).ToString("X2");
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value;
        }
    }
}

このコードは、名前空間とConvert メソッドと ConvertBack メソッドの第4パラメーターを除けば WinRT XAML と同じになります。それでは、これらのクラスを使用する ColorItemsSource プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ...
        xmlns:ch11="clr-namespace:Petzold.ProgrammingWindows6.Chapter11;assembly=Petzold.ProgrammingWindows6.Chapter11"
        ... >
    <Window.Resources>
        <ch11:ByteToHexStringConverter x:Key="byteToHexString" />
    </Window.Resources>
    <Grid>
        <ScrollViewer>
            <ItemsControl x:Name="itemsControl">

                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Border BorderBrush="Black"
                                BorderThickness="1"
                                Width="336"
                                Margin="6">
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto" />
                                    <ColumnDefinition Width="*"/>
                                </Grid.ColumnDefinitions>

                                <Rectangle Grid.Column="0"
                                           Height="72"
                                           Width="72"
                                           Margin="6">
                                    <Rectangle.Fill>
                                        <SolidColorBrush Color="{Binding Color}" />
                                    </Rectangle.Fill>
                                </Rectangle>

                                <StackPanel Grid.Column="1"
                                            VerticalAlignment="Center">
                                    <TextBlock FontSize="24"
                                               Text="{Binding Name}" />

                                    <ContentControl FontSize="18">
                                        <StackPanel Orientation="Horizontal">
                                            <TextBlock Text="{Binding Color.A,
                                                                      Converter={StaticResource byteToHexString}}" />
                                            <TextBlock Text="-" />
                                            <TextBlock Text="{Binding Color.R,
                                                                      Converter={StaticResource byteToHexString}}" />
                                            <TextBlock Text="-" />
                                            <TextBlock Text="{Binding Color.G,
                                                                      Converter={StaticResource byteToHexString}}" />
                                            <TextBlock Text="-" />
                                            <TextBlock Text="{Binding Color.B,
                                                                      Converter={StaticResource byteToHexString}}" />
                                        </StackPanel>
                                    </ContentControl>
                                </StackPanel>
                            </Grid>
                        </Border>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </ScrollViewer>
    </Grid>
</Window>

この XAML は、名前空間「xmlns:ch11」の定義方法と組み込みのスタイルを除けば WinRT XAML と同じになります。それでは、データ ソースを設定する MainWindow.xam.cs の抜粋を示します。

 public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        itemsControl.ItemsSource = NamedColor.All;
    }
}

このコードは、WinRT XAML と同じになります。それでは、実行結果を示します。
ColorItemsSource

今度は、コードを使用しないでデータ バインディングを行う ColorItemsSourceWithBinding プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ...
        xmlns:ch11="clr-namespace:Petzold.ProgrammingWindows6.Chapter11;assembly=Petzold.ProgrammingWindows6.Chapter11"
        ... >
    <Window.Resources>
        <ch11:NamedColor x:Key="namedColor" />
        <ch11:ByteToHexStringConverter x:Key="byteToHexString" />
    </Window.Resources>

    <Grid>
        <ScrollViewer>
            <ItemsControl ItemsSource="{Binding Source={StaticResource namedColor},
                                                Path=All}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Border BorderBrush="Black"
                                BorderThickness="1"
                                Width="336"
                                Margin="6">
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto" />
                                    <ColumnDefinition Width="*"/>
                                </Grid.ColumnDefinitions>
                                <Rectangle Grid.Column="0"
                                           Height="72"
                                           Width="72"
                                           Margin="6">
                                    <Rectangle.Fill>
                                        <SolidColorBrush Color="{Binding Color}" />
                                    </Rectangle.Fill>
                                </Rectangle>
                                <StackPanel Grid.Column="1"
                                            VerticalAlignment="Center">
                                    <TextBlock FontSize="24"
                                               Text="{Binding Name}" />
                                    <ContentControl FontSize="18">
                                        <StackPanel Orientation="Horizontal">
                                            <TextBlock Text="{Binding Color.A,
                                                                      Converter={StaticResource byteToHexString}}" />
                                            <TextBlock Text="-" />
                                            <TextBlock Text="{Binding Color.R,
                                                                      Converter={StaticResource byteToHexString}}" />
                                            <TextBlock Text="-" />
                                            <TextBlock Text="{Binding Color.G,
                                                                      Converter={StaticResource byteToHexString}}" />
                                            <TextBlock Text="-" />
                                            <TextBlock Text="{Binding Color.B,
                                                                      Converter={StaticResource byteToHexString}}" />
                                        </StackPanel>
                                    </ContentControl>
                                </StackPanel>
                            </Grid>
                        </Border>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </ScrollViewer>
    </Grid>
</Window>

この XAML は、名前空間の記述と組み込みのスタイルを除けば WinRT XAML と同じになります。もちろん、実行結果も ColorItemsSource プロジェクトと同じになります。「コレクションとデータ バインディングとテンプレートの組み合わせです。WinRT プログラミングの本質はそこにあります」(書籍より引用)と書籍に記述されていますが、これは XAML 系 UI 技術に共通する特徴ですから、WPF XAML にも当てはまります。また、書籍ではDataTemplate の定義内容などを説明していますので、書籍の熟読をお願いします。

11.4(P520) コレクションとインタフェース

本節では、コレクションをバインディングするために使用されている IEnumerable<T> インタフェースや IDictionary<TKey, TValue> インタフェース、INotifyPropertyChanged インタフェース、INotifyCollectionChanged インタフェースと WinRT の関係を説明しています。WinRT と .NET Framework のコレクションは、透過的に使えるようにプロジェクションという仕組みが用意されています。そして、データ バインディングを考える上で、本節の説明は WPF XAML にも当てはまりますので書籍の熟読をお願いします。

11.5(P522) タップと選択

本節では、DataTemplate で表現されるデータを選択するためにタップ イベントでどのように処理するかを説明しています。そして、選択したアイテムを使う SimpleListBox プロジェクトの Mainwindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <ch11:NamedColor x:Key="namedColor" />
    </Window.Resources>

    <Grid>
        <ListBox Name="lstbox"
                 ItemsSource="{Binding Source={StaticResource namedColor},
                                       Path=All}"
                 DisplayMemberPath="Name"
                 Width="288"
                 HorizontalAlignment="Center" />
        <Grid.Background>
            <SolidColorBrush Color="{Binding ElementName=lstbox,
                                             Path=SelectedItem.Color}" />
        </Grid.Background>
    </Grid>
</Window>

この XAML は、WinRT XAML と同じになります。Grid の Background にリストボックスで選択した色をバインディングしていることが、XAML から理解できることでしょう。それでは、実行結果を示します。
SimpleListBox

書籍では、SelectedValuePath プロパティなどの使い方を説明してから、ListBox の ItemTemplate に DataTemplate を使用する説明をしています。それでは、ListBoxWithItemTemplate プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <ch11:NamedColor x:Key="namedColor" />
    </Window.Resources>
    <Grid>
        <ListBox Name="lstbox"
                 ItemsSource="{Binding Source={StaticResource namedColor},
                                       Path=All}"
                 Width="380">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="{Binding RelativeSource={RelativeSource TemplatedParent},
                                                  Path=Foreground}"
                            BorderThickness="1"
                            Width="336"
                            Margin="6"
                            Loaded="OnItemLoaded">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto" />
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>
                            <Rectangle Grid.Column="0"
                                       Height="72"
                                       Width="72"
                                       Margin="6">
                                <Rectangle.Fill>
                                    <SolidColorBrush Color="{Binding Color}" />
                                </Rectangle.Fill>
                            </Rectangle>
                            <TextBlock Grid.Column="1"
                                       FontSize="24"
                                       Text="{Binding Name}"
                                       VerticalAlignment="Center" />
                        </Grid>
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Grid.Background>
            <SolidColorBrush Color="{Binding ElementName=lstbox,
                                             Path=SelectedItem.Color}" />
        </Grid.Background>
    </Grid>
</Window>

この XAML は、WinRT XAML と同じになります。それでは、実行結果を示します。
ListBoxWithItemTemplate

書籍では、特殊なデータ バインディング記述として「TemplatedParent」を説明しています。もちろん、組み込みのスタイルを除けば WPF XAML にも共通します。このことは、XAML そのものを大きく変更していないことでも明らかですから、詳しい説明は書籍を参照してください。

11.6(P527) 見えるパネルと見えないパネル

本節では、ListBoxWithItemTemplate プロジェクトの Border に対する Loaded イベント ハンドラーを使って UI の仮想化を説明しています。仮想化を説明してから、ItemPanelTemplate の説明になります。それでは、ItemPanleTemplate を使用する HorizontalListBox プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <ch11:NamedColor x:Key="namedColor" />
    </Window.Resources>

    <Grid>
        <ListBox Name="lstbox"
                 ItemsSource="{Binding Source={StaticResource namedColor},
                                       Path=All}"
                 Height="120"
                 ScrollViewer.HorizontalScrollBarVisibility="Auto"
                 ScrollViewer.VerticalScrollBarVisibility="Disabled">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="{Binding RelativeSource={RelativeSource TemplatedParent},
                                                  Path=Foreground}"
                            BorderThickness="1"
                            Width="336"
                            Margin="6"
                            Loaded="OnItemLoaded">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto" />
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>
                            <Rectangle Grid.Column="0"
                                       Height="72"
                                       Width="72"
                                       Margin="6">
                                <Rectangle.Fill>
                                    <SolidColorBrush Color="{Binding Color}" />
                                </Rectangle.Fill>
                            </Rectangle>
                            <TextBlock Grid.Column="1"
                                       FontSize="24"
                                       Text="{Binding Name}"
                                       VerticalAlignment="Center" />
                        </Grid>
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel Orientation="Horizontal" />
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>
        <Grid.Background>
            <SolidColorBrush Color="{Binding ElementName=lstbox,
                                             Path=SelectedItem.Color}" />
        </Grid.Background>
    </Grid>
</Window>

この XAML は、ScrollViewer の HorizontalScrollMode と VerticalScrollMode プロパティを除けば WinRT XAML と同じになります。WinRT XAML の HorizontalScrollMode と VerticalScrollMode プロパティは、ScrollMode 列挙を指定するものであり、WPF XAML では Panning プロパティに相当するものになります。Panning プロパティとは、タッチ操作におけるスクロールを制御するためのプロパティですから、タッチ対応にするのであれば指定した方が良いでしょう。Panning プロパティを指定しなかった場合は、デフォルトでパン ジェスチャが有効になっています。そして、ItemsPanelTemplate に VirtualizingStackPanel を指定して 水平方向(Horizontal) に指定していますから、アイテムが縦方向ではなく横(水平)方向に表現されることとなります。それでは、実行結果を示します。
HorizontalListBox

書籍には、ItemsPanelTemplate の説明がありますので熟読をお願いします。UI の仮想化は、Windows Forms になく XAML 系 の UI 技術の特徴でもあり、高速化に寄与しています。WinRT XAML では、UI の仮想化だけではなくデータの仮想化もサポートされていますので、興味があれば自分で調べることをお勧めします。

11.7(P532) カスタム パネル

本節では、WrapGrid や VariableSizedWrapGrid パネルがうまく使えないことを出発点にして、独自のカスタム パネルを定義するために必要な事項を説明しています。そして、WPF XAML に含まれている UniformGrid パネルを独自に定義することに説明が移ります。しかし、この記事は WPF XAML なので、UniformGrid が標準提供されていますから、WinRT XAML のように UniformGrid を独自に定義する必要はありません。もし、カスタム パネルを自分で作成したい場合は書籍や「Applications = Code + Markup」など参考にするのと WPF Toolkit などの具体例なども参考にしてください。それでは、UniformGrid パネルを使用する AllColorsItemsControl プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <ch11:NamedColor x:Key="namedColor" />
        <ch11:ColorToContrastColorConverter x:Key="colorConverter" />
    </Window.Resources>

    <Grid>
        <ItemsControl ItemsSource="{Binding Source={StaticResource namedColor},
                                            Path=All}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="{Binding RelativeSource={RelativeSource TemplatedParent},
                                                  Path=Foreground}"
                            BorderThickness="2"
                            Margin="2">
                        <Border.Background>
                            <SolidColorBrush Color="{Binding Color}" />
                        </Border.Background>
                        <Viewbox>
                            <TextBlock Text="{Binding Name}"
                                       HorizontalAlignment="Center"
                                       VerticalAlignment="Center">
                                <TextBlock.Foreground>
                                    <SolidColorBrush Color="{Binding Color,
                                                Converter={StaticResource colorConverter}}" />
                                </TextBlock.Foreground>
                            </TextBlock>
                        </Viewbox>
                    </Border>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <UniformGrid />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
    </Grid>
</Window>

この XAML は、組み込みスタイルと「ch11:UniformGrid」を除けば WinRT XAML と同じになります。次にモノクロ諧調を計算する Petzold.ProgrammingWindows6.Chapter11 プロジェクトの ColorToContrastColorConverter.cs を示します。

 using System;
using System.Windows.Data;
using System.Windows.Media;

namespace Petzold.ProgrammingWindows6.Chapter11
{
    public class ColorToContrastColorConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            Color clr = (Color)value;
            double grayShade = 0.30 * clr.R + 0.59 * clr.G + 0.11 * clr.B;
            return grayShade > 128 ? Colors.Black : Colors.White;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value;
        }
    }
}

この コードは、Convert と ConvertBack メソッドの第4引数を除けば WinRT XAML と同じになります。それでは、実行結果を示します。
AllColorsItemsControl

実は、Petzold.ProgrammingWindows6.Chapter11 プロジェクトに定義されている UniformGrid も WPF 向けに移植してあります(UniformGrid2 クラス)。でも、ウィンドウ サイズをリサイズした場合の表示がうまく行っていません。多分、MeasureOverride メソッドなどで適切なサイズを計算できていないのが原因だと思われます。書籍を WPF XAML に置き換えての説明という観点では標準の UniformGrid で目的を達成できていますので、興味がある方が UniformGrid2 クラスを完成させて頂ければと思います。完成したならば、どこかに記事を掲載してください。そうすることで、WPF を学ぶ人にとって有益なものになることでしょう。

今度は、Border 要素と TextBlock 要素にサイズ指定した ListBoxWithUniformGrid プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <ch11:NamedColor x:Key="namedColor" />
        <ch11:ColorToContrastColorConverter x:Key="colorConverter" />
    </Window.Resources>

    <Grid>
        <ListBox ItemsSource="{Binding Source={StaticResource namedColor},
                                       Path=All}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="{Binding RelativeSource={RelativeSource TemplatedParent},
                                                  Path=Foreground}"
                            Width="288"
                            Height="72"
                            BorderThickness="3"
                            Margin="3">
                        <Border.Background>
                            <SolidColorBrush Color="{Binding Color}" />
                        </Border.Background>

                        <TextBlock Text="{Binding Name}"
                                       FontSize="24"
                                       HorizontalAlignment="Center"
                                       VerticalAlignment="Center">
                            <TextBlock.Foreground>
                                <SolidColorBrush Color="{Binding Color,
                                                Converter={StaticResource colorConverter}}" />
                            </TextBlock.Foreground>
                        </TextBlock>
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <UniformGrid />
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>
    </Grid>
</Window>

この XAML は、AllCorlorsItemsControl と同じ箇所だけを WPF XAML 用に変更しています。Border に Width と Height を指定した結果は、WinRT XAML と同じで正しく表示されています。そでは、実行結果を示します。
ListBoxWithUniformGrid

ListBoxWithUniformGrid プロジェクト で、UniformGrid を Petzold.ProgrammingWindows6.Chapter11 プロジェクトに定義されている UniformGrid2 に置き換える場合は、Rows か Columns プロパティを明示的に指定することで正常に動作します。つまり、書籍にも記述されていますが利用可能な幅と子要素の最大幅に基づいて、列の数を割り出すロジックなどにおいて WinRT XAML との違いがあることになります。これらの問題を解消すれば、WPF XAML 用のカスタム パネルである UniformGrid2 を完成させることができることでしょう。書籍では、スクロールの切り替え方なども説明していますので、書籍の熟読をお願いします。

11.8(P546) アイテム テンプレートによる棒グラフ

本節では、表題の通りアイテム テンプレートを使って棒グラフを描画するアイテム テンプレートを説明しています。それでは、RgbBarChart プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <ch11:NamedColor x:Key="namedColor" />
        <ch11:ByteToHexStringConverter x:Key="byteToHex" />
    </Window.Resources>

    <Grid>
        <ItemsControl ItemsSource="{Binding Source={StaticResource namedColor},
                                            Path=All}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel Name="stackPanel"
                                Height="765"
                                RenderTransformOrigin="0.5 0.5"
                                Margin="1 0" >
                        <StackPanel.RenderTransform>
                            <ScaleTransform ScaleY="-1" />
                        </StackPanel.RenderTransform>
                        <Rectangle Fill="Red" 
                                   Height="{Binding Color.R}" />
                        <Rectangle Fill="Green"
                                   Height="{Binding Color.G}" />
                        <Rectangle Fill="Blue"
                                   Height="{Binding Color.B}" />
                        <ToolTipService.ToolTip>
                            <ToolTip x:Name="tooltip"
                                     PlacementTarget="{Binding ElementName=stackPanel}">
                                <Grid>
                                    <StackPanel >
                                        <TextBlock Text="{Binding Name}"
                                                   HorizontalAlignment="Center" />
                                        <StackPanel DataContext="{Binding Color}" 
                                                    Orientation="Horizontal"
                                                    HorizontalAlignment="Center">
                                            <TextBlock Text="R=" />
                                            <TextBlock Text="{Binding R}" />
                                            <TextBlock Text=" G=" />
                                            <TextBlock Text="{Binding G}" />
                                            <TextBlock Text=" B=" />
                                            <TextBlock Text="{Binding B}" />
                                        </StackPanel>
                                    </StackPanel>
                                </Grid>
                            </ToolTip>
                        </ToolTipService.ToolTip>
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <UniformGrid Rows="1" />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
    </Grid>
</Window>

この XAML は、組み込みのスタイルと Grid の DataContext、StackPanel の DataContext を除けば WinRT XAML と同じになります。この DataContext の指定方法は、WinRT XAML と WPF XAML で異なる動作をするものになっています。それでは、実行結果を示します。
RgbBarChart

結論から言えば、WPF XAML のアイテム テンプレートではバインディングされたアイテムが ToolTip であっても自動的に反映されるために「Grid DataContext="{Binding ElementName=tooltip, Path=PlacementTarget}"」を記述してはいけません。記述してしまうと、記述に従って StackPanel へバインディングされるために何も表示されなくなります。一方で WinRT XAML では、Tooltip は表示を行う時にバインディングが解決されるような動きをしています。つまり、「Grid DataContext="{Binding ElementName=tooltip, Path=PlacementTarget}"」の記述によって ToolTip を表示する時に StackPanel の DataContext へとデータソースが解決されます。この動きによって、期待通りに ToolTip が表示されるようになります。この動きの違いは、WPF XAML はフルスタックのフレームワークであり ToolTip を含めて統一的にデータソースが解決されるからであり、WinRT XAML では必要がないものは必要とされるまで解決しない(実行速度を優先)という考え方に基づいたサブセットと考えると理解し易いかも知れません。この RdbBarChart プロジェクトの WPF XAML 版の欠点は、グラフの大きさが固定されているのでウィンドウのリサイズに対応できないことです。この点については、グラフの高さを計算するロジックにウィンドウ サイズを加味し、サイズ変更イベントの処理を組み合わせれば解決することができますので、自分で挑戦してみてください。もちろん、書籍にあるように RenderTransform プロパティで調整するのでも構いません。

11.9(P548) FlipView コントロール

本節では、WinRT XAML に導入された FlipView コントロールを使ってアイテム テンプレートを使用する方法を説明しています。従って、WPF XAML ではアイテム テンプレートの説明を別のコントロールに流用することはできますが、WinRT の FlipView のようなコントロールを使いたいとすると、自分で作成するか、サードパーティー製のコントロールを使用することになります。たとえば、DevExpress の製品などになります。

11.10(P551) 基本的なコントロール テンプレート

本節では、DataTemplate、ItemsPanelTemplate に続く 3つ目のテンプレートとして ControlTemplate の説明をしています。ControlTemplate の説明として、Style に始まり、ControlTemplate へと続き、TemplateBinding を説明し、ContentPresenter の説明へと続きます。ほとんどの説明が、WPF XAML にも利用できますので、書籍の熟読をお願いします。

11.11(P562) ビジュアル状態マネージャー

本節では、コントロールがユーザー操作に反応するフィードバックを定義するビジュアル状態マネージャー(VisualStateManager)の説明をしています。この説明のために Button コントロールを使用して、7つのビジュアル状態を説明し、2つのグループに分類されることを説明しています。

  • CommonStates
    Normal、MouseOrver、Pressed、Disabled
  • FocusStates (WinRT では、FocusedStates)
    Forcused、Unforcused

WinRT XAML と WPF XAML では、ビジュアル状態の状態名が少し異なっています。具体例を次に示します。

WinRT WPF グループ
PointOrver MouseOrver CommonStates
PointerFocused 無し FocusStates

この点に注意すれば、WinRT XAML のサンプルや WPF XAML のサンプルを相互に書き換えることができます。書籍では、これらのビジュアル状態の定義と ContentPresenter の使い方を説明してから、カスタム ボタンを定義した全体像を示します。それでは、CustomButtonTemplate プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... >
    <Window.Resources>
        <ControlTemplate x:Key="buttonTemplate" TargetType="Button">
            <Grid>
             <VisualStateManager.VisualStateGroups>
                    <VisualStateGroup x:Name="CommonStates">
                      <VisualState x:Name="Normal" />
                       <VisualState x:Name="MouseOver" />
                        <VisualState x:Name="Pressed">
                            <Storyboard>
                              <ColorAnimationUsingKeyFrames Storyboard.TargetName="border"
                                                               Storyboard.TargetProperty="(Background).(SolidColorBrush.Color)">
                                    <EasingColorKeyFrame KeyTime="0" Value="LightGray" />
                                </ColorAnimationUsingKeyFrames>

                                <!-- border に TextBlock.Foreground を追加する必要がある
                                     ContentPresenter に Foreground がないため-->
                                <ColorAnimationUsingKeyFrames Storyboard.TargetName="border"
                                                               Storyboard.TargetProperty="(TextBlock.Foreground).(SolidColorBrush.Color)">
                                    <EasingColorKeyFrame KeyTime="0" Value="Black" />
                                </ColorAnimationUsingKeyFrames>
                            </Storyboard>
                     </VisualState>

                        <VisualState x:Name="Disabled">
                           <Storyboard>
                              <ObjectAnimationUsingKeyFrames Storyboard.TargetName="disabledRect"
                                  Storyboard.TargetProperty="(UIElement.Visibility)">
                                  <DiscreteObjectKeyFrame KeyTime="0"
                                      Value="{x:Static Visibility.Visible}" />
                             </ObjectAnimationUsingKeyFrames>
                          </Storyboard>
                     </VisualState>
                    </VisualStateGroup>

              <VisualStateGroup x:Name="FocusStates">
                        <VisualState x:Name="Unfocused"/>
                     <VisualState x:Name="Focused">
                            <Storyboard>
                              <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="focusRectangle">
                                    <EasingDoubleKeyFrame KeyTime="0" Value="1"/>
                             </DoubleAnimationUsingKeyFrames>
                          </Storyboard>
                     </VisualState>
              </VisualStateGroup>
             </VisualStateManager.VisualStateGroups>
               <Border x:Name="border"
                  Background="{TemplateBinding Background}"
                   BorderBrush="{TemplateBinding BorderBrush}"
                 BorderThickness="{TemplateBinding BorderThickness}"
                            TextBlock.Foreground="{TemplateBinding Foreground}"
                  CornerRadius="12">
                   <Grid x:Name="grid">
                      <ContentPresenter x:Name="contentPresenter"
                          Content="{TemplateBinding Content}"
                         ContentTemplate="{TemplateBinding ContentTemplate}"
                         Margin="{TemplateBinding Padding}"
                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
                        <Rectangle x:Name="focusRectangle"
                           Stroke="{TemplateBinding Foreground}"
                           Opacity="0"
                         StrokeThickness="1"
                         StrokeDashArray="2 2"
                           Margin="4"
                          RadiusX="12"
                            RadiusY="12" />
                  </Grid>
               </Border>
             <Rectangle x:Name="disabledRect"
                 Visibility="Collapsed"
                  Fill="Black"
                    Opacity="0.5" />
         </Grid>
        </ControlTemplate>

        <Style x:Key="buttonStyle" TargetType="Button">
            <Setter Property="Background" Value="White" />
            <Setter Property="Foreground" Value="Blue" />
            <Setter Property="BorderBrush" Value="Red" />
            <Setter Property="BorderThickness" Value="3" />
            <Setter Property="FontSize" Value="24" />
            <Setter Property="Padding" Value="12" />
            <Setter Property="Template" Value="{StaticResource buttonTemplate}" />
        </Style>
    </Window.Resources>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Button Content="Disable center button"
                Grid.Column="0"
                Style='{StaticResource buttonStyle}'
                Click='OnButton1Click'
                HorizontalAlignment='Center'
                VerticalAlignment='Center' />
        <Button x:Name='centerButton'
                Content='Center button'
                Grid.Column='1'
                Style='{StaticResource buttonStyle}'
                FontSize='48'
                Background='DarkGray'
                Foreground='Red'
                HorizontalAlignment='Center'
                VerticalAlignment='Center' />
        <Button Content='Enable center button'
                Grid.Column='2'
                Style='{StaticResource buttonStyle}'
                Click='OnButton3Click'
                HorizontalAlignment='Center'
                VerticalAlignment='Center' />
    </Grid>
</Window>

この XAML は、既に説明したビジュアル状態の他にもアニメーションなどの数か所を変更しています。変更箇所を次に示します。

  • CommonStates の PointOver を MouseOrver に変更。
  • CommonStates の Pressed の Storybord 内のアニメーションを変更。
    ObjectAnimationUsingKeyFrame を ColorAnimationUsingKeyFrames へ変更。
    TargetProperty 添付プロパティを「(Background).(SolidColorBrush.Color)」表記へ変更。
    TargetProperty 添付プロパティを「(TextBlock.Foreground).(SolidColorBrush.Color)」表記へ変更(WPF XAML の ContentPresenter は Foreground プロパティを持たないため)。
    DiscreteObjectKeyFrame を EasingColorKeyFrame へ変更。
  • CommonStates の Disabled の Storyboard 内のアニメーションを変更。
    TargetProperty 添付プロパティを「(UIElement.Visibility)」 へ変更。
    DiscreteObjectKeyFrame の Value を「{x:Static Visibility.Visible}」へ変更。
  • ForcusedStates を ForcusStates に変更。
  • ForcusStates の Forcused の storyboard 内のアニメーションを変更。
    DoubleAnimation を DoubleAnimationUsingKeyFrames に変更。
    TargetProperty 添付プロパティを「(UIElement.Opacity)」表記に変更。
  • Border へ 「TextBlock.Foreground="{TemplateBinding Foreground}"」 要素を追加。
    WPF XAML の ContentPresenter が Foreground プロパティを持たないため。
  • 組み込みスタイルを変更

変更箇所を見れば、ビジュアル状態の差異による変更とアニメーションの変更であることを理解できることでしょう。ContentPresenter クラスに関しては、Foreground プロパティを WinRT XAML が持っており、WPF XAML が持たないことによる変更を加えています。この理由が、Border 要素に TextBlock.Forground 添付プロパティを追加した理由であり、Button の Content にテキストを設定した時に使われるのが TextBlock であることを利用して添付プロパティにしています。ボタンをクリックした時のイベント ハンドラーを MainWindow.xaml.cs より抜粋して示します。

 private void OnButton1Click(object sender, RoutedEventArgs e)
{
    centerButton.IsEnabled = false;
}

private void OnButton3Click(object sender, RoutedEventArgs e)
{
    centerButton.IsEnabled = true;
}

このコードは、WinRT XAML と同じになります。それでは、実行結果を示します。
CustomButtonTemplate

スクリーン ショットを見れば、フォーカスを取得した場合に点線がボタン内に表示されていることに気が付くことでしょう。これが、ビジュアル状態として定義した Focused のアニメーションによる効果になります。このようなコントロール テンプレートは、理解できるまでは複雑に思えますから、書籍を参考にしながら色々と試して学習することをお勧めします。でも、全ての人がコントロール テンプレートを定義する必要があるかと言えば、カスタム コントロールを作成するなどの一部の人に限られることでしょう。でも、コントロール テンプレートの仕組みを知っていることは、とても大切なことです。

11.12(P571) generic.xaml の使用

本節では、Windows ストア アプリ プロジェクトに含まれるスタイル シートを説明しています。Visual Studio 2012 のプロジェクトでは、StandardStyle.xaml が含まれており、Visual Studio 2013 のプロジェクトには StandardStyle.xaml は含まれていません。StandrdStyle.xaml は generic.xaml から作成されており、Visual Studio 2013 のプロジェクトをビルド時に generic.xaml が組み込まれるようになっています。これらのスタイル シートが、どのような役目を持っているかを説明しています。

WPF XAML では、カスタム コントロールを作成する場合に generic.xaml というスタイル シートが必要になります。Visual Studio でカスタム コントロール テンプレートを使って新しいアイテムを追加すれば、プロジェクト内に Themes フォルダが作成されて、その中に Generic.xaml というスタイル シートが作成されます。従って、カスタム コントロールを作成する場合に必要になるスタイル シートであると理解していただければ結構です。

11.13(P571) テンプレートのパーツ

本節では、コントロール テンプレートがどのような構造になっているかという観点でコントロールが含む部品である構成要素をパーツとして説明しています。その過程で、OnApplyTemplate メソッドや GetTemplateChild メソッドなども説明しています。OnApplyTemplate メソッドは public メソッドですが、GetTemplateChild メソッドが protected メソッドである点に注意してください。つまり、GetTemplateChild メソッドでコントロールのパーツへアクセスするには、対象のコントロールを継承したクラスを作成して、作成したクラス内で GetTemplateChild メソッドを使用しなければならないからです。

書籍では、具体例として Slider コントロールに HorizontalTemplate と VerticalTemplate という名前を付けてください。という説明があり、Slider のコントロール テンプレートをカスタマイズした BareBonesSilder プロジェクトの説明になります。ここで知っておかないといけない事は、HorizontalTemplate と VerticalTemplate という名前をどこから入手したかということです。最初に思いつくのが、ドキュメントに記述されているのではないかということです。事実として、ドキュメントに記述されているものもあります。

WPF XAML の Slider には、HorizontalTemplate と VerticalTemplate という名前は定義されていませんので、BoreBonesSlider プロジェクトと同じ方法を使ったテンプレートの定義方法は使えません。この理由は、テンプレートのパーツの構成が異なるからです。プログラミング Windows の著者であるペゾルドは、BoreBonesSlider プロジェクトの着想を MSDN マガジンの「テンプレートを使用した WPF コントロールのカスタマイズ」という記事にあることが、書籍の中の注記に記述されています。この記事の中に、BareBonesProgressBar サンプルがあります。このサンプルは、プログレス バーのテンプレートをカスタマイズするというものですが、このサンプルを Visual Studio 2013 へ移植したものの実行結果を示します。
BareBonesProgressBar

このサンプルのコードを理解することで、どのようにカスタマイズができるかを理解できることでしょう。

書籍では、BareBonesSlider プロジェクトに手を加えることでスライダーをバネにした SpringLoaderSlider の説明をしています。このサンプルも、MSDN マガジンの「テンプレートを使用した WPF コントロールのカスタマイズ」という記事に原点があります。従って、この記事の SpringLoadedScrollBar サンプルを Visual Studio 2013 へ移植したものの実行結果を示します(このサンプルは、ProgressBar をカスタマイズしています)。
SpringLoadedScrollBar

書籍のサンプルと違う点は、スライダーの向きを縦方向にするテンプレートが含まれていない点になりますが、スライダー をバネ状にカスタマイズするという観点では同じものになりますから、コントロールのカスタマイズという観点では有益なものになることでしょう。

書籍では、SpeedMeterProgressBar プロジェクトを使ってスライダーと速度計(プログレス バー)を使うサンプルの説明をしています。このサンプルも、MSDN マガジンの「テンプレートを使用した WPF コントロールのカスタマイズ」という記事に原点があります。従って、この記事の SpeedMeterProgressBar サンプルを Visual Studio 2013 へ移植したものの実行結果を示します。
SpeedometerProgressBar

書籍にも記述がありますが、MSDN マガジンの「テンプレートを使用した WPF コントロールのカスタマイズ」という記事を原点に WinRT XAML へ移植しているので、もとの記事を読むことが役立つことでしょう。

この節の最初で説明しましたが、標準コントロールのスタイルやテンプレートを学習するには、どうしたら良いでしょうか。ドキュメントを探すのも 1つの手法ですが、全てのコントロールに対してスタイルやドキュメントが記述されているわけではありません。このような場合に行うとしたら、Blend for Visual Studio や Visual Studio を使ったテンプレートの編集機能を使用することをお勧めします。テンプレート編集をビジュアルに行うことができるので、ここでは Blend を使ったテンプレート編集の方法を説明します。最初に、Blend でプロジェクトを開きます。次にテンプレートを編集したいコントロールを選択して、コンテキスト メニューから[テンプレートの編集]-[コピーして編集]を選択します。この方法は、WinRT XAML も WPF XAML でも同じです。
ch11 blend1
そうすると、ダイアログが表示されますので、名前と定義先を指定して「OK」ボタンをクリックします。定義先とは、作成するスタイルを定義するリソースの場所になります。アプリケーションとは、App.xaml を意味し、このドキュメントが現在編集している Window や Page になります。
ch11 blend2

スタイル リソースが作成されると、スタイルの編集画面に切り替わります。
ch11 blend3

編集画面では、大きくは次に示す 2つの作業を行います。

  • 状態パネル
    ビジュアル状態を定義するために使用します。ビジュアル状態の名前を知らなくても、状態パネルを使って指定して、アニメーションの自動記録などを使うことでタイムラインを自動生成することができます。
  • オブジェクトとタイムライン
    ドキュメント ツリーを開いていくことで、目的となるオブジェクトを編集したりすることができます。
    ch11 blend4

このサンプルでは、書籍と同じように Slider コントロール(WPF XAML)のテンプレートを編集しています。参考までに作成された、スタイルとテンプレートを示します。

 <Window.Resources>
  <SolidColorBrush x:Key="SliderThumb.Static.Foreground" Color="#FFE5E5E5"/>
    <SolidColorBrush x:Key="SliderThumb.MouseOver.Background" Color="#FFDCECFC"/>
 <SolidColorBrush x:Key="SliderThumb.MouseOver.Border" Color="#FF7Eb4EA"/>
 <SolidColorBrush x:Key="SliderThumb.Pressed.Background" Color="#FFDAECFC"/>
   <SolidColorBrush x:Key="SliderThumb.Pressed.Border" Color="#FF569DE5"/>
   <SolidColorBrush x:Key="SliderThumb.Disabled.Background" Color="#FFF0F0F0"/>
  <SolidColorBrush x:Key="SliderThumb.Disabled.Border" Color="#FFD9D9D9"/>
  <SolidColorBrush x:Key="SliderThumb.Static.Background" Color="#FFF0F0F0"/>
    <SolidColorBrush x:Key="SliderThumb.Static.Border" Color="#FFACACAC"/>
    <ControlTemplate x:Key="SliderThumbHorizontalTop" TargetType="{x:Type Thumb}">
        <Grid HorizontalAlignment="Center" UseLayoutRounding="True" VerticalAlignment="Center">
           <Path x:Name="grip" Data="M 0,6 C0,6 5.5,0 5.5,0 5.5,0 11,6 11,6 11,6 11,18 11,18 11,18 0,18 0,18 0,18 0,6 0,6 z" Fill="{StaticResource SliderThumb.Static.Background}" Stretch="Fill" SnapsToDevicePixels="True" Stroke="{StaticResource SliderThumb.Static.Border}" StrokeThickness="1" UseLayoutRounding="True" VerticalAlignment="Center"/>
       </Grid>
       <ControlTemplate.Triggers>
            <Trigger Property="IsMouseOver" Value="true">
             <Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Background}"/>
             <Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Border}"/>
           </Trigger>
            <Trigger Property="IsDragging" Value="true">
              <Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Background}"/>
               <Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Border}"/>
         </Trigger>
            <Trigger Property="IsEnabled" Value="false">
              <Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Background}"/>
              <Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Border}"/>
            </Trigger>
        </ControlTemplate.Triggers>
   </ControlTemplate>
    <ControlTemplate x:Key="SliderThumbHorizontalBottom" TargetType="{x:Type Thumb}">
     <Grid HorizontalAlignment="Center" UseLayoutRounding="True" VerticalAlignment="Center">
           <Path x:Name="grip" Data="M 0,12 C0,12 5.5,18 5.5,18 5.5,18 11,12 11,12 11,12 11,0 11,0 11,0 0,0 0,0 0,0 0,12 0,12 z" Fill="{StaticResource SliderThumb.Static.Background}" Stretch="Fill" SnapsToDevicePixels="True" Stroke="{StaticResource SliderThumb.Static.Border}" StrokeThickness="1" UseLayoutRounding="True" VerticalAlignment="Center"/>
       </Grid>
       <ControlTemplate.Triggers>
            <Trigger Property="IsMouseOver" Value="true">
             <Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Background}"/>
             <Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Border}"/>
           </Trigger>
            <Trigger Property="IsDragging" Value="true">
              <Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Background}"/>
               <Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Border}"/>
         </Trigger>
            <Trigger Property="IsEnabled" Value="false">
              <Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Background}"/>
              <Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Border}"/>
            </Trigger>
        </ControlTemplate.Triggers>
   </ControlTemplate>
    <SolidColorBrush x:Key="SliderThumb.Track.Border" Color="#FFD6D6D6"/>
 <SolidColorBrush x:Key="SliderThumb.Track.Background" Color="#FFE7EAEA"/>
 <Style x:Key="RepeatButtonTransparent" TargetType="{x:Type RepeatButton}">
        <Setter Property="OverridesDefaultStyle" Value="true"/>
       <Setter Property="Background" Value="Transparent"/>
       <Setter Property="Focusable" Value="false"/>
      <Setter Property="IsTabStop" Value="false"/>
      <Setter Property="Template">
          <Setter.Value>
                <ControlTemplate TargetType="{x:Type RepeatButton}">
                  <Rectangle Fill="{TemplateBinding Background}" Height="{TemplateBinding Height}" Width="{TemplateBinding Width}"/>
                </ControlTemplate>
            </Setter.Value>
       </Setter>
 </Style>
  <ControlTemplate x:Key="SliderThumbHorizontalDefault" TargetType="{x:Type Thumb}">
        <Grid HorizontalAlignment="Center" UseLayoutRounding="True" VerticalAlignment="Center">
           <Path x:Name="grip" Data="M 0,0 C0,0 11,0 11,0 11,0 11,18 11,18 11,18 0,18 0,18 0,18 0,0 0,0 z" Fill="{StaticResource SliderThumb.Static.Background}" Stretch="Fill" SnapsToDevicePixels="True" Stroke="{StaticResource SliderThumb.Static.Border}" StrokeThickness="1" UseLayoutRounding="True" VerticalAlignment="Center"/>
     </Grid>
       <ControlTemplate.Triggers>
            <Trigger Property="IsMouseOver" Value="true">
             <Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Background}"/>
             <Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.MouseOver.Border}"/>
           </Trigger>
            <Trigger Property="IsDragging" Value="true">
              <Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Background}"/>
               <Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Pressed.Border}"/>
         </Trigger>
            <Trigger Property="IsEnabled" Value="false">
              <Setter Property="Fill" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Background}"/>
              <Setter Property="Stroke" TargetName="grip" Value="{StaticResource SliderThumb.Disabled.Border}"/>
            </Trigger>
        </ControlTemplate.Triggers>
   </ControlTemplate>
    <ControlTemplate x:Key="SliderHorizontal" TargetType="{x:Type Slider}">
       <Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
         <Grid>
                <Grid.RowDefinitions>
                 <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto" MinHeight="{TemplateBinding MinHeight}"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <TickBar x:Name="TopTick" Fill="{TemplateBinding Foreground}" Height="4" Margin="0,0,0,2" Placement="Top" Grid.Row="0" Visibility="Collapsed"/>
               <TickBar x:Name="BottomTick" Fill="{TemplateBinding Foreground}" Height="4" Margin="0,2,0,0" Placement="Bottom" Grid.Row="2" Visibility="Collapsed"/>
             <Border x:Name="TrackBackground" BorderBrush="{StaticResource SliderThumb.Track.Border}" BorderThickness="1" Background="{StaticResource SliderThumb.Track.Background}" Height="4.0" Margin="5,0" Grid.Row="1" VerticalAlignment="center">
                    <Canvas Margin="-6,-1">
                       <Rectangle x:Name="PART_SelectionRange" Fill="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" Height="4.0" Visibility="Hidden"/>
                 </Canvas>
             </Border>
             <Track x:Name="PART_Track" Grid.Row="1">
                  <Track.DecreaseRepeatButton>
                      <RepeatButton Command="{x:Static Slider.DecreaseLarge}" Style='{StaticResource RepeatButtonTransparent}'/>
                    </Track.DecreaseRepeatButton>
                 <Track.IncreaseRepeatButton>
                      <RepeatButton Command='{x:Static Slider.IncreaseLarge}' Style='{StaticResource RepeatButtonTransparent}'/>
                    </Track.IncreaseRepeatButton>
                 <Track.Thumb>
                     <Thumb x:Name='Thumb' Focusable='False' Height='18' OverridesDefaultStyle='True' Template='{StaticResource SliderThumbHorizontalDefault}' VerticalAlignment='Center' Width='11'/>
                 </Track.Thumb>
                </Track>
          </Grid>
       </Border>
     <ControlTemplate.Triggers>
            <Trigger Property='TickPlacement' Value='TopLeft'>
                <Setter Property='Visibility' TargetName='TopTick' Value='Visible'/>
              <Setter Property='Template' TargetName='Thumb' Value='{StaticResource SliderThumbHorizontalTop}'/>
                <Setter Property='Margin' TargetName='TrackBackground' Value='5,2,5,0'/>
          </Trigger>
            <Trigger Property='TickPlacement' Value='BottomRight'>
                <Setter Property='Visibility' TargetName='BottomTick' Value='Visible'/>
               <Setter Property='Template' TargetName='Thumb' Value='{StaticResource SliderThumbHorizontalBottom}'/>
             <Setter Property='Margin' TargetName='TrackBackground' Value='5,0,5,2'/>
          </Trigger>
            <Trigger Property='TickPlacement' Value='Both'>
               <Setter Property='Visibility' TargetName='TopTick' Value='Visible'/>
              <Setter Property='Visibility' TargetName='BottomTick' Value='Visible'/>
           </Trigger>
            <Trigger Property='IsSelectionRangeEnabled' Value='true'>
             <Setter Property='Visibility' TargetName='PART_SelectionRange' Value='Visible'/>
          </Trigger>
            <Trigger Property='IsKeyboardFocused' Value='true'>
               <Setter Property='Foreground' TargetName='Thumb' Value='Blue'/>
           </Trigger>
        </ControlTemplate.Triggers>
   </ControlTemplate>
    <ControlTemplate x:Key='SliderThumbVerticalLeft' TargetType='{x:Type Thumb}'>
     <Grid HorizontalAlignment='Center' UseLayoutRounding='True' VerticalAlignment='Center'>
           <Path x:Name='grip' Data='M 6,11 C6,11 0,5.5 0,5.5 0,5.5 6,0 6,0 6,0 18,0 18,0 18,0 18,11 18,11 18,11 6,11 6,11 z' Fill='{StaticResource SliderThumb.Static.Background}' Stretch='Fill' Stroke='{StaticResource SliderThumb.Static.Border}'/>
     </Grid>
       <ControlTemplate.Triggers>
            <Trigger Property='IsMouseOver' Value='true'>
             <Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.MouseOver.Background}'/>
             <Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.MouseOver.Border}'/>
           </Trigger>
            <Trigger Property='IsDragging' Value='true'>
              <Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.Pressed.Background}'/>
               <Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.Pressed.Border}'/>
         </Trigger>
            <Trigger Property='IsEnabled' Value='false'>
              <Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.Disabled.Background}'/>
              <Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.Disabled.Border}'/>
            </Trigger>
        </ControlTemplate.Triggers>
   </ControlTemplate>
    <ControlTemplate x:Key='SliderThumbVerticalRight' TargetType='{x:Type Thumb}'>
        <Grid HorizontalAlignment='Center' UseLayoutRounding='True' VerticalAlignment='Center'>
           <Path x:Name='grip' Data='M 12,11 C12,11 18,5.5 18,5.5 18,5.5 12,0 12,0 12,0 0,0 0,0 0,0 0,11 0,11 0,11 12,11 12,11 z' Fill='{StaticResource SliderThumb.Static.Background}' Stretch='Fill' Stroke='{StaticResource SliderThumb.Static.Border}'/>
     </Grid>
       <ControlTemplate.Triggers>
            <Trigger Property='IsMouseOver' Value='true'>
             <Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.MouseOver.Background}'/>
             <Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.MouseOver.Border}'/>
           </Trigger>
            <Trigger Property='IsDragging' Value='true'>
              <Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.Pressed.Background}'/>
               <Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.Pressed.Border}'/>
         </Trigger>
            <Trigger Property='IsEnabled' Value='false'>
              <Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.Disabled.Background}'/>
              <Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.Disabled.Border}'/>
            </Trigger>
        </ControlTemplate.Triggers>
   </ControlTemplate>
    <ControlTemplate x:Key='SliderThumbVerticalDefault' TargetType='{x:Type Thumb}'>
      <Grid HorizontalAlignment='Center' UseLayoutRounding='True' VerticalAlignment='Center'>
           <Path x:Name='grip' Data='M0.5,0.5 L18.5,0.5 18.5,11.5 0.5,11.5z' Fill='{StaticResource SliderThumb.Static.Background}' Stretch='Fill' Stroke='{StaticResource SliderThumb.Static.Border}'/>
      </Grid>
       <ControlTemplate.Triggers>
            <Trigger Property='IsMouseOver' Value='true'>
             <Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.MouseOver.Background}'/>
             <Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.MouseOver.Border}'/>
           </Trigger>
            <Trigger Property='IsDragging' Value='true'>
              <Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.Pressed.Background}'/>
               <Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.Pressed.Border}'/>
         </Trigger>
            <Trigger Property='IsEnabled' Value='false'>
              <Setter Property='Fill' TargetName='grip' Value='{StaticResource SliderThumb.Disabled.Background}'/>
              <Setter Property='Stroke' TargetName='grip' Value='{StaticResource SliderThumb.Disabled.Border}'/>
            </Trigger>
        </ControlTemplate.Triggers>
   </ControlTemplate>
    <ControlTemplate x:Key='SliderVertical' TargetType='{x:Type Slider}'>
     <Border x:Name='border' BorderBrush='{TemplateBinding BorderBrush}' BorderThickness='{TemplateBinding BorderThickness}' Background='{TemplateBinding Background}' SnapsToDevicePixels='True'>
         <Grid>
                <Grid.ColumnDefinitions>
                  <ColumnDefinition Width='Auto'/>
                  <ColumnDefinition MinWidth='{TemplateBinding MinWidth}' Width='Auto'/>
                    <ColumnDefinition Width='Auto'/>
              </Grid.ColumnDefinitions>
             <TickBar x:Name='TopTick' Grid.Column='0' Fill='{TemplateBinding Foreground}' Margin='0,0,2,0' Placement='Left' Visibility='Collapsed' Width='4'/>
                <TickBar x:Name='BottomTick' Grid.Column='2' Fill='{TemplateBinding Foreground}' Margin='2,0,0,0' Placement='Right' Visibility='Collapsed' Width='4'/>
                <Border x:Name='TrackBackground' BorderBrush='{StaticResource SliderThumb.Track.Border}' BorderThickness='1' Background='{StaticResource SliderThumb.Track.Background}' Grid.Column='1' HorizontalAlignment='center' Margin='0,5' Width='4.0'>
                    <Canvas Margin='-1,-6'>
                       <Rectangle x:Name='PART_SelectionRange' Fill='{DynamicResource {x:Static SystemColors.HighlightBrushKey}}' Visibility='Hidden' Width='4.0'/>
                  </Canvas>
             </Border>
             <Track x:Name='PART_Track' Grid.Column='1'>
                   <Track.DecreaseRepeatButton>
                      <RepeatButton Command='{x:Static Slider.DecreaseLarge}' Style='{StaticResource RepeatButtonTransparent}'/>
                    </Track.DecreaseRepeatButton>
                 <Track.IncreaseRepeatButton>
                      <RepeatButton Command='{x:Static Slider.IncreaseLarge}' Style='{StaticResource RepeatButtonTransparent}'/>
                    </Track.IncreaseRepeatButton>
                 <Track.Thumb>
                     <Thumb x:Name='Thumb' Focusable='False' Height='11' OverridesDefaultStyle='True' Template='{StaticResource SliderThumbVerticalDefault}' VerticalAlignment='Top' Width='18'/>
                  </Track.Thumb>
                </Track>
          </Grid>
       </Border>
     <ControlTemplate.Triggers>
            <Trigger Property='TickPlacement' Value='TopLeft'>
                <Setter Property='Visibility' TargetName='TopTick' Value='Visible'/>
              <Setter Property='Template' TargetName='Thumb' Value='{StaticResource SliderThumbVerticalLeft}'/>
             <Setter Property='Margin' TargetName='TrackBackground' Value='2,5,0,5'/>
          </Trigger>
            <Trigger Property='TickPlacement' Value='BottomRight'>
                <Setter Property='Visibility' TargetName='BottomTick' Value='Visible'/>
               <Setter Property='Template' TargetName='Thumb' Value='{StaticResource SliderThumbVerticalRight}'/>
                <Setter Property='Margin' TargetName='TrackBackground' Value='0,5,2,5'/>
          </Trigger>
            <Trigger Property='TickPlacement' Value='Both'>
               <Setter Property='Visibility' TargetName='TopTick' Value='Visible'/>
              <Setter Property='Visibility' TargetName='BottomTick' Value='Visible'/>
           </Trigger>
            <Trigger Property='IsSelectionRangeEnabled' Value='true'>
             <Setter Property='Visibility' TargetName='PART_SelectionRange' Value='Visible'/>
          </Trigger>
            <Trigger Property='IsKeyboardFocused' Value='true'>
               <Setter Property='Foreground' TargetName='Thumb' Value='Blue'/>
           </Trigger>
        </ControlTemplate.Triggers>
   </ControlTemplate>
    <Style x:Key='SliderStyle1' TargetType='{x:Type Slider}'>
     <Setter Property='Stylus.IsPressAndHoldEnabled' Value='false'/>
       <Setter Property='Background' Value='Transparent'/>
       <Setter Property='BorderBrush' Value='Transparent'/>
      <Setter Property='Foreground' Value='{StaticResource SliderThumb.Static.Foreground}'/>
        <Setter Property='Template' Value='{StaticResource SliderHorizontal}'/>
       <Style.Triggers>
          <Trigger Property='Orientation' Value='Vertical'>
             <Setter Property='Template' Value='{StaticResource SliderVertical}'/>
         </Trigger>
        </Style.Triggers>
 </Style>
</Window.Resources>

このスタイルとコントロール テンプレートが膨大なことを気にしないですください。内容は多いですが、Blend を使うことでビジュアルに編集することが可能だからです。このように、ドキュメントに記述がなくてもツールを使うことで標準コントロールのスタイルとテンプレートの編集を行うことができるようになっています。これも、XAML 系 UI 技術の特徴になっています。

11.14(P580) カスタム コントロール

本節では、カスタム コントロールに適用するスタイルやテンプレートが Themes フォルダーの Generic.xaml というスタイル シートに記述されなければならないことを説明しています。このことを説明するために Petzold.ProgrammingWindows6.Chapter11 プロジェクトにカスタム コントロールを作るという説明になっています。それでは、Petzold.ProgrammingWindows6.Chapter11 プロジェクトの NewToggle.cs を示します。

 using System;
using System.Windows;
using System.Windows.Controls;

namespace Petzold.ProgrammingWindows6.Chapter11
{
    public class NewToggle : ContentControl
    {
        public event EventHandler IsCheckedChanged;
        Button uncheckButton, checkButton;

        static NewToggle()
        {
            CheckedContentProperty = DependencyProperty.Register("CheckedContent",
                typeof(object),
                typeof(NewToggle),
                new PropertyMetadata(null));
            IsCheckedProperty = DependencyProperty.Register("IsChecked",
                typeof(bool),
                typeof(NewToggle),
                new PropertyMetadata(false, OnIsCheckedChanged));
        }

        public NewToggle()
        {
            this.DefaultStyleKey = typeof(NewToggle);
        }

        public static DependencyProperty CheckedContentProperty { private set; get; }

        public static DependencyProperty IsCheckedProperty { private set; get; }
        public object CheckedContent
        {
            set { SetValue(CheckedContentProperty, value); }
            get { return GetValue(CheckedContentProperty); }
        }

        public bool IsChecked
        {
            set { SetValue(IsCheckedProperty, value); }
            get { return (bool)GetValue(IsCheckedProperty); }
        }

        // protected をpublic
        public override void OnApplyTemplate()
        {
            if (uncheckButton != null)
                uncheckButton.Click -= OnButtonClick;
            if (checkButton != null)
                checkButton.Click -= OnButtonClick;
            uncheckButton = GetTemplateChild("UncheckButton") as Button;
            checkButton = GetTemplateChild("CheckButton") as Button;
            if (uncheckButton != null)
                uncheckButton.Click += OnButtonClick;
            if (checkButton != null)
                checkButton.Click += OnButtonClick;
            base.OnApplyTemplate();
        }

        void OnButtonClick(object sender, RoutedEventArgs args)
        {
            this.IsChecked = sender == checkButton;
        }

        static void OnIsCheckedChanged(DependencyObject obj,
                                     DependencyPropertyChangedEventArgs args)
        {
            (obj as NewToggle).OnIsCheckedChanged(EventArgs.Empty);
        }

        protected virtual void OnIsCheckedChanged(EventArgs args)
        {
            VisualStateManager.GoToState(this,
                                         this.IsChecked ? "Checked" : "Unchecked",
                                         true);
            if (IsCheckedChanged != null)
                IsCheckedChanged(this, args);
        }
    }
}

このコードは、OnApplyTemplate メソッドのアクセシビリティを protected から public へ変更しただけになります。これは、WinRT XAML と WPF XAML で OnApplyTemplate メソッドのアクセシビリティが異なることが理由です。それでは、Themes フォルダーの Generic.xaml を示します。

 <ResourceDictionary
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Petzold.ProgrammingWindows6.Chapter11">
    <Style TargetType="local:NewToggle">
        <Setter Property="BorderBrush" Value="Black" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:NewToggle">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CheckStates">
                                <VisualState x:Name="Unchecked" />
                                <VisualState x:Name="Checked">
                                    <Storyboard>
                                        <ThicknessAnimationUsingKeyFrames 
                                                Storyboard.TargetName="UncheckButton"
                                                Storyboard.TargetProperty="(Border.BorderThickness)">
                                            <DiscreteThicknessKeyFrame KeyTime="0"
                                                                       Value="0" />
                                        </ThicknessAnimationUsingKeyFrames>
                                        <ThicknessAnimationUsingKeyFrames 
                                                Storyboard.TargetName="CheckButton"
                                                Storyboard.TargetProperty="(Border.BorderThickness)">
                                            <DiscreteThicknessKeyFrame KeyTime="0"
                                                                    Value="8" />                                        </ThicknessAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <UniformGrid Rows="1">
                            <Button Name="UncheckButton"
                                    Content="{TemplateBinding Content}"
                                    ContentTemplate="{TemplateBinding ContentTemplate}"
                                    FontSize="{TemplateBinding FontSize}"
                                    BorderBrush="Red"
                                    BorderThickness="8"
                                    HorizontalAlignment="Stretch" />

                            <!-- Content="{TemplateBinding CheckedContent}" -->
                            <Button Name="CheckButton"
                                    Content="{Binding RelativeSource={RelativeSource AncestorType=local:NewToggle}, Path=CheckedContent}"
                                    ContentTemplate="{TemplateBinding ContentTemplate}"
                                    FontSize="{TemplateBinding FontSize}"
                                    BorderBrush="Green"
                                    BorderThickness="0"
                                    HorizontalAlignment="Stretch" />
                        </UniformGrid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

このスタイル シートは、次に示す点以外は WinRT XAML と同じになります。

  • 組み込みスタイルを変更。
  • ObjectAnimationUsingKeyFrame を ThicknessAnimationUsingKeyFrames に変更。
    TargetProperty 添付プロパティを 「(Border.BorderThickness)」に変更。
  • local:UniformGrid を UniformGrid に変更。
  • Button の Content を「{Binding RelativeSource={RelativeSource AncestorType=local:NewToggle}, Path=CheckedContent}」に変更。
    WinRT XAML では、作成しているカスタム コントロールの依存関係プロパティを記述できます(CheckedContent)。が、WPF XAML は、このような省略記法が許可されていないので、RelativeSource を使った正式な記述になります。

今度は、NewToggle コントロールを使用する NewToggleDemo プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ... 
        xmlns:ch11="clr-namespace:Petzold.ProgrammingWindows6.Chapter11;assembly=Petzold.ProgrammingWindows6.Chapter11"
        ... >
    <Window.Resources>
        <Style TargetType="ch11:NewToggle">
            <Setter Property="HorizontalAlignment" Value="Center" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
    </Window.Resources>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <ch11:NewToggle Content="Don't do it!"
                        CheckedContent="Let's go for it!"
                        Grid.Column="0" Margin="20"
                        FontSize="24" />
        <ch11:NewToggle Grid.Column="1" Margin="20">
            <ch11:NewToggle.Content>
                <Image Source="Images/MunchScream.jpg" />
            </ch11:NewToggle.Content>
            <ch11:NewToggle.CheckedContent>
                <Image Source="Images/BotticelliVenus.jpg" />
            </ch11:NewToggle.CheckedContent>
        </ch11:NewToggle>
    </Grid>
</Window>

この XAML は、組み込みスタイルを除けば WinRT XAML と同じになります。それでは、実行結果を示します。
NewToggleDemo

ここまでの説明で、コントロールのスタイルとテンプレートというものが、かなりの部分で WinRT XAML と WPF XAML で共通であることが理解できたことでしょう。標準コントロールが持つスタイルの定義そのものは、WinRT XAML と WPF XAML で同じものありますが、Slider コントロールのように異なる考え方で作成されているものもあります。この問題は、提供されるコントロール自体が想定されている利用方法自体が違うことに起因していると考えることができます。この記事で説明した Blend を使用したテンプレートの編集機能を使えば、利用するコントロールのテンプレート編集は容易になることでしょう。考え方は、書籍に記述されていますので、熟読をお願いします。この節の最後としては、ペゾルドの「テンプレートを使用した WPF コントロールのカスタマイズ」という記事に記載されていますが、コントロールのスタイルをダンプするツールが「Applications = Code + Markup」という書籍のサンプルに含まれていますから、このサンプルである DumpControlTemplate を Visual Studio 2013 へ移植したものをサンプルに含めていますので、自分で試してみてください。DumpControlTemplate の実行結果を示します。
DumpControlTemplate

11.15(P586) テンプレートとアイテム コンテナー

本節では、コレクションをバインディングするコントロールで Selector( WinRT XAML では SelectorItem)を継承するコントロールが持つアイテム コンテナーを説明しています。提供するクラス階層などは、WinRT XAML と WPF XAML で異なりますが、考え方は同じなので置き換えて考えることで書籍の説明は両方に当てはまります。それでは、アイテム コンテナー スタイルを指定する CustomListBoxItemStyle プロジェクトの MainWindow.xaml の抜粋を示します。

 <Window ...
        xmlns:ch11="clr-namespace:Petzold.ProgrammingWindows6.Chapter11;assembly=Petzold.ProgrammingWindows6.Chapter11"
        ... >
    <Window.Resources>
        <ch11:NamedColor x:Key="namedColor" />
    </Window.Resources>

    <Grid>
        <ListBox Name="lstbox"
                 ItemsSource="{Binding Source={StaticResource namedColor},
                                       Path=All}"
                 Width="380">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    ...
                </DataTemplate>
            </ListBox.ItemTemplate>

            <ListBox.ItemContainerStyle>
                <Style TargetType="ListBoxItem">
                    <Setter Property="Background" Value="Transparent" />
                    <!--<Setter Property="TabNavigation" Value="True" />-->
                    <Setter Property="Padding" Value="8,10" />
                    <Setter Property="HorizontalContentAlignment" Value="Left" />
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="ListBoxItem">
                                <Border Background="{TemplateBinding Background}"
                                        BorderBrush="{TemplateBinding BorderBrush}"
                                        BorderThickness="{TemplateBinding BorderThickness}">

                                    <VisualStateManager.VisualStateGroups>
                                        <VisualStateGroup x:Name="SelectionStates">
                                            <VisualState x:Name="Unselected">
                                                <Storyboard>
                                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
                                                                                   Storyboard.TargetProperty="(TextElement.FontStyle)">
                                                        <DiscreteObjectKeyFrame KeyTime="0">
                                                            <DiscreteObjectKeyFrame.Value>
                                                                <FontStyle>Normal</FontStyle>
                                                            </DiscreteObjectKeyFrame.Value>
                                                        </DiscreteObjectKeyFrame>
                                                    </ObjectAnimationUsingKeyFrames>
                                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
                                                                                   Storyboard.TargetProperty="(TextElement.FontWeight)">
                                                        <DiscreteObjectKeyFrame KeyTime="0">
                                                            <DiscreteObjectKeyFrame.Value>
                                                                <FontWeight>Normal</FontWeight>
                                                            </DiscreteObjectKeyFrame.Value>
                                                        </DiscreteObjectKeyFrame>
                                                    </ObjectAnimationUsingKeyFrames>
                                                </Storyboard>
                                            </VisualState>
                                            <VisualState x:Name="Selected" />
                                            <VisualState x:Name="SelectedUnfocused" />
                                        </VisualStateGroup>
                                    </VisualStateManager.VisualStateGroups>

                                    <Grid Background="Transparent">
                                        <ContentPresenter x:Name="ContentPresenter"
                                                          TextElement.FontStyle='Italic'
                                                          TextElement.FontWeight='Bold'
                                                          Content='{TemplateBinding Content}'
                                                          ContentTemplate='{TemplateBinding ContentTemplate}'
                                                          HorizontalAlignment='{TemplateBinding HorizontalContentAlignment}'
                                                          VerticalAlignment='{TemplateBinding VerticalContentAlignment}'
                                                          Margin='{TemplateBinding Padding}' />
                                    </Grid>
                                </Border>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>
        <Grid.Background>
            <SolidColorBrush Color='{Binding ElementName=lstbox,
                                             Path=SelectedItem.Color}' />
        </Grid.Background>
    </Grid>
</Window>

この XAML は、次に示す点を除けば WinRT XAML と同じになります。

  • ListBox.ItemContainerStyle で TabNavigation プロパティを削除。
    WinRT XAML 固有のためです。
  • ビジュアル状態、SelectedDisabled、SelectedPointOrver、SelectedPressed を削除。
    WPF XAML に定義されていないためです。
  • ビジュアル状態 Unselected の Storyboard のアニメーションを変更。
    ObjectAnimationUsingKeyFrame の TargetProperty 添付プロパティ を「(TextElement.FontStyle)」と「(TextElement.FontWeight)」に変更。
    DiscreteObjectKeyFrame の Value 属性をプロパティ構文に変更(FontStyles、FontWeights 列挙で値を指定するため)。
  • ContentPresenter に TextElement.FontStyle属性とTextElement.FontWeight 属性を追加。
    WPF XAML の ContentPresenter が FontStyke と FontWeight プロパティをサポートしないためです。
    逆に WinRT XAML の ContentPresenter がサポートしているだけとも言えます。

記述は書籍と同じように省略していますが、DataTemplate は WinRT XAML と同じになります。もちろん、実行結果も同じになります。
CustomListBoxItemStyle

書籍と「テンプレートを使用した WPF コントロールのカスタマイズ」という記事を熟読することで、3 つのテンプレートをより理解することができることでしょう。書籍にも記述されていますが、WPF XAML は XAML UI 技術のフルセットなどので WPF を正しく理解していれば、サブセットである WinRT XAML への応用や移植がし易くなりますので、特にテンプレートを正しく理解するようにしてください。

ここまで説明してきた違いを意識しながら、第11章を読むことで WPF にも書籍の内容を応用することができるようになることでしょう。もちろん、この記事だけで説明していることも理解して頂ければ、WinRT XAML と WPF XAML に役立つことでしょう。

ch11.zip