WisdomSoft - for your serial experiences.

7.6 テンプレート

WPF では UIElement の構造をテンプレートとしてデータ化でき、任意のコントロールに設定できます。テンプレートを用いることで、既存のコントロールの挙動を変更することなく構造を変更できます。この仕組みはカスタムのコントロール作成などに応用されます。

7.6.1 要素構造をデータ化する

まず、誤解を避けるために先に説明しなければならないことがあります。この場で説明するテンプレートとは C++ などのプログラミング言語で用いられる構文におけるテンプレートとはまったく異なるものです。WPF では、データやコントロールの描画構造をデータ化する手段として、テンプレートと呼ばれる仕組みが導入されています。

スタイルは、オブジェクトの依存プロパティに設定する値を統一化するための仕組みですが、スタイル自身がオブジェクトを生成したり、操作を行うことはできません。スタイルは、オブジェクトの状態を情報として提供するだけでした。

これに対して、テンプレートはオブジェクトの構造を提供するデータです。テンプレートを使用することで、コントロールが描画するコンテンツの構造を提供することができ、コントロールはテンプレートの情報に従ってコンテンツをインスタンス化して設定する仕組みを提供しています。コントロールに対してテンプレートを設定するには Template プロパティを使用します。

Control クラス Template プロパティ
public ControlTemplate Template { get; set; }

Template プロパティには、コントロールの描画構造を提供する System.Windows.Controls.ControlTemplate クラスのオブジェクトを提供します。テンプレートの基本的な構造は基底クラスである System.Windows.FrameworkTemplate クラスで提供されていますが、ControlTemplate クラスはコントロールに対する描画構造を提供する専用のクラスです。

System.Windows.Controls.ControlTemplate クラス
System.Object 
   System.Windows.Threading.DispatcherObject 
     System.Windows.FrameworkTemplate 
      System.Windows.Controls.ControlTemplate
[LocalizabilityAttribute(LocalizationCategory.None, Readability=Readability.Unreadable)] 
publicclass ControlTemplate : FrameworkTemplate

このクラスのコンストラクタはオーバーロードされています。このクラスのコンストラクタは、以下のようなものがあります。

ControlTemplate クラスのコンストラクタ
public ControlTemplate ()
public ControlTemplate (Type targetType)

targetType には、このテンプレートが対象とするコントロールの型を指定します。コンストラクタで指定するこの型は TargetType プロパティから設定・取得することも可能です。

ControlTemplate クラス TargetType プロパティ
public Type TargetType { get; set; }

TargetType の型と、テンプレートが設定されたコントロールの型が異なる場合は例外が発生します。

テンプレートが提供する描画構造は VisualTree プロパティに設定します。このプロパティから、コントロールのコンテンツとするクラスの型はプロパティの設定情報を提供します。

FrameworkTemplate クラス VisualTree プロパティ
public FrameworkElementFactory VisualTree { get; set; }

VisualTree プロパティには、System.Windows.FrameworkElementFactory クラスのオブジェクトを設定します。FrameworkElementFactory クラスは、テンプレートのデータを提供するためのクラスです。

System.Windows.FrameworkElementFactory クラス
[LocalizabilityAttribute(LocalizationCategory.NeverLocalize)] 
public class FrameworkElementFactory

FrameworkElementFactory クラスは、その名の通りテンプレートを設定されるオブジェクトが、テンプレートが表す描画構造にしたがって FrameworkElement オブジェクトを生成するための情報を提供します。

このクラスのコンストラクタはオーバーロードされています。

FrameworkElementFactory クラスのコンストラクタ
public FrameworkElementFactory ()
public FrameworkElementFactory (Type type)

type パラメータには、この FrameworkElementFactory が表す描画要素となる FrameworkElement 型に互換性のある型を指定します。例えば、ControlTemplate テンプレートで楕円形のコントロールを定義させたい場合は Ellipse 型を設定することになるでしょう。

コンストラクタの type パラメータに指定した値は Type プロパティから設定・取得することができます。

FrameworkElementFactory クラス Type プロパティ
public Type Type { get; set; }

テンプレートが設定されたコントロールは、この Type プロパティに設定されている要素をインスタンス化して自身のコンテンツとして表示しようと試みます。しかし、型情報だけではインスタンス化したオブジェクトのプロパティが適切に設定されていないため、適切な値を設定してあげなければなりません。テンプレートで定義した要素のプロパティを設定するには SetValue() メソッドを使います。

