WPF アーキテクチャ

このトピックは、Windows Presentation Foundation (WPF) のクラス階層のガイド ツアーです。 WPF の主要なサブシステムの大部分と、それらの相互作用について説明します。 また、WPF のアーキテクトが行ういくつかの選択についても詳しく説明します。

System.Object

WPF の主要なプログラミング モデルは、マネージド コードによって公開されます。 WPF の設計フェーズの初期段階では、システムのマネージド コンポーネントとアンマネージド コンポーネントの間のどこに線を引くかについていくつかの論争がありました。 CLR には、開発の生産性と堅牢性を高めるさまざまな機能 (メモリ管理、エラー処理、共通型システムなど) がありますが、それらにはコストが伴います。

次の図では、WPF の主要なコンポーネントが示されています。 図の赤い部分 (PresentationFramework、PresentationCore、milcore) は、WPF の主要なコード部分です。 これらのうち、milcore だけがアンマネージド コンポーネントです。 milcore は、DirectX との緊密な統合を可能にするためにアンマネージド コードで記述されています。 WPF でのすべての表示は、DirectX エンジンによって行われるため、ハードウェアとソフトウェアで効率的にレンダリングできます。 WPF では、メモリと実行を細かく制御する必要もあります。 milcore の合成エンジンは、パフォーマンスに大きな影響を与えるため、パフォーマンスを高めるには CLR の多くの利点を放棄する必要があります。

The position of WPF within the .NET Framework.

WPF のマネージド部分とアンマネージド部分の間の通信については、このトピックで後ほど説明します。 以下ではマネージド プログラミング モデルの残りの部分について説明します。

System.Threading.DispatcherObject

WPF のほとんどのオブジェクトは、コンカレンシーとスレッド処理を行うための基本的なコンストラクトを提供する DispatcherObject から派生します。 WPF は、ディスパッチャーによって実装されるメッセージング システムが基になっています。 これは、馴染みのある Win32 メッセージ ポンプとよく似ています。実際、WPF ディスパッチャーでは、クロス スレッド呼び出しを実行するために User32 メッセージが使用されます。

WPF でのコンカレンシーに関しては、ディスパッチャーとスレッド アフィニティという 2 つの主要概念を理解しておく必要があります。

WPF の設計フェーズでの目標は、単一の実行スレッドではあってもスレッド "アフィニティ" ではないモデルに移行することでした。 スレッド アフィニティは、実行中のスレッドの ID を使用してコンポーネントで何らかの種類の状態が格納されると発生します。 その最も一般的な形式は、スレッド ローカル ストア (TLS) を使用して状態を格納することです。 スレッド アフィニティでは、実行の各論理スレッドが、オペレーティング システムの 1 つの物理スレッドによって所有されている必要があり、これによりメモリ集中型になる可能性があります。 最終的に、WPF のスレッド モデルは、スレッド アフィニティのある単一スレッド実行という既存の User32 スレッド モデルとの同期が維持されました。 このようになった主な理由は相互運用性でした。OLE 2.0、クリップボード、Internet Explorer のようなシステムではすべて、単一スレッド アフィニティ (STA) の実行が必要になります。

STA スレッドを使用するオブジェクトがある場合は、スレッド間で通信を行い、正しいスレッドであることを検証する手段が必要です。 ここでにディスパッチャーの役割があります。 ディスパッチャーは、優先度付けされた複数のキューを使用する基本的なメッセージ ディスパッチ システムです。 メッセージの例としては、生の入力通知 (マウス移動)、フレームワーク機能 (レイアウト)、ユーザー コマンド (このメソッドの実行) などがあります。 DispatcherObject から派生することにより、STA の動作を持つ CLR オブジェクトを作成し、作成時にディスパッチャーへのポインターを提供されます。

System.Windows.DependencyObject

WPF の構築で考慮されたアーキテクチャに関する主要な思想の 1 つは、メソッドまたはイベントよりプロパティを優先することでした。 プロパティは宣言型であり、アクションではなく意図を簡単に指定できます。 これは、また、ユーザー インターフェイスのコンテンツの表示に対して、モデル駆動型またはデータ駆動型のシステムもサポートしていました。 この哲学には、アプリケーションの動作をより適切に制御するために、バインドできるプロパティをより多く作成するという意図された効果がありました。

