WisdomSoft - for your serial experiences.

3.1 キャンバス

複数の UIElement を画面上に配置するにはパネルを用います。パネルは追加された子要素をレイアウトしますが、どのようにレイアウトするかは派生クラスの実装に依存します。キャンバスはパネルの一種で、指定した座標に子要素を配置します。

3.1.1 複数のコンテンツを保有する

ContentControl クラスのオブジェクトは、任意の UIElement を保有することができますが、保有することができるコンテンツは常に 1 つです。コンテンツは入れ子にすることができるので、コンテンツの中に別のコンテンツを保有させることはできますが、同一の階層内に複数のコンテンツを配置することはできません。

複数の UIElement を並べて表示するには、複数の UIElement を保有し、それらを管理する専用のコントロールを使わなければなりません。WPF では、パネルを利用することで、任意の数のコントロールや図形などの描画要素をレイアウトし、表示することができます。

複数の描画要素などを配置するには System.Windows.Controls.Panel クラスを使います。Panel クラスは、複数のコンテンツを配置するためのコントロールのルートクラスです。

System.Windows.Controls.Panel クラス
System.Object 
   System.Windows.Threading.DispatcherObject 
     System.Windows.DependencyObject 
       System.Windows.Media.Visual 
         System.Windows.UIElement 
           System.Windows.FrameworkElement 
            System.Windows.Controls.Panel
[ContentPropertyAttribute("Children")] 
[LocalizabilityAttribute(LocalizationCategory.Ignore)] 
public abstract class Panel : FrameworkElement, IAddChild

Panel クラスは抽象クラスなので、直接インスタンス化することはできません。すべてのパネルは、Panel 抽象クラスを継承しています。個々の Panel クラスの実装によって、複数の UIElement をどのようにレイアウトするかが決定されます。

Panel クラスは、任意の数の子要素を管理するためのオブジェクトを保有しています。Panel オブジェクトに新しい要素を追加したり、Panel オブジェクトが保有している要素を調べるには Children プロパティから UIElementCollection オブジェクトを取得します。」

Panel クラス Children プロパティ
public UIElementCollection Children { get; }

Children プロパティは、読み取り専用で System.Windows.Controls.UIElementCollection クラスのオブジェクトを返します。

System.Windows.Controls.UIElementCollection クラス
public class UIElementCollection : IList, ICollection, IEnumerable

UIElementCollection クラスは、IList インターフェイスを実装している配列オブジェクトです。Panel は、このオブジェクトで保有する子要素の列を管理しています。Panel オブジェクトにコントロールなどの UIElement を追加するには、Children プロパティから取得した UIElementCollection オブジェクトの Add() メソッドに、追加するオブジェクトと渡します。

UIElementCollection クラス Add() メソッド
public virtual int Add (UIElement element)

element には、パネルに追加する UIElement オブジェクトを渡します。このオブジェクトがどのような形でレイアウトされ、どのように表示されるかは Panel を継承する派生クラスに依存します。

UIElementCollection オブジェクトに追加された UIElement は、インデクサから取得することができます。

UIElementCollection クラスのインデクサ
public virtual UIElement this [int index] { get; set; }

index には、取得する UIElement オブジェクトのインデックスを指定します。これは、配列と同様に 0 番から、要素を追加した順番で並べられています。

追加した要素をコレクションから削除するには、Remove() メソッド、または RemoveAt() メソッドを使います。

UIElementCollection クラス Remove() メソッド
public virtual void Remove (UIElement element)
UIElementCollection クラス RemoveAt() メソッド
public virtual void RemoveAt (int index)

Remove() メソッドの element には、パネルから削除する UIElement オブジェクトを指定します。RemoveAt() メソッドの index には、削除する子要素の 0 番からのインデックスを指定します。

UIElementCollection オブジェクトが保有している要素の数を取得するには Count プロパティを使います。

UIElementCollection クラス Count プロパティ
public virtual int Count { get; }

UIElementCollection クラスは、任意の数の要素を追加できる動的な配列を実現するためのコレクション API に基づいているため、IList インターフェイスや ICollection を実装しているため、コレクション API を利用したい経験があれば、これらのプロパティについては熟知していることでしょう。UIElementCollection クラス自体は、Panel クラスが保有する子要素の配列を管理するためのコレクションに過ぎません。

3.1.2 任意座標への表示

Panel を継承するクラスはいくつかありますが、その中でも最もシンプルなレイアウトを提供するのは System.Windows.Controls.Canvas クラスです。Canvas クラスは、子要素を指定された任意の座標に表示するパネルです。