FrameworkElementFactory クラス SetValue() メソッド
public void SetValue (DependencyProperty dp, Object value)

dp パラメータには設定する対象の依存プロパティを、value には設定する値を指定します。例えば、FrameworkElementFactory オブジェクトが Ellipse クラスを表している場合、Shape.FillProperty 依存プロパティを dp パラメータに指定し、value パラメータに Brushes.Red を設定すれば、赤い Ellipse オブジェクトがテンプレートの内容となります。

コード1
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

class Test {
	[STAThread]
	public static void Main() {
		FrameworkElementFactory root = new FrameworkElementFactory();
		root.Type = typeof(Ellipse);
		root.SetValue(Shape.FillProperty, Brushes.Red);

		ControlTemplate template = new ControlTemplate();
		template.VisualTree = root;

		Control ctrl = new Control();
		ctrl.Template = template;

		Window wnd = new Window();
		wnd.Content = ctrl;

		Application app = new Application();
		app.Run(wnd);
	}
}
実行結果
コード1 実行結果

コード1は、Ellipse オブジェクトを作成して表示しているわけではありません。赤い楕円形を定義するテンプレートを先に作成し、このテンプレートをコントロールに適用することで赤い楕円形を表示しています。このプログラムでは Control オブジェクトが内部でテンプレートの情報に従って自身のコンテンツをインスタンス化して設定しています。

こうしてみるとテンプレートは何に役立つのか理解し難いかもしれませんが、テンプレートを使用することで、複数のコントロール上で同一の独自コンテンツを表示させることができるようになります。これは、新しいコントロールのデザインを外部から実行時に設定することができることを表しています。プラグイン可能なデザインを提供するアプリケーションなどは、この仕組みを積極的に使うことで柔軟に対応することができます。

しかし、FrameworkElementFactory を定義してテンプレートを構築する作業は生産的ではありません。スタイルやテンプレートは、後に説明する XAML と呼ばれるツールのために存在するものなので、プログラムからインスタンス化して作成することも可能ですが、本来は XAML 上で利用するためのクラスだと考えてください。

7.6.2 テンプレートの階層化

テンプレートは、XAML で使われることが前提なので構造的に記述することができます。テンプレートが保有するデータである FrameworkElementFactory に親子関係を設定して木構造を表現することができます。ある FrameworkElementFactory オブジェクトに子 FrameworkElementFactory オブジェクトを設定するには AppendChild() メソッドを使います。

FrameworkElementFactory クラス AppendChild() メソッド
public void AppendChild (FrameworkElementFactory child)

child パラメータには子となる FrameworkElementFactory オブジェクトを設定します。これによって、テンプレートを設定するコントロールのコンテンツに、パネルなどに含まれる複数の描画要素を配置させることができます。

コード2
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

class Test {
	[STAThread]
	public static void Main() {
		FrameworkElementFactory child1 = new FrameworkElementFactory();
		child1.Type = typeof(Button);
		child1.SetValue(ContentControl.ContentProperty, "リリカル");

		FrameworkElementFactory child2 = new FrameworkElementFactory();
		child2.Type = typeof(Button);
		child2.SetValue(ContentControl.ContentProperty, "マジカル");

		FrameworkElementFactory root = new FrameworkElementFactory();
		root.Type = typeof(StackPanel);
		root.AppendChild(child1);
		root.AppendChild(child2);

		ControlTemplate template = new ControlTemplate();
		template.VisualTree = root;

		Control ctrl = new Control();
		ctrl.Template = template;

		Window wnd = new Window();
		wnd.Content = ctrl;

		Application app = new Application();
		app.Run(wnd);
	}
}
実行結果
コード2 実行結果

コード2では、2 つの Button オブジェクトを含む StackPanel を定義するテンプレートを作成しています。テンプレートが保有する FrameworkElementFactory オブジェクトは StackPanel を定義していますが、さらに、このオブジェクトに AppendChild() メソッドから Button オブジェクトを定義する子 FrameworkElementFactory オブジェクトを追加しています。

7.6.3 テンプレートのバインディング