システムをよりプロパティ駆動型にするには、CLR によって提供されるものより機能の豊富なプロパティ システムが必要でした。 このような追加機能の簡単な例は変更通知です。 双方向のバインドを有効にするには、バインドの両側で変更通知がサポートされている必要があります。 動作をプロパティ値に結び付けるには、プロパティ値が変更されたときに通知を受け取る必要があります。 Microsoft .NET Framework の INotifyPropertyChange インターフェイスを使用すると、オブジェクトで変更通知を発行できますが、これはオプションです。

WPF では、DependencyObject 型から派生した機能豊富なプロパティ システムが用意されています。 プロパティ システムは、プロパティ式間の依存関係を追跡し、依存関係が変化したときはプロパティ値を自動的に再検証するという点で、実際には "依存関係" プロパティ システムです。 たとえば、継承するプロパティがある場合 (FontSize など)、値を継承する要素の親でプロパティが変更されると、システムによって自動的に更新されます。

WPF プロパティ システムの基礎は、プロパティ式の概念です。 WPF のこの最初のリリースでは、プロパティ式システムは閉じられており、式はすべてフレームワークの一部として提供されています。 プロパティ システムでデータ バインディング、スタイル設定、または継承がハード コーディングされるのではなく、後でフレームワーク内のレイヤーによって提供されるのは、式のためです。

プロパティ システムでは、プロパティ値のスパース ストレージも提供されます。 オブジェクトには (何百とまではいかなくても) 何十ものプロパティがある場合があり、ほとんどの値は既定の状態 (継承、スタイルによる設定など) であるため、オブジェクトのすべてのインスタンスで、定義されているすべてのプロパティを重視する必要はありません。

プロパティ システムの最後の新機能は、添付プロパティの概念です。 WPF の要素は、合成とコンポーネントの再利用の原則に基づいて構築されています。 多くの場合、含む側の要素 (Grid レイアウト要素など) には、子要素の動作を制御するために、子要素についての追加データが必要です (行または列の情報など)。 これらのすべてのプロパティをすべての要素と関連付けるのではなく、任意のオブジェクトで他のすべてのオブジェクトに対してプロパティ定義を提供できます。 これは、JavaScript の "expando" 機能に似ています。

System.Windows.Media.Visual

システムが定義されたら、次のステップは画面にピクセルを描画することです。 Visual クラスでは、ビジュアル オブジェクトのツリーが構築されます。各オブジェクトには、必要に応じて、描画命令と、それらの命令 (クリッピング、変換など) のレンダリング方法に関するメタデータが含まれています。 Visual は、非常に軽量で高い柔軟性を持つように設計されているため、ほとんどの機能ではパブリック API は公開されておらず、保護されたコールバック関数に大きく依存しています。

Visual は、実際には WPF 合成システムへのエントリ ポイントです。 Visual は、マネージド API とアンマネージド milcore という 2 つのサブシステムの間の接続ポイントです。

WPF では、milcore によって管理されるアンマネージド データ構造を走査することによってデータが表示されます。 合成ノードと呼ばれるこれらの構造は、各ノードにレンダリング命令が含まれる階層表示ツリーを表します。 このツリー (次の図の右側に示されているもの) には、メッセージング プロトコルを使用することによってのみアクセスできます。

WPF のプログラミングでは、Visual 要素と派生型を作成します。それらは、このメッセージング プロトコルを使用して合成ツリーに対する内部的な通信を行います。 WPF の各 Visual では、合成ノードを作成しても (1 つまたは複数) 作成しなくてもかまいません。

The Windows Presentation Foundation Visual Tree.

このアーキテクチャの詳細に関して、注意する必要がある非常に重要な点は、ビジュアルと描画命令のツリー全体がキャッシュされるということです。 グラフィックスの用語を使うなら、WPF では保持されたレンダリング システムが使用されます。 これにより、合成システムはユーザー コードへのコールバックでブロックされず、システムでは高いリフレッシュ レートでを再描画できます。 これにより、アプリケーションの表示が応答しなくなるのを防ぐことができます。