System.Windows.Controls.Canvas クラス
System.Object 
   System.Windows.Threading.DispatcherObject 
     System.Windows.DependencyObject 
       System.Windows.Media.Visual 
         System.Windows.UIElement 
           System.Windows.FrameworkElement 
             System.Windows.Controls.Panel 
              System.Windows.Controls.Canvas
public class Canvas : Panel

Canvas クラスは Panel クラスを継承しているので、前述した Children プロパティから UIElement を追加することができます。Canvas クラスのコンストラクタは、パラメータを何も受け取らないシンプルなコンストラクタのみが用意されています。

Canvas クラスのコンストラクタ
public Canvas ()

Panel クラスもまた UIElement なので、ContentControl のコンテンツとして設定したり、パネルに追加することができます。最終的には、何らかのパネルを Window オブジェクトの Content プロパティに設定して表示するという形が一般的でしょう。

特に座標を設定しない場合、Canvas クラスは子要素の座標を X 座標、Y 座標ともに 0 として解釈されるため、パネルの左上隅に UIElement が表示されます。

パネルには、任意の UIElement を指定することができますが、この時点では、また UIElement を実装する具体的なクラス郡を紹介していません。コントロールなどの詳細は後ほど解説するので、この場では最も単純な Button クラスを使ってパネルにボタンを表示させてみましょう。Button クラスは、ContentControl クラスを継承しているので Content プロパティなど、これまで解説したいくつかのプロパティをそのまま利用することができます。

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