テンプレートは、描画構造を定義して複数のコントロールで共有することができますが、テンプレートによる描画構造の定義は静的なものです。FrameworkElementFactory オブジェクトの SetValue() メソッドで指定した値は、全てのコントロールで同じ値が使われるため汎用性がありません。この問題を解決するには、テンプレートで定義する要素に対してバインディングを行います。

ただし、テンプレートが定義している描画要素はインスタンス化されていないので、これまでのバインディングの方法をそのまま使うことはできません。例えば、テンプレートで定義している Ellipse オブジェクトの背景色を、ControlTemplate オブジェクトを設定するコントロールの Background プロパティにバインディングしたい場合、Ellipse オブジェクトはインスタンス化されていないのでターゲットに設定できません。また、テンプレートの親となるコントロールも、テンプレート定義時に決定することはできません。

テンプレートで定義した要素を、テンプレートを保有するコントロールと関連付けるには Binding クラスの RelativeSource プロパティを使用します。RelativeSource プロパティは、バインディングソースを固有のオブジェクトで設定するのではなく、バインディングターゲットから相対的に配置されているオブジェクトを表します。

Binding クラス RelativeSource プロパティ
public RelativeSource RelativeSource { get; set; }

RelativeSource プロパティに設定するのは System.Windows.Data.RelativeSource クラスのオブジェクトです。

System.Windows.Data.RelativeSource クラス
System.Object 
   System.Windows.Markup.MarkupExtension 
    System.Windows.Data.RelativeSource
[MarkupExtensionReturnTypeAttribute(typeof(RelativeSource))] 
public class RelativeSource : MarkupExtension, ISupportInitialize

RelativeSource クラスは、バインディングソースの相対座標を表現するためのクラスです。コンストラクタからインスタンスを生成することも可能ですが、通常、このクラスのインスタンスは静的なプロパティから取得します。

テンプレートを保有している親コントロール(テンプレートが使用される要素)をバインディングソースとしたい場合、TemplatedParent プロパティが返す RelativeSource を使います。

RelativeSource クラス TemplatedParent プロパティ
public static RelativeSource TemplatedParent { get; }

このプロパティが返したオブジェクトを Binding クラスの RelativeSource プロパティに設定することで、このバインディングオブジェクトのソースはテンプレートを設定する要素となります。よって、テンプレートにしたがってインスタンス化を行うコントロールごとに、異なるソースが相対的に与えられることになります。

次に、テンプレートが定義するオブジェクトをターゲットに設定する必要がありますが、これはインスタンス化されていないため直接ターゲットとすることはできません。テンプレートが定義するオブジェクトをターゲットに設定するには FrameworkElementFactory クラスの SetBinding() メソッドを使います。

FrameworkElementFactory クラス SetBinding() メソッド
public void SetBinding (DependencyProperty dp, BindingBase binding)

dp パラメータには、対象となる依存プロパティを、binding にはバインディングオブジェクトを指定します。このメソッドは、FrameworkElementFactory が定義している要素に対してバインディングを行います。このように、バインディングを設定することでより柔軟なテンプレートを実現することができます。

コード3
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Shapes;

class Test {
	[STAThread]
	public static void Main() {
		Binding bind = new Binding("Background");
		bind.RelativeSource = RelativeSource.TemplatedParent;
		
		FrameworkElementFactory root = new FrameworkElementFactory();
		root.Type = typeof(Ellipse);
		root.SetBinding(Ellipse.FillProperty, bind);

		ControlTemplate template = new ControlTemplate();
		template.VisualTree = root;

		Control ctrl = new Control();
		ctrl.Background = Brushes.Black;
		ctrl.Template = template;

		Window wnd = new Window();
		wnd.Content = ctrl;

		Application app = new Application();
		app.Run(wnd);
	}
}
実行結果
コード3 実行結果

コード3は、親コントロールの Background プロパティの値で楕円の内部を塗りつぶすように、楕円形を定義するテンプレートの FrameworkElementFactory オブジェクトにバインディングを設定しています。コントロールにはテンプレートを設定しているため楕円が表示されますが、この楕円を塗りつぶす色はコントロールの Background プロパティで決定されます。

この結果から、バインディングを試用することで同一のテンプレートでも設定するコントロールによって異なる結果を表示することができるようになることがわかります。