図ではわかりませんが、システムによって実際に合成が行われる方法も重要です。

User32 と GDI では、システムはイミディエイト モードのクリッピング システムで動作します。 コンポーネントのレンダリングが必要な場合は、それより外側のピクセルはコンポーネントで変更できないクリッピング境界がシステムによって確立された後、そのボックス内のピクセルの描画がコンポーネントに対して要求されます。 このシステムは、操作する必要があるのは影響を受けるコンポーネントだけであり、2 つのコンポーネントが 1 つのピクセルの色に関与することがないため、メモリに制約のあるシステムで非常にうまく動作します。

WPF では、"ペインター アルゴリズム" 描画モデルが使用されます。 これは、各コンポーネントをクリッピングするのではなく、表示を背面から前面にレンダリングするよう各コンポーネントに要求することを意味します。 これにより、各コンポーネントでは前のコンポーネントの表示に重ねて描画できます。 このモデルの利点は、複雑で部分的に透明な形状を作成できることです。 現在の最新のグラフィックス ハードウェアを使えば、このモデルを比較的高速に実行できます (User32 と GDI が作成された当時では不可能でした)。

前に説明したように、WPF の中核となる理念は、より宣言的な "プロパティ中心" のプログラミング モデルに移行することです。 ビジュアル システムでは、これに関して興味深い点が 2 つあります。

1 つ目は、保持モード グラフィックス システムについて考えると、これにより命令型の DrawLine/DrawLine 型モデルからデータ指向モデルの new Line()/new Line() に実際に移行しています。 このデータ駆動型レンダリングへの移行により、描画命令での複雑な操作を、プロパティを使用して表すことができます。 Drawing から派生する型は、実質的にレンダリングのためのオブジェクト モデルです。

2 つ目は、アニメーション システムを評価すると、ほぼ完全に宣言型であることがわかります。 開発者は、次の位置や次の色を計算する必要はなく、アニメーション オブジェクトの一連のプロパティとしてアニメーションを表現できます。 これらのアニメーションでは開発者またはデザイナーの意図を表すことができ (このボタンをここからそこまで 5 秒で移動する)、それを実現するための最も効率的な方法をシステムで決定することができます。

System.Windows.UIElement

UIElement では、レイアウト、入力、イベントなどのコア サブシステムが定義されています。

レイアウトは、WPF での主要な概念です。 多くのシステムでは、レイアウト モデルのセットは固定されているか (HTML でサポートされているレイアウトのモデルは、フロー、絶対、テーブルの 3 つです)、レイアウトのモデルがありません (User32 では絶対配置のみがサポートされています)。 WPF は、開発者やデザイナーは柔軟で拡張性のあるレイアウト モデルを必要としており、それは命令型ロジックではなくプロパティ値によって駆動される、という前提から始まりました。 UIElement レベルで、レイアウトのための基本的なコントラクトが導入されています。それは、Measure パスと Arrange パスによる 2 フェーズ モデルです。

Measure を使うと、必要なサイズをコンポーネントで決定できます。 親要素が子に対して最適な位置とサイズを決定するための測定を何回も要求することがよくあるので、これは Arrange とは別のフェーズになっています。 親要素が子要素に測定を要求するという事実は、WPF のもう 1 つの重要な理念である "コンテンツのサイズに合わせる" ことを示しています。 WPF のすべてのコントロールは、そのコンテンツの自然なサイズに合わせてサイズを決定することができます。 これにより、ローカライズがはるかに簡単になり、要素のサイズ変更に合わせて動的なレイアウトを行うことができます。 Arrange フェーズでは、親は各子を配置し、その最終的なサイズを決定できます。

WPF の出力側である Visual と関連オブジェクトについては、多くの時間をかけて説明されることがよくあります。 しかし、入力側にも非常に多くのイノベーションがあります。 おそらく、WPF の入力モデルにおける最も基本的な変更点は、入力イベントがシステム内をルーティングされる一貫したモデルです。