class Test {
	[STAThread]
	public static void Main() {
		Button button = new Button ();
		button.Content = "ごきげんよう";
		button.FontSize = 30;

		Canvas canvas = new Canvas();
		canvas.Children.Add(button);

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

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

コード1は、Canvas インスタンスを作成して、作成した Canvas オブジェクトの Children プロパティから Button オブジェクトを追加しています。追加したボタンオブジェクトは、コンテンツを表示することができる必要なサイズに初期化されていますが、座標は明示的に指定されています。座標が明示的に指定されていない場合、 パネルの左上隅に表示されます。

追加した UIElement を表示する座標を設定するには、Canvas クラスの静的なメソッドを利用します。UIElement の X 座標を設定するには SetLeft() メソッドを、Y 座標を設定するには SetTop() メソッドを使います。 これらのメソッドは、「2.9 アタッチプロパティ」で紹介したアタッチプロパティを UIElement に設定します。

Canvas クラス SetLeft() メソッド
public static void SetLeft (UIElement element, double length)
Canvas クラス SetTop() メソッド
public static void SetTop (UIElement element, double length)

element には、座標を設定する UIElement オブジェクトを、length には座標を設定します。SetLeft() メソッドは、オブジェクトの左隅を配置する座標を、SetTop() メソッドはオブジェクトの上隅を配置する座標を、それぞれ親パネルの左上隅を基点とした相対座標をで指定します。

Win32 API や .NET の Windows Forms、あるいは Java の AWT や Swing といったライブラリを利用してきた方にとって、SetLeft() や SetTop() メソッドの設計は奇妙に感じるかもしれません。なぜ、UIElement クラスに X や Y プロパティ、または Left や Top プロパティというものが存在しないのか、疑問に思うことでしょう。Canvas クラスの静的な SetLeft() や SetTop() のような煩雑で手続き的な作業よりも、その方が便利であると考えることもできます。

これは、WPF では、従来の考え方とは別に、特定のパネルの子要素となったときにのみ作用する特別なプロパティを、オブジェクトが状況に応じて保有することができるという設計が採用されたためです。つまり、WPF では、UIElement はレイアウトを担当する Panel に追加されたときに自動的に配置されるもので、UIElement 自身が、自分自身をレイアウトするための座標を主張するべきではないのです。

しかし、Canvas クラスでは明示的に配置座標を設定しなければなりませんが、この座標は Canvas オブジェクトの子要素となるときにのみ利用される、Canvas のアタッチプロパティなのです。こうしたプロパティは .NET や C# 言語の仕組みとして用意されたものではないので、WPF で依存プロパティという仕組みで実装されています。依存プロパティの詳細は「2.8 依存プロパティ」で解説しました。

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

class Test {
	[STAThread]
	public static void Main() {
		Button button = new Button();
		button.Content = "ごきげんよう、お姉さま";
		Canvas.SetLeft(button, 100);
		Canvas.SetTop(button, 50);

		Canvas canvas = new Canvas();
		canvas.Children.Add(button);

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

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

コード2では、Canvas に追加する Button オブジェクトの座標を明示的に設定しています。表示されるボタンは、親パネルの左隅から 100 ピクセル、上隅から 50 ピクセルの位置に配置されます。

因みに、UIElement が Canvas 上に配置されている座標が必要な場合は、Canvas クラスの GetLeft() メソッドGetTop() メソッドから取得することができます。

Canvas クラス GetLeft() プロパティ
[TypeConverterAttribute(
	"System.Windows.LengthConverter, PresentationFramework, Version=3.0.0.0,
	Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null"
)] 
[AttachedPropertyBrowsableForChildrenAttribute] 
public static double GetLeft (UIElement element)
Canvas クラス GetTop() プロパティ
[TypeConverterAttribute(
	"System.Windows.LengthConverter, PresentationFramework, Version=3.0.0.0,
	Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null"
)] 
[AttachedPropertyBrowsableForChildrenAttribute] 
public static double GetTop (UIElement element)

それぞれ、パラメータに指定した element が保有する SetLeft() 及び SetTop() メソッドから設定された座標値を返します。

当然、Canvas には複数の UIElement を保有させることができるので、それぞれの UIElement に適切な座標を指定すれば、複数のコントロールや図形を Canvas 上に表示させることができます。 

コード3
using System;
using System.Windows;
using System.Windows.Controls;

class Test {
	[STAThread]
	public static void Main() {
		Button button1 = new Button();
		button1.Content = "Live Alive";
		Canvas.SetLeft(button1, 100);
		Canvas.SetTop(button1, 50);

		Button button2 = new Button();
		button2.Content = "Some day in the rain";
		Canvas.SetLeft(button2, 150);
		Canvas.SetTop(button2, 100);

		Canvas canvas = new Canvas();
		canvas.Children.Add(button1);
		canvas.Children.Add(button2);

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

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

コード3は、Canvas 上に 2 つのボタンを表示しています。このように、Panel を継承するクラスを利用することで、同一の階層上に複数の子要素を配置させることができます。

3.1.3 Zインデックス

Canvas オブジェクトは、コントロールを指定した座標に明示的に配置することができるため、UIElement に設定されている座標とサイズによっては、複数の UIElement が互いに重なり合って表示される可能性があります。

図1 重なり合う要素
重なり合う要素

図1は、ボタンコントロールが互いに重なっている状態の Canvas です。このように、座標やサイズによっては、描画する要素が互いに重なり合ってしまうことがあります。

既定では、このような場合は Canvas に追加した順番に表示する優先順位が決定します。最後に追加した要素が最も手前に表示され、最初に追加した要素は後に追加した要素の後ろに隠されます。

このような重ね合わせの順序は、UIElement の Z インデックスを変更することで明示的に制御することができます。Z インデックスを設定するには Canvas クラスの SetZIndex() メソッドを使い、現在設定されている Z 順序を取得するには GetZIndex() メソッドを使います。

Canvas クラス SetZIndex() メソッド
public static void SetZIndex (UIElement element,int value)
Canvas クラス GetZIndex() メソッド
public static int GetZIndex (UIElement element)

element パラメータには、Z インデックスを設定する UIElement オブジェクトを指定します。SetZIndex() の value パラメータには、設定する Z インデックスの値を指定します。Z インデックスは int 型の数値で表現され、より値が大きいほど手前にあると解釈されます。

コード4
using System;
using System.Windows;
using System.Windows.Controls;

class Test {
	[STAThread]
	public static void Main() {
		Button button1 = new Button();
		button1.Content = "Melancholy";
		button1.FontSize = 40;
		Canvas.SetLeft(button1, 10);
		Canvas.SetTop(button1, 10);
		Canvas.SetZIndex(button1, 0);

		Button button2 = new Button();
		button2.Content = "Indignation";
		button2.FontSize = 40;
		Canvas.SetLeft(button2, 100);
		Canvas.SetTop(button2, 30);
		Canvas.SetZIndex(button2, 2);

		Button button3 = new Button();
		button3.Content = "Boredom";
		button3.FontSize = 40;
		Canvas.SetLeft(button3, 200);
		Canvas.SetTop(button3, 50);
		Canvas.SetZIndex(button3, 1);

		Canvas canvas = new Canvas();
		canvas.Children.Add(button1);
		canvas.Children.Add(button2);
		canvas.Children.Add(button3);

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

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

コード4は、重なり合うボタンコントロールの順番を SetZIndex() メソッドを用いて明示的に指定したプログラムです。ボタンコントロールは button1、button2、button3 オブジェクトの順番で追加しているので、本来ならば、最後に追加した "Boredom" ボタンが最前面に表示されますが、SetZIndex() によって "Indignation" ボタンが最前面に配置されています。