Posted at

Livetで始めるWPF(ざっくり)入門 その5

More than 3 years have passed since last update.


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というものを使ってごにょごにょするんですが、プロパティへの単純代入では表現できません。ということで実例を見てみましょう。

TextBlockText属性を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ボタンのBackgoundBlueでなくなっています。これは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の中身はこんな感じです。


button.xaml

<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は枠線を意味します。ContentPresenterButton.Contentの中身です。OKボタンの場合はOKという文字列の事です。

さて、IsMouseOverに対するトリガーが以下のようになってます。

<Trigger Property="IsMouseOver" Value="True">

<Setter Property="Background" TargetName="border" Value="#FFBEE6FD"/>
<Setter Property="BorderBrush" TargetName="border" Value="#FF3C7FB1"/>
</Trigger>

つまり、マウスオーバー時の色が決め打ちなんですね。トリガー以外の、例えばBorderBackgroundとかはTemplateBindingというマークアップ拡張を使ってます。これは、スタイルとかでBackgourndが変更されたら、その変更を考慮しますよ的なやつです。なので、トリガーでBackgroundを変更しても、その変更をTemplateBindingで受けてくれていない限り、実際に背景色は変わりません。Foregroundは決め打ちで定義されてないので変更されます。

なので、もしマウスオーバー時にButtonの背景色を変えたい場合、ControlTemplateそのものを上書く必要があります。正直このあたりは標準コントロールの残念なところなんですが、まあ、しょうがないです。

で、コントロールからControlTemplateを抜き出す方法がVisual Studioに提供されています。Button要素を選択した状態でプロパティウィンドウからTemplateプロパティを見つけます。検索バーにTemplateと入力すれば見つかります。Templateプロパティの右側の四角ボタンをクリックし、「新しいリソースに変換」を選択します。適当なキー(例えばCustomButton)を入力してOKを押すと、さきほどお見せしたControlTemplateWindow.Resources内に定義されます。あとはButton要素の属性にTemplate="{DynamicResource CustomButton}"を追加すれば、指定されたControlTemplateが適用されます。

試しにControlTemplate内のIsMouseOverに対するトリガーを削除してみましょう。そうすると、マウスオーバーしてもデフォルトの見た目に戻らなくなりました。また、Buttonに対するスタイルでIsMouseOverのトリガー内でBackgroundを変更しても、ちゃんと変更されるようになります。

つまり、ControlTemplateを使うと、見た目を変える程度であればXAML内で完結してしまいます。


まとめ

だいぶと駆け足でXAMLについて紹介してみました。XAMLは闇が深く奥が深く、まだまだ紹介していない機能があるというか、全部紹介するのはちょっと無理なので、あとは困るたびにググりましょう。大抵の場合Stack Overflowに答えが書いてあります。まあ、それでも野心的なUIとかにしなければ、今までの知識でもそこそこのものは作れるかもしれません。多分。

次回もXAMLです。ようやくLivetとBlend SDKが活躍します。多分。