入力は、カーネル モード デバイス ドライバーの信号として生成され、Windows カーネルと User32 が関係する複雑なプロセスを通して、正しいプロセスとスレッドにルーティングされます。 入力に対応する User32 メッセージは、WPF にルーティングされると、WPF の生の入力メッセージに変換されて、ディスパッチャーに送信されます。 WPF では、生の入力イベントを複数の実際のイベントに変換することができ、これにより、"MouseEnter" のような機能を、配信が保証された低レベルのシステムで実装できます。

各入力イベントは、少なくとも 2 つのイベント ("プレビュー" イベントと実際のイベント) に変換されます。 WPF のすべてのイベントには、要素ツリーを通じたルーティングの概念があります。 イベントがターゲットからツリーを上方にルートまで移動する場合は "バブル" と呼ばれ、ルートから開始してターゲットまで下方に移動する場合は "トンネル" と呼ばれます。 入力プレビュー イベントがトンネルされると、ツリー内の任意の要素でイベントをフィルター処理したり、イベントに対するアクションを実行したりすることができます。 その場合、通常の (プレビューではない) イベントが、ターゲットからルートまでバブルされます。

トンネル フェーズとバブル フェーズのこのような分割により、キーボード アクセラレータなどの機能の実装が、複合環境で一貫した方法で動作します。 User32 では、サポートするすべてのアクセラレータ (Ctrl + N から "新規作成" へのマッピングなど) が含まれる単一のグローバル テーブルを作成することにより、キーボード アクセラレータを実装します。 アプリケーションのディスパッチャーでは TranslateAccelerator を呼び出します。これにより、User32 の入力メッセージが傍受され、登録されているアクセラレータに一致するものがあるかどうかが判定されます。 WPF では、システムが完全に "構成可能" であるため、この機能は動作しません。すべての要素で任意のキーボード アクセラレータを処理および使用できます。 入力に対するこの 2 フェーズ モデルにより、コンポーネントで独自の "TranslateAccelerator" を実装できます。

これをさらに 1 歩進めるため、UIElement では CommandBindings の概念も導入されています。 WPF のコマンド システムを使うと、開発者は、コマンド エンド ポイント (ICommand を実装するもの) の観点から機能を定義できます。 コマンド バインドにより、要素で入力ジェスチャ (Ctrl + N) とコマンド (新規作成) の間のマッピングを定義できます。 入力ジェスチャとコマンド定義はどちらも拡張可能であり、使用時に結び付けることができます。 これにより、たとえば、エンド ユーザーがアプリケーション内で使用するキー バインドをカスタマイズできるようにすることが簡単になります。

ここまでは、WPF の "コア" 機能 (PresentationCore アセンブリで実装されている機能) に注目してきました。 WPF の構築において、基本要素 (MeasureArrange によるレイアウトのコントラクトなど) とフレームワーク要素 (Grid のような特定のレイアウトの実装など) の明確な分離は、望ましい結果でした。 目標は、外部の開発者が必要に応じて独自のフレームワークを作成できるようにする拡張ポイントを、スタックの低い部分で提供することでした。

System.Windows.FrameworkElement

FrameworkElement には、異なる 2 つの見方があります。 まず、WPF の下位レベルで導入されたサブシステムに対する一連のポリシーとカスタマイズが導入されています。 また、一連の新しいサブシステムも導入されています。

FrameworkElement によって導入される主なポリシーは、アプリケーションのレイアウトに関するものです。 FrameworkElementUIElement によって導入された基本的なレイアウト コントラクトが基になっており、レイアウトの作成者がプロパティ駆動のレイアウト セマンティクスの一貫したセットを簡単に使用できるレイアウト "スロット" の概念が追加されています。 HorizontalAlignmentVerticalAlignmentMinWidthMargin のようなプロパティ (一例です) により、FrameworkElement から派生するすべてのコンポーネントに、レイアウト コンテナー内での一貫した動作が提供されます。

FrameworkElement では、また、WPF のコア レイヤーにある多くの機能に対する API の公開も容易になります。 たとえば、FrameworkElement を使うと、BeginStoryboard メソッドを通してアニメーションに直接アクセスできます。 Storyboard により、一連のプロパティに対する複数のアニメーションをスクリプト化する方法が提供されます。

