XAML応用編
前回はXAML基本編ということで、主にXAML->C#の変換ルールを取り上げました。これだけだとXAMLじゃなくてXMLです。ということで、今回はXAML独自の、つまりXMLから拡張された機能について解説します。
マークアップ拡張
前回述べたように、属性に値を設定する場合、その値はリテラルである必要があります。もっと言うと、XAMLにおいてリテラルは文字列しかないので、属性の設定は文字列に限定されます。つまり、<Hoge Piyo="hogepiyo"/>
という書き方しか出来ません。もし、Piyo
属性が文字列で表現できない場合、以下のように書く必要があるのでした。
<Hoge>
<Hoge.Piyo>
<SomethingComplexObject>
...
</SomethingComplexObject>
</Hoge.Piyo>
</Hoge>
で、データバインディングという技術があるわけですが、これはXAMLの属性とC#のプロパティと双方向的に繋げるものでした。System.Windows.Data.Binding
というものを使ってごにょごにょするんですが、プロパティへの単純代入では表現できません。ということで実例を見てみましょう。
TextBlock
のText
属性をViewModelのDescription
プロパティにバインディングするViewがこちらです。
<TextBlock Text="{Binding Description}"/>
これをC#で書くとこんな感じになります。
var textBlock = new textBlock();
textBlock.SetBinding(TextBlock.TextProperty, new Binding("Description"));
というわけで、今までのXAML変換ルールでは対処できません。こういう例外に対応するためのものがマークアップ拡張と呼ばれる構文です。
マークアップ拡張は属性への設定として使用します。<Hoge Piyo="{...}"/>
みたいな感じです。この{}
で囲まれた部分がマークアップ拡張です。データバインディングを使用する際の{Binding Description}
というのはBinding
という名前のマークアップ拡張を使用しますよ、という意味です。で、マークアップ拡張ですが、System.Windows.Markup.MarkupExtension
クラスを継承すればユーザー定義可能です。まあマークアップ拡張をユーザー定義することなんてそうあるものでもないのでここでは端折りますが、Provider
というメソッドに実処理を書くようオーバーライドするだけです。Binding
の場合は先ほど示したようなC#コードを書くだけです。
マークアップ拡張の構文ですが、Attribute="{Markup Argument, Property=OtherArgument}"
みたいな感じです。
XAMLで属性に設定可能な値は出尽くしました。つまり、文字列かマークアップ拡張かのいずれかです。
スタイル
ここまでで何度かWPFをWeb技術に例えながら説明してきました。XAMLはHTMLのようなもので、その文脈においてC#はJavaScriptであり、MVVMはMVC、LivetはRails等MVC系フレームワークといった具合です。もちろん正確には違いますが、まあ、ざっくりこんな感じです。さて、CSSに相当するものはどこあるのでしょう?
ここで、HTMLとXAMLが根本的に異なる技術であるというちゃぶ台返しが必要です。HTMLはSGMLを歴史に持つように、文章記述を目的とした技術です。が、XAMLはアプリケーションのUIを記述するための技術として開発されました。文章表現なんて一切考えていません。ですので、HTMLとCSSが分離されていることはHTMLの理念からして妥当ですが、XAMLの理念からするとむしろUIという単位で統合されてしかるべきものです。もちろんXAMLでも再利用性、コンポーネント性のためにCSSに相当する部分を分離することは可能ですが、その場合でもXAML技術の中に閉じています。
前置きが長くなりましたが、WebのCSSに相当する部分がスタイルです。が、スタイルはCSSよりもかなりリッチです。単純に見た目、例えば色を変えたりも可能ですが、特定のイベントに応じたスタイルの変化も可能ですし、カスタムイベントに対応させることも、イベント発火時の振る舞いをカスタマイズすることも可能です。結局のところXAMLはC#にコンパイルされるわけなので、スタイルの表現力はC#や一般的なプログラミング言語と同等です。
コントロールの種類に対する適用
特定のコントロールの見た目を変更したいケースは多いでしょう。今回はButtonを青くしてみましょう。あ、ここではXML名前空間をずらずら書きたくなかったんで最低限のものしか書いてませんが、試すときはこのままだとInitializeComponent()
の呼び出しで文句言われると思うので名前空間はそのままにしておきましょう。
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="Blue"/>
</Style>
</Window.Resources>
<Button Content="OK" Height="30" Width="80"/>
</Window>
どうでしょう、だいぶと趣味の悪いボタンが表示されたと思います。ボタンをいくつ並べても全部趣味の悪いボタンとして描画されます。これはCSSでいうのところ
button {
background-color: blue;
}
に相当します。
XAMLのスタイルは自分又は親要素のResources
属性の中でStyle
要素を使って定義します。Resources
属性はリソースというXAMLの機能で、複数のコントロールから利用したいようなものを定義する時に使います。リソースは親要素ならどこに書いても良いので、そのView全体で使うならWindow.Resources
に、あるコントロールの子要素でしか適用しないならその親コントロールのResources
に書くのが良いと思います。メンバ変数とローカル変数みたいな感じですね。
Style
要素のTargetType
で指定したコントロールが、その中で定義されたスタイルの適用対象です。見た目等を変更したい場合、Setter
要素を使って対象コントロールのプロパティを直接指定して変更させます。この辺り、XAMLはCSSよりも汎用的なやり方なので応用は利くんですが、ちょっと恣意的でわかりにくい感じですね。あ、Style
要素の中に複数のSetter
を並べられます。また、Resources
属性の中にも複数のStyle
要素を並べられます。
特定のコントロールに対する適用
HTML/CSSではHTML側にid/classを振り、それを元にCSS側で見た目を定義します。が、XAMLでは逆になります。つまり、スタイルにkeyを振り、コントロールで見た目をkeyで指定します。この辺りに文章のHTMLとUIのXAMLという設計理念の差が大きく出ていますね。
スタイル振り分けの例として、2つのボタンの内1つだけ見た目を変えてみます。
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="Blue"/>
</Style>
<Style x:Key="Wraning" TargetType="Button">
<Setter Property="BorderBrush" Value="Red"/>
</Style>
</Window.Resources>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<Button Content="OK" Height="30" Width="80"/>
<Button Content="NO" Height="30" Width="80" Style="{StaticResource Wraning}"/>
</StackPanel>
</Window>
x:Key
でスタイルにkeyを割り当て、StaticResource
マークアップ拡張でリソースを指定します。が、NOボタンのBackgound
がBlue
でなくなっています。これはNOボタンのスタイルがWarning
スタイルになったので、Buttonのスタイルが上書かれてしまったためです。CSSの場合だと、この辺りは適用が勝手に継承されるんですが、XAMLのスタイルは明示的にスタイルの継承を示してやる必要があります。つまり、Warning
スタイルはButton
スタイルを継承している旨を示すと、Button
スタイルへの適用であるBackground
の変更も反映させることができます。スタイルの継承はBasedOn
属性を使います。Warning
スタイルを以下のように書き換えてみてください。
<Style x:Key="Wraning" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="BorderBrush" Value="Red"/>
</Style>
x:Type
は文字通り型を表現するためのもので、まあ、typeof(Button)
という意味です。これで背景色が青色のまま、NOボタンのボーダーだけ赤くなりました。
コントロールに直接スタイルを適用
再利用性を一切考慮しない場合、コントロールのStyle
属性に直接Style
要素を書くことも出来ます。これはHTMLでstyle
属性に直接CSSを書くのと同じです。
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="Blue"/>
</Style>
</Window.Resources>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<Button Content="OK" Height="30" Width="80"/>
<Button Content="NO" Height="30" Width="80">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="BorderBrush" Value="Red"/>
</Style>
</Button.Style>
</Button>
</StackPanel>
</Window>
この時Style
要素にTargetType
属性を書かないといけないのはHTML/CSSと比べると冗長ですね。まあ、直接コントロールにスタイルを書くことがどうなのか、みたいな感じですが。
トリガー
CSS、特にCSS3では様々なイベントが扱えます。代表的なのは:hover
や:checked
でしょうか。スタイルも同様のことができます。
先ほどのサンプルでは背景色が青いボタンを作りましたが、マウスホバー時には元に色に戻ってしまいました。というわけで、マウスホバーが起きても背景色を青いままにしてみましょう。
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="Blue"/>
<Setter.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Blue"/>
</Trigger>
</Setter.Triggers>
</Style>
</Window.Resources>
<Button Content="OK" Height="30" Width="80"/>
</Window>
Trigger
要素はプロパティの変化に応じてSetter
を実行させるものです。Style.Triggers
内に記述します。
さて、実はこれは思うように動きません。この辺りはちょっとややこしいのですが、WPFのコントロールはデフォルトで見た目やいろんなイベントに対する挙動が決まっています。で、今回の場合、Button
のデフォルトの挙動が勝っちゃっている感じです。例えば試しにTrigger
内のSetter
を<Setter Property="Foreground" Value="Blue"/>
と変えてみてください。こうすると背景色はデフォルトに戻っちゃいますが、マウスオーバーするたびにOK/NOの文字が青くなります。つまり、トリガー自体は効いてるわけです。
で、デフォルトの挙動をどう変えるのか、という話ですが、具体的にはControlTemplate
というものを使います。
ControlTemplate
WPFのコントロールは全てXAMLで定義されています。これはWeb系の人にはWeb Componentsみたいなものです、と言えばわかりやすいかもしれません。Control Template
は、Web Componentsで言うところのHTML Templateのようなものです。例えばButtonの中身はこんな感じです。
<ControlTemplate x:Key="CustomButton" TargetType="{x:Type Button}">
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
<ContentPresenter x:Name="contentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsDefaulted" Value="True">
<Setter Property="BorderBrush" TargetName="border" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" TargetName="border" Value="#FFBEE6FD"/>
<Setter Property="BorderBrush" TargetName="border" Value="#FF3C7FB1"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" TargetName="border" Value="#FFC4E5F6"/>
<Setter Property="BorderBrush" TargetName="border" Value="#FF2C628B"/>
</Trigger>
<Trigger Property="ToggleButton.IsChecked" Value="True">
<Setter Property="Background" TargetName="border" Value="#FFBCDDEE"/>
<Setter Property="BorderBrush" TargetName="border" Value="#FF245A83"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" TargetName="border" Value="#FFF4F4F4"/>
<Setter Property="BorderBrush" TargetName="border" Value="#FFADB2B5"/>
<Setter Property="TextElement.Foreground" TargetName="contentPresenter" Value="#FF838383"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Border
があって、内部にContentPresenter
があって、あとはイベント毎にトリガーがある感じですね。Border
は枠線を意味します。ContentPresenter
はButton.Content
の中身です。OKボタンの場合はOKという文字列の事です。
さて、IsMouseOver
に対するトリガーが以下のようになってます。
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" TargetName="border" Value="#FFBEE6FD"/>
<Setter Property="BorderBrush" TargetName="border" Value="#FF3C7FB1"/>
</Trigger>
つまり、マウスオーバー時の色が決め打ちなんですね。トリガー以外の、例えばBorder
のBackground
とかはTemplateBinding
というマークアップ拡張を使ってます。これは、スタイルとかでBackgournd
が変更されたら、その変更を考慮しますよ的なやつです。なので、トリガーでBackground
を変更しても、その変更をTemplateBinding
で受けてくれていない限り、実際に背景色は変わりません。Foreground
は決め打ちで定義されてないので変更されます。
なので、もしマウスオーバー時にButton
の背景色を変えたい場合、ControlTemplate
そのものを上書く必要があります。正直このあたりは標準コントロールの残念なところなんですが、まあ、しょうがないです。
で、コントロールからControlTemplate
を抜き出す方法がVisual Studioに提供されています。Button
要素を選択した状態でプロパティウィンドウからTemplate
プロパティを見つけます。検索バーにTemplateと入力すれば見つかります。Template
プロパティの右側の四角ボタンをクリックし、「新しいリソースに変換」を選択します。適当なキー(例えばCustomButton)を入力してOKを押すと、さきほどお見せしたControlTemplate
がWindow.Resources
内に定義されます。あとはButton要素の属性にTemplate="{DynamicResource CustomButton}"
を追加すれば、指定されたControlTemplateが適用されます。
試しにControlTemplate内のIsMouseOver
に対するトリガーを削除してみましょう。そうすると、マウスオーバーしてもデフォルトの見た目に戻らなくなりました。また、Buttonに対するスタイルでIsMouseOver
のトリガー内でBackground
を変更しても、ちゃんと変更されるようになります。
つまり、ControlTemplateを使うと、見た目を変える程度であればXAML内で完結してしまいます。
まとめ
だいぶと駆け足でXAMLについて紹介してみました。XAMLは闇が深く奥が深く、まだまだ紹介していない機能があるというか、全部紹介するのはちょっと無理なので、あとは困るたびにググりましょう。大抵の場合Stack Overflowに答えが書いてあります。まあ、それでも野心的なUIとかにしなければ、今までの知識でもそこそこのものは作れるかもしれません。多分。
次回もXAMLです。ようやくLivetとBlend SDKが活躍します。多分。