前回は、Window クラスの役割と、ContentControl、Control、FrameworkElement から継承する機能を見ました。
今回は、さらに基底側にあるクラスを見ていきます。
対象は次の 4 つです。
System.Windows.UIElementSystem.Windows.Media.VisualSystem.Windows.DependencyObjectSystem.Windows.Threading.DispatcherObject
これらは、普段の WPF アプリケーション開発で直接継承する機会は多くありません。しかし、入力イベント、描画、依存関係プロパティ、UI スレッドといった WPF の根本的な仕組みを理解するうえで重要です。
Window の継承階層
Window の代表的な継承階層は次の通りです。
DispatcherObject
└─ DependencyObject
└─ Visual
└─ UIElement
└─ FrameworkElement
└─ Control
└─ ContentControl
└─ Window
前回扱った FrameworkElement、Control、ContentControl は、アプリケーション開発者が普段よく意識する機能を提供していました。
一方、今回扱うクラスは、WPF の内部構造に近い土台です。
| クラス | 主な役割 |
|---|---|
UIElement |
入力、フォーカス、イベント、レイアウト参加、表示状態を扱う |
Visual |
描画、座標変換、ビジュアルツリーの基礎を提供する |
DependencyObject |
依存関係プロパティシステムを提供する |
DispatcherObject |
オブジェクトを UI スレッドの Dispatcher に関連付ける |
System.Windows.UIElement
UIElement は、WPF の UI 要素として振る舞うための基本機能を提供するクラスです。
ここでいう UI 要素とは、画面上に表示され、入力を受け取り、レイアウトに参加し、イベントを発生させる要素のことです。
UIElement の主要メンバーには、次のようなものがあります。
| メンバー | 役割 |
|---|---|
IsEnabled |
要素が操作可能かどうか |
IsVisible |
表示されているかどうか |
Visibility |
Visible、Hidden、Collapsed の表示状態 |
Focusable |
フォーカスを受け取れるかどうか |
IsKeyboardFocused |
キーボードフォーカスを持っているか |
Focus() |
要素にフォーカスを移す |
InvalidateVisual() |
再描画を要求する |
Measure() / Arrange() |
レイアウト計算に関わるメソッド |
MouseDown / MouseUp |
マウス入力イベント |
KeyDown / KeyUp |
キーボード入力イベント |
たとえば、Window でキー入力を受け取りたい場合、KeyDown イベントを使えます。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
KeyDown += (s, e) =>
{
if (e.Key == Key.Escape)
{
Close();
}
};
}
}
このような入力イベントの基礎は、UIElement から提供されています。
Visibility
UIElement の中でもよく使うのが Visibility です。
<TextBlock Text="読み込み中..."
Visibility="Collapsed" />
Visibility には次の 3 つの値があります。
| 値 | 意味 |
|---|---|
Visible |
表示する |
Hidden |
表示しないが、レイアウト上の領域は残す |
Collapsed |
表示せず、レイアウト上の領域も取らない |
Hidden と Collapsed の違いは重要です。
Hidden は見えないだけで場所は確保されます。Collapsed は存在しないように扱われ、周囲の要素が詰めて配置されます。
ルーティングイベント
UIElement は、WPF のイベントシステムとも深く関係します。
WPF には ルーティングイベント という仕組みがあります。通常の .NET イベントと異なり、イベントが要素ツリーを通って親子方向に伝わります。
代表的なルーティングイベントには、次のようなものがあります。
MouseDownPreviewMouseDownKeyDownPreviewKeyDownGotFocusLostFocus
たとえば、Button の中に TextBlock がある場合でも、親要素側でマウスイベントを受け取れます。
<Grid MouseDown="Grid_MouseDown">
<Button Content="OK" />
</Grid>
このようなイベント伝播の仕組みは、WPF の複雑な UI を扱ううえで重要です。
System.Windows.Media.Visual
Visual は、WPF の描画システムの基礎となるクラスです。
Visual は、画面に表示されるオブジェクトとして、描画、座標変換、クリッピング、ヒットテスト、ビジュアルツリーなどに関わります。
主要な役割は次の通りです。
| 役割 | 説明 |
|---|---|
| 描画 | 画面に表示される内容の基礎になる |
| ビジュアルツリー | 表示要素の親子関係を構成する |
| 座標変換 | 親要素、子要素、画面座標間の変換を行う |
| ヒットテスト | 指定した位置にある表示要素を調べる |
| クリッピング | 表示範囲を制限する |
WPF には、論理ツリーとビジュアルツリーという 2 種類のツリーがあります。
論理ツリー は、XAML で定義した要素の意味的な親子関係です。
<Window>
<Grid>
<Button Content="OK" />
</Grid>
</Window>
この場合、論理的には Window の中に Grid があり、その中に Button があります。
一方、ビジュアルツリー は、実際の描画に使われるより細かい親子関係です。Button の中には、テンプレートによって生成された境界線やテキスト表示要素などが含まれます。
Visual は、このビジュアルツリーの基盤になるクラスです。
System.Windows.DependencyObject
DependencyObject は、WPF の依存関係プロパティシステムを利用できるようにする基底クラスです。
WPF では、通常の C# プロパティだけでなく、依存関係プロパティ という特別なプロパティシステムが使われます。
依存関係プロパティは、次のような機能を支えています。
- データバインディング
- スタイル
- アニメーション
- プロパティ値の継承
- 既定値
- 変更通知
- リソース参照
たとえば、Button.Background や Window.Title、FrameworkElement.Width など、多くの WPF プロパティは依存関係プロパティとして実装されています。
依存関係プロパティは、次のように静的フィールドとして登録されます。
public static readonly DependencyProperty MessageProperty =
DependencyProperty.Register(
nameof(Message),
typeof(string),
typeof(MyControl),
new PropertyMetadata("Hello"));
public string Message
{
get => (string)GetValue(MessageProperty);
set => SetValue(MessageProperty, value);
}
GetValue や SetValue は DependencyObject が提供するメソッドです。
object value = GetValue(MessageProperty);
SetValue(MessageProperty, "New Message");
つまり、Window が Title、Width、DataContext、Visibility などを WPF らしく扱えるのは、継承階層の中に DependencyObject があるためです。
System.Windows.Threading.DispatcherObject
DispatcherObject は、WPF オブジェクトを Dispatcher に関連付ける基底クラスです。
WPF の UI は、基本的に UI スレッドで操作する必要があります。別スレッドから直接 UI 要素を操作すると例外が発生します。
// バックグラウンドスレッドから直接 UI を操作すると問題になる
Title = "更新しました";
この制約を支えているのが DispatcherObject と Dispatcher です。
DispatcherObject の主要メンバーは次の通りです。
| メンバー | 役割 |
|---|---|
Dispatcher |
関連付けられた Dispatcher を取得する |
CheckAccess() |
現在のスレッドからアクセス可能か確認する |
VerifyAccess() |
現在のスレッドからアクセスできない場合に例外を投げる |
バックグラウンド処理から UI を更新したい場合は、Dispatcher.Invoke や Dispatcher.BeginInvoke を使って UI スレッドに処理を戻します。
Dispatcher.Invoke(() =>
{
Title = "更新しました";
});
CheckAccess() を使うと、現在のスレッドが UI スレッドかどうかを確認できます。
if (CheckAccess())
{
Title = "更新しました";
}
else
{
Dispatcher.Invoke(() =>
{
Title = "更新しました";
});
}
Window、Button、TextBox などの WPF UI 要素は、この DispatcherObject の性質を持っています。そのため、UI 要素は作成されたスレッドに属し、別スレッドから自由に操作できないようになっています。
なぜこの階層が重要なのか
Window を使うだけなら、継承階層をすべて暗記する必要はありません。
しかし、WPF で起こる多くの疑問は、この階層を見ると理解しやすくなります。
たとえば、次のような疑問です。
- なぜ
WindowにDataContextを設定できるのか - なぜ
ButtonにContentとして任意の UI 要素を入れられるのか - なぜ
VisibilityをCollapsedにするとレイアウトから消えるのか - なぜ別スレッドから UI を更新できないのか
- なぜ XAML のプロパティがバインディングやアニメーションに対応できるのか
これらは、すべて Window 自身だけでなく、基底クラスから受け継いだ仕組みによって実現されています。
まとめ
Window は、WPF アプリケーションのトップレベルウィンドウを表すクラスです。
その下には、WPF の基盤となる重要なクラスが積み重なっています。
UIElement は入力、フォーカス、表示状態、イベントを提供します。Visual は描画とビジュアルツリーの基盤です。DependencyObject は依存関係プロパティを提供し、バインディング、スタイル、アニメーションを支えます。DispatcherObject は、WPF の UI オブジェクトを UI スレッドに関連付けます。
WPF のクラス階層は一見複雑ですが、それぞれの階層は明確な役割を持っています。
Window を理解するときは、単に「画面を表すクラス」と見るだけでなく、「WPF の入力、描画、プロパティ、スレッド管理の仕組みをすべて受け継いだトップレベル要素」と捉えると、全体像がつかみやすくなります。