XAML: Templateについて その2
やっとこさ ControlTemplate が終わったんでまとめる。
目次
まずは簡単なおさらいからで、ControlTemplate でできることは「外観の再定義」である。
DataTemplate でも外観の変更はできるのだけど、ControlTemplate はバインディング等も改造できるので、こちらの方ができることが多い。
DataTemplate は、バインディングする際に親定義から探してバインドするといったことができない。
つまり、TemplateBinding や RelativeSource の Mode=TemplatedParent が使えない。 ControlTemplate は Template プロパティに対して定義していく。 まずは 簡単に ControlTemplate を適用したものを載せる。 上記の xaml コードは以下のような結果になる。
見てわかるとおり、ただの TextBlock にしか見えない。たとえ Button 側に BorderBrush や BorderThickness を設定しても、うまいこと ControlTemplate に反映してくれるわけではないということである。しかも表示されているテキストは Button 側に設定したものではない。これではボタンとして見てわからないので、当然改造しなければならない。
かといって、ControlTemplate 内に定義している Border 等に決め決めで設定しても、その色やテキストしか使えず、使い物にならない。 これを解決するのが何度か出てきている TemplateBinding である。 TemplateBinding は前章で説明した内容を解決するものである。これは、「ControlTemplate を適用するコントロールのプロパティをバインディングソースにする」ということである。 つまり、TemplateBinding を使用すると下記のことを実現できる。 もちろん、上記以外にもいろいろ実現できる。HorizontalAlignment も VerticalAlignment もバインディングソースにできる。
Button 側に設定した内容を ControlTemplate に引き継げるのである。 xaml コードを見ていく。 そして、適用した結果の画像が下記である。
見てのとおり、枠線の色と太さ、それとテキストが見事に反映されている。
ここで、Text へのバインディングが TemplateBinding でないことに気が付く。
これは、Buttun の Content プロパティを TwoWay でバインディングしなければいけないのが理由である。
TemplateBinding は OneWay のバインディングを前提としており、TwoWay のバインディングができないためだ。
そして、TwoWay でのバインディングかつ TemplateBinding と同じ効果をもたらすのが RelativeSource による Binding というわけである。
正確に書くと、「Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Mode=TwoWay, Path=Content」といったように、Mode の選択が可能なのである。 ControlTemplate にバインドするプロパティが TwoWay でバインドしないといけないのかを見極める必要がある。
なお、OneWay の場合は RelativeSource でもよい。「Binding RelativeSource={RelativeSource Mode=TemplatedParet}, Mode=OneWay, Path=....」の省略形が「TemplateBinding」なのである。
(なお、ぶっちゃけ TwoWay が必要なやつで TemplateBinding するとエラーになったりするのでそのタイミングで RelativeSource の書き方を試せばいい・・・) しかし、ここで気づくかもしれないが、実は外観はできても「ボタンを押したときの動作が見えない」のだ。
これは VisualState という状態遷移を考慮していないためである。この状態遷移を考慮するうえで必要なのが VisualStateManager だ。 VisualStateManager は外観状態を管理するものである(そのまんま)。注意したいのが、この VisualState はコントロールごとにあること。
今回の例に挙げ続けている Button コントロールは2グループの VisualStateGroup からなる。 すべての状態を厳密に決める必要はない。特に動作がなくていい状態は空の指定ができるためだ。
そして、これまでの xaml コードはボタンの Template を直接いじくってきたが、ここにきてリソースにおけるように Style として定義していく。
Style としてリソースに定義しておけば、すべてのボタンに対して同じ外観、VisualState を反映できる。 以下が Style の xaml コードだ。一気に長くなるが、下記の実装になる。 xaml コードを見ればわかるが、VisualState はプロパティの値を上書きしているだけである。なお、この上書きに対してはアニメーションを用いることが可能である。今回はアニメーションを使わず、瞬時にプロパティを上書きしていく。 このスタイルを使って簡単なデモをする。 デモ内容 まずは結果から。
ちゃんとデモ内容どおりに外観が変化していることがわかる。
VisualSteteManager で状態を再定義してやれば、馴染みの動作になるというわけである。
なお、真ん中のボタンを押したときの画像は割愛する。 このデモに使った xaml コードと C# コードも載せておく。 xaml コード(スタイル部分は省略している。) C# コード 次はこの ControlTemplate の知識を使って「カスタムコントロール」を作っていく。
この知識があって初めて、カスタムコントロールが自在に作れるようになるのだ。ControlTemplate でできること
基本的な ControlTemplate
<Button Content="Click me!"
Click="Button_Click"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Grid.Row="0"
Grid.Column="1"
BorderBrush="Red"
BorderThickness="2">
<Button.Template>
<ControlTemplate>
<Border>
<TextBlock Text="temporary"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
TemplateBinding
<Button Content="Click me!"
Click="Button_Click"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Grid.Row="0"
Grid.Column="2"
BorderBrush="Red"
BorderThickness="2">
<Button.Template>
<ControlTemplate>
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<TextBlock Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Content}"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
VisualStateManager
<Style x:Key="btnStyle3" TargetType="Button">
<Setter Property="BorderBrush" Value="Red"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="FontSize" Value="24"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="12">
<!--#region 状態定義 -->
<!-- 各状態の定義がないと遷移できない。 -->
<!-- また、この定義はすべて書いておかないといけない。なお、不要なものはタグを空にしておけばよい。 -->
<!-- さらには、定義場所も注意がいる。下手すると全く反映されない。 -->
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver"/>
<!-- ボタン押下時の色変更 -->
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="border"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0:0:0">
<DiscreteObjectKeyFrame.Value>
<SolidColorBrush Color="LightBlue"/>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<!-- 操作不可時、灰色の Rectangle で覆う -->
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="disabledRect"
Storyboard.TargetProperty="Visibility">
<!-- DiscreteObjectKeyFrame : 特定の値に直接変更するやつ -->
<!-- キーフレームは、アニメーションが開始されてから特定の時間が経過したときのターゲットプロパティの値を示す -->
<DiscreteObjectKeyFrame KeyTime="0:0:0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="FocusedStates">
<VisualState x:Name="Unfocused"/>
<!-- フォーカスした際、瞬時に点線の透明度を 1 にするアニメーション -->
<VisualState x:Name="Focused">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="focusRectangle"
Storyboard.TargetProperty="Opacity"
To="1" Duration="0"/>
</Storyboard>
</VisualState>
<VisualState x:Name="PointerFocused"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<!--#endregion-->
<Grid>
<!-- 内容の本体 -->
<ContentPresenter Name="contentpresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"/>
<!-- フォーカス時の点線用Rectangle -->
<Rectangle Name="focusRectangle"
Opacity="0"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="1"
StrokeDashArray="2 2"
Margin="4"
RadiusX="12"
RadiusY="12"/>
<!-- 操作不可時の覆い隠す用Rectangle -->
<Rectangle Name="disabledRect"
Fill="Black"
Visibility="Collapsed"
Opacity="0.5"
RadiusX="12"
RadiusY="12"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Button Content="Disable right btn"
Click="Button_Click_1"
Grid.Column="0"
Padding="12"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource btnStyle3}"/>
<Button Name="operateBtn"
Content="btn"
Click="Button_Click"
Grid.Column="1"
Padding="12"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource btnStyle3}"/>
<Button Content="Enable left btn"
Click="Button_Click_2"
Grid.Column="2"
Padding="12"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource btnStyle3}"/>
</Grid>
public partial class ButtonTemplateDemo : Window
{
public ButtonTemplateDemo()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
System.Diagnostics.Debug.WriteLine("Click!");
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
operateBtn.IsEnabled = false;
}
private void Button_Click_2(object sender, RoutedEventArgs e)
{
operateBtn.IsEnabled = true;
}
}
まとめ