FrameworkElement によって導入された最も重要な 2 つのことは、データ バインディングとスタイルです。

WPF のデータ バインディング サブシステムは、Windows フォームまたは ASP.NET を使用してアプリケーションのユーザー インターフェイス (UI) を作成したことがある誰にでも、比較的なじみがあるはずです。 これらの各システムには、特定の要素の 1 つ以上のプロパティをデータにバインドする必要があることを表すための簡単な方法があります。 WPF では、プロパティ バインディング、変換、およびリスト バインディングが完全にサポートされています。

WPF でのデータ バインディングの最も興味深い機能の 1 つは、データ テンプレートの導入です。 データ テンプレートを使うと、データの一部を視覚化する方法を宣言によって指定できます。 データにバインドできるカスタム ユーザー インターフェイスを作成する代わりに、問題を転換して、作成される表示をデータで決定できるようにすることができます。

スタイルは、実際には軽量な形式のデータ バインディングです。 スタイルを使用すると、共有定義のプロパティのセットを、要素の 1 つ以上のインスタンスにバインドできます。 スタイルは、明示的な参照によって (Style プロパティを設定することにより)、またはスタイルを要素の CLR 型に関連付けることによって暗黙的に、要素に適用されます。

System.Windows.Controls.Control

Control の最も重要な機能はテンプレートです。 WPF の合成システムを保持モードのレンダリング システムと考えた場合、テンプレートを使用すると、パラメーター化された宣言型の方法によりコントロールでレンダリングを記述できます。 ControlTemplate は、コントロールによって提供されるプロパティにバインドされる子要素のセットを作成するためのスクリプトにすぎません。

Control では、ForegroundBackgroundPadding などのストック プロパティのセットが提供されています。テンプレート作成者はそれを使用して、コントロールの表示をカスタマイズできます。 コントロールの実装では、データ モデルと相互作用モデルが提供されています。 相互作用モデルでは、一連のコマンド (ウィンドウの [閉じる] など) と、入力ジェスチャ (ウィンドウの上の隅にある赤い X のクリックなど) に対するバインドが定義されています。 データ モデルには、相互作用モデルまたは表示をカスタマイズするためのプロパティのセットが用意されています (テンプレートによって決定されます)。

データ モデル (プロパティ)、相互作用モデル (コマンドとイベント)、表示モデル (テンプレート) のこのような分離により、コントロールの外観と動作を完全にカスタマイズできるようになります。

コントロールのデータ モデルの一般的な側面はコンテンツ モデルです。 Button のようなコントロールを見ると、"Content" という名前の Object 型のプロパティがあることがわかります。 Windows フォームと ASP.NET では、通常、このプロパティは文字列ですが、それによりボタンに配置できる内容の型が制限されます。 ボタンの内容には、単純な文字列、複雑なデータ オブジェクト、または要素ツリー全体のいずれかを使用できます。 データ オブジェクトの場合は、データ テンプレートを使用して表示を作成します。

まとめ

WPF は、動的でデータ駆動型のプレゼンテーション システムを作成できるように設計されています。 システムのすべての部分は、動作を駆動するプロパティ セットを通じてオブジェクトを作成するように設計されています。 データ バインディングはシステムの基本的な部分であり、すべてのレイヤーで統合されています。

従来のアプリケーションでは、表示を作成してから、何らかのデータにバインドします。 WPF では、コントロールに関するすべてのことと表示のすべての側面は、何らかの種類のデータ バインディングによって生成されます。 ボタン内に表示されるテキストは、ボタン内に合成コントロールを作成し、その表示をボタンのコンテンツ プロパティにバインドすることによって表示されます。

WPF ベースのアプリケーションの開発を始めれば、非常に馴染みのあるものに感じるはずです。 プロパティの設定、オブジェクトの使用、データのバインドは、Windows フォームまたは ASP.NET を使用するのとよく似た方法で行うことができます。 WPF のアーキテクチャについてさらに詳しく調べれば、アプリケーションのコア ドライバーとしてデータを基本的に扱う、よりリッチなアプリケーションを作成できる可能性があることがわかります。

関連項目