MS_WPFArchitecture - NetDevInfraWGinOSSConsortium/NetDevInfraWiki GitHub Wiki
- 戻る(WPF)
- Microsoft Learn > WPF の基礎 > WPF アーキテクチャ
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/advanced/wpf-architecture
補足(本ページの読み方): 本ページは
WPF の内部構造を、クラス階層に沿って積み上げて説明するという
構成をとっている。この順序自体に意味がある。【本ページの構成= WPF の設計そのもの】 ① クラス階層 … 各層が「何を追加するか」 ② 要素ツリー … 論理ツリーとビジュアル ツリー ③ WPF プロパティ システム … 依存関係/添付/継承 ④ データ バインディング … ③ の上に成り立つ ⑤ ルーティング イベント … ② の上に成り立つ → ③④⑤ は、それぞれ ①② のどの層で導入されたかが決まっている → 【階層を理解すれば、機能の有無が予測できる】★Windows Forms との根本的な違いを先に押さえておくと、
以降が読みやすい。
Windows Forms WPF 描画 GDI+(各コントロールが HWND を持つ) DirectX(HWND は最上位のみ)★ 見た目の変更 継承して OnPaintを書くControlTemplate で差し替える 配置 絶対座標(Location / Size) レイアウト システム(相対・可変) データ連携 DataBindingsデータ バインディング(宣言的) ★ UI の定義 デザイナが生成するコード XAML(宣言的マークアップ) 解像度 ピクセル依存 ベクタ(DPI 非依存) **「なるべく XAML の宣言的プロパティで書く」**という
WPF の理念(本文中で言及される)が、
依存関係プロパティを必要とした——という因果関係が本ページの主題である。
System.Object
- System.Threading.DispatcherObject
- System.Windows.DependencyObject
- System.Windows.Media.Visual
- System.Windows.UIElement
- System.Windows.FrameworkElement
- System.Windows.Controls.Control
- System.Windows.Controls.ContentControl
- System.Windows.Controls.ItemsControl
移行メモ(一覧は階層構造である): 原文は箇条書きで並列に見えるが、
実際には順に継承した階層である
(WPFのコントロール にも同じ図を示した)。System.Object └ DispatcherObject … スレッド親和性 └ DependencyObject … 依存関係プロパティ └ Visual … 描画 └ UIElement … レイアウト・入力・ルーティング イベント └ FrameworkElement … スタイル・データ バインディング └ Control … テンプレート ├ ContentControl └ ItemsControl移行メモ(名前空間の誤り): 原文の
System.Threading.DispatcherObjectは、正しくは
System.Windows.Threading.DispatcherObjectである
(本文中の MSDN リンク先は正しい名前空間を指している)。
https://learn.microsoft.com/ja-jp/dotnet/api/system.object
- WPF のプログラミング モデルを提供するフレームワークは、
CLR 上のマネージ コンポーネント(PresentationFramework.dll、PresentationCore.dll)
として公開される。 - マネージ コンポーネントには、開発の生産性と信頼性を高める多数の機能
(メモリ管理、エラー処理、共通型システムなど)が用意されているが、
性能など犠牲となるものもある。
これに対し、アンマネージ コンポーネント(milcore.dll)はだけ。
- メモリと実行の細かい制御も、WPF に対する要件の 1 つ。
- DirectX との緊密な統合の実現するため。
ハードウェア レンダリングおよびソフトウェア レンダリングの効率性を考え、
WPF での表示はすべて DirectX エンジンによって実行されるようになっている。
補足(
milcore.dllの位置付け): WPF の性能特性を決めている
重要な要素なので補足する。【WPF の 3 層構造】 PresentationFramework.dll … マネージ。スタイル、テンプレート、 データ バインディング、コントロール PresentationCore.dll … マネージ。Visual、UIElement、描画の基礎 ───────────────────────────────────── milcore.dll … 【アンマネージ】★ ・MIL(Media Integration Layer) ・【コンポジション エンジン】 ・DirectX を直接叩く ・OS(DWM: デスクトップ ウィンドウ マネージャー)とも共有される【保持モード描画(Retained Mode)】★ これが WPF の本質 【Windows Forms(即時モード)】 再描画が必要 → WM_PAINT → 【毎回 OnPaint で描き直す】 → 描画コードはアプリ側にある 【WPF(保持モード)】 アプリは「何を描くか」を【ビジュアル ツリーとして登録する】 → milcore が【それを保持し、独自に再描画する】 → アプリのコードは再描画のたびに走らない ★ → アニメーションが滑らかに動く(UI スレッドを介さない)移行メモ(原文の「アンマネージ コンポーネント(milcore.dll)はだけ。」):
文が途中で切れていると読める。
文脈から**「アンマネージ コンポーネントは milcore.dll だけである」**
という意味と解する。
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.threading.dispatcherobject
DispatcherObject は、派生した CLR オブジェクトに、
STAオブジェクトとしての動作を実装する基本抽象クラスである。
通常、WPF アプリケーションは、
- レンダリング スレッド:レンダリングを処理するスレッド
- UI スレッド:アプリケーションの主スレッド(イベント処理、UI 処理)
の2つのスレッドを使用して実行される。
「UI スレッド」と「レンダリング スレッド」の関係は、
- 「レンダリング スレッド」は、「UI スレッド」がユーザ入力を受け取り、
イベント処理・UI 処理をしている間に、バック グラウンドで実行される。 - このため、ほとんどの WPF アプリケーションは、単一の「UI スレッド」で済むが、
状況によっては、応答性を高める目的でバック グラウンド スレッドを使用する場合もある。 - WPF では、バック グラウンド処理の実装を支援する DispatcherObject を使用できる。
-
Microsoft Learn > WPF の基礎 > スレッド モデル
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/advanced/threading-model -
DispatcherObject.Invoke、BeginInvoke メソッドを使用することにより、
バック グラウンド処理からの「UI 変更処理」(= UI 要素の操作処理)が、
容易に実装可能となる。-
MSDN マガジン > 発行物 > WPF のスレッド:Dispatcher を使用して応答性の高い
アプリケーションを構築する
https://learn.microsoft.com/ja-jp/archive/msdn-magazine/2007/october/wpf-threading-build-more-responsive-apps-with-the-dispatcher -
Microsoft Learn > .NET Framework クラス ライブラリ
-
System.Windows.Threading.Dispatcher.Invoke メソッド
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.threading.dispatcher.invokeこのメソッドに指定されたデリゲートは、同期実行されるため、
バック グラウンド スレッドが実行結果を表示する「UI 変更処理」に
利用することが多い。 -
System.Windows.Threading.Dispatcher.BeginInvoke メソッド
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.threading.dispatcher.begininvoke -
System.Windows.Threading.DispatcherPriority 列挙体
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.threading.dispatcherpriority
-
-
-
Invoke、BeginInvoke のメソッドは、
- 内部的には(STAと同様にスレッド間通信の手段である)
Windows メッセージキューに UI 変更処理をエンキューする。 - エンキューされた「UI 変更処理」は、「UI スレッド」がアイドル状態の際に
デキューされて、「UI スレッド」により処理される。
- 内部的には(STAと同様にスレッド間通信の手段である)
-
これにより、
- 「UI 変更処理」は、必ず、特定の「UI スレッド」により処理されるようになる。
- マルチ スレッド処理においてスレッド セーフではない UI コントロールの
変更処理が問題とならなくなる。
補足(
Control.Invokeとの対応): この節の内容は、
Control.Invoke、.BeginInvoke の WPF 版である。
仕組みは同じで、名前が違うだけである。
Windows Forms WPF 同期 Control.InvokeDispatcher.Invoke非同期 Control.BeginInvokeDispatcher.BeginInvoke/InvokeAsync判定 InvokeRequiredDispatcher.CheckAccess()現在の推奨 async/awaitasync/await★
DispatcherPriorityは WPF 固有の重要な機能である。【優先度(高い順)】 Send … 【即座に】(同期。Invoke の既定) Normal … 通常(BeginInvoke の既定) DataBind … データ バインディングの処理 Render … レンダリング Loaded … レイアウト完了後 ★ Input … 入力処理 Background … 【他に何もないとき】 ContextIdle / ApplicationIdle / SystemIdle … アイドル時 → 「レイアウトが終わってから実行したい」といった タイミング制御ができる// レイアウト完了後にフォーカスを当てる(よく使う) Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() => textBox1.Focus()));
DispatcherObject.CheckAccess()/VerifyAccess():if (!textBlock1.CheckAccess()) // 別スレッドからか? textBlock1.Dispatcher.Invoke(() => textBlock1.Text = "完了"); else textBlock1.Text = "完了";【注意】 CheckAccess / VerifyAccess は 【IntelliSense に出ない】(EditorBrowsable 属性で隠されている) が、public であり呼び出せる ★「レンダリング スレッドは別」という点の帰結:
・アニメーションは【UI スレッドが忙しくても動く】ことがある → milcore 側で補間されるため ・逆に、UI スレッドを止めると 【入力とレイアウトは止まる】(描画だけ生きている) ・「固まっているのにアニメーションが動く」という 一見奇妙な症状はこれが原因
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.dependencyobject
DependencyObject は、派生した CLR オブジェクトに「WPF プロパティ システム」を実装する。
- WPF のアーキテクチャの理念は、「メソッドやイベント(コードビハインド)よりも、
なるべく XAML のマークアップによる宣言的プロパティを使用する」ことである。 - これを実現する「WPF プロパティ システム」を実装する DependencyObject により、
開発者は多数の宣言的プロパティを使用し、コントロールに開発者の意図を設定できる。
このため WPF の開発元は、この XAML のマークアップによる宣言的プロパティによる
制御の範囲を拡大するために、「WPF プロパティ システム」の「依存関係プロパティ」が
必要であると判断した。
-
「依存関係プロパティ」は、プロパティの依存関係を把握し、双方向の接続と変更通知を実現する。
-
具体的には、ソースとなるオブジェクトのプロパティの変更が通知された場合、
(必須ではないが、INotifyPropertyChanged インターフェイスを使用すると、
オブジェクトによる変更通知の発行が可能になる。)
ターゲットとなるオブジェクトのプロパティ値を自動的に検証、計算するなど、
高度なオブジェクト プロパティ間の接続を実現する。- Microsoft Learn > .NET Framework クラス ライブラリ
System.ComponentModel.INotifyPropertyChanged インターフェイス
https://learn.microsoft.com/ja-jp/dotnet/api/system.componentmodel.inotifypropertychanged
- Microsoft Learn > .NET Framework クラス ライブラリ
-
「依存関係プロパティ」の入力には、次のものがある。
-
動的な「リソース」
- 「スタイル」
- 「テンプレート」
- 「アニメーション」
-
「データ バインディング」
- 「ビュー・モデル オブジェクト」
-
「添付プロパティ」
親子要素のリレーションの値(子要素 → 親要素)を設定する。 -
「プロパティ値の継承」
親子要素のリレーションの値(親要素 → 子要素)を設定する。
-
補足(「依存関係」という名前の由来): 「なぜ 依存関係 プロパティなのか」
は分かりにくいので補っておく。【1 つのプロパティの値が、複数の入力に「依存」する】★ Button.Background の値は、どこから来るか? ① アニメーション ← 最も強い ② ローカル値(Background="Red" と直接書いた) ③ テンプレート トリガー ④ 暗黙のスタイル / スタイル トリガー ⑤ テーマ スタイル ⑥ 継承された値 ⑦ 既定値 ← 最も弱い → 「今どの値が有効か」を【プロパティ システムが判定する】 → だから値をフィールドに持てない= 辞書構造で管理する【この優先順位が実務で効く場面】★ 「スタイルで色を指定したのに反映されない」 → コード または XAML で【ローカル値】を設定している → ローカル値はスタイルより強い → ClearValue() でローカル値を消すと、スタイルが効くようになる「一種の辞書構造で値を保持する」ことの利点:
・【既定値のままなら記憶領域を消費しない】 → Control には数百のプロパティがあるが、 すべてをフィールドで持つとメモリを食う → 明示的に設定されたものだけを保持する ★ ・変更通知が組み込まれる(イベントを自分で実装しなくてよい) ・アニメーション・バインディングの対象になれる定義方法の詳細は本ページ後半の
「WPF プロパティ システム」節、
および WPFのコントロール を参照。
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.visual
Visual は、WPF の中心的な機能である描画をサポートする基本抽象クラスである。
Visual は、マネージ コンポーネントとアンマネージ コンポーネントの
2つのサブシステムの接続ポイントであり、
- Visual で定義されているマネージ データ(描画情報、描画方法など)から、
- 「ビジュアル ツリー」(後述)と呼ばれるツリー構造のアンマネージ データを構成する。
- これを「レンダリング スレッド」がツリー構造の上から下にスキャンする。
これによって、描画内容をレンダリングする。
-
WPF グラフィックス レンダリングの概要
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/graphics-multimedia/wpf-graphics-rendering-overview -
出力表示 : 描画内容をレンダリングする。
- クリッピング : CG で、画像の一部を切り抜く。
- 変換 : 変換(回転、拡大縮小、傾斜、平行移動)を実行する。
- 境界の計算 : ビジュアルの外接する四角形を決定する。
- ヒット テスト : 境界に含まれているかどうかを判定する。
-
プロパティの例
- VisualOpacity : 不透明度
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.visual.visualopacity - VisualOpacityMask : 不透明マスク
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.visual.visualopacitymask - VisualClip : クリッピング(クリップ領域の決定)
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.visual.visualclip - VisualTransform : 変換(ビジュアルの変換値)
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.visual.visualtransform - VisualEdgemode : 端の描画方法
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.visual.visualedgemode
- VisualOpacity : 不透明度
-
Visual オブジェクトの階層構造
┬Visual
├UIElement
│└FrameworkElement
├ContainerVisual
│├DrawingVisual
│├HostVisual
└Viewport3DVisual
-
System.Windows.Media.ContainerVisual クラス
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.containervisual
Visual オブジェクトのコレクションのコンテナとして使用される。 -
System.Windows.Media.DrawingVisual クラス
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.drawingvisual- 図形(ベクタ グラフィックス)、イメージ、モニター(ビデオ)、グリフ(テキスト)の
描画に使用する描画クラス。 - レイアウトやイベントの処理を実現しないため軽量で、背景やクリップ アートの描画に適す。
- また、ContainerVisual クラスから派生するため、
Visual オブジェクトのコレクションを格納できる。
- 図形(ベクタ グラフィックス)、イメージ、モニター(ビデオ)、グリフ(テキスト)の
-
System.Windows.Media.Media3D.Viewport3DVisual クラス
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.media3d.viewport3dvisual- 2D の Visual オブジェクトと Visual3D オブジェクト間のブリッジを提供する。
- Viewport3DVisual では、Camera プロパティ(シーンを表示)と
Viewport プロパティ(投影をサーフェイス上にマップ)を定義する必要がある。
移行メモ(原文の階層図とクラス名): 図の罫線が崩れているが、
内容としては以下の階層である。Visual ├ UIElement │ └ FrameworkElement ├ ContainerVisual │ ├ DrawingVisual │ └ HostVisual └ Viewport3DVisualまた、原文の
Media3D(全角の 3)は
Media3Dが正しい(本ページでは半角に修正した)。補足(
DrawingVisualの使いどころ): 「レイアウトやイベントの処理を
実現しないため軽量」という記述は重要である。【大量描画での性能】★ ・UIElement 派生(Rectangle 等)を 1 万個置く → レイアウト計算・イベント処理・依存関係プロパティの オーバーヘッドが 1 万個分かかる → 【非常に重い】 ・DrawingVisual を使う → 描画だけ。桁違いに軽い → グラフ、地図、シミュレーションの描画に使われる 【現在の選択肢】 ・DrawingVisual(軽量) ・WriteableBitmap(ピクセル単位で自前描画) ・【SkiaSharp / D3DImage】(さらに高速。GPU を直接使う)
-
Visual オブジェクトの描画コンテンツ
-
Visual オブジェクトは、描画コンテンツを格納した下記の4種類の
レンダリング データを命令リストとして格納する。 -
また、命令リストを表すオブジェクトとして、DrawingGroup オブジェクト、
Drawing オブジェクトがある。 -
なお、Drawing クラスは基本抽象クラスであり、
描画コンテンツの種類に合わせて、様々な派生クラスがある。- 描画コンテンツ:ベクタ グラフィックス、イメージ、モニター、グリフ
- 派生クラス:GeometryDrawing、ImageDrawing、VideoDrawing、GlyphRunDrawing
-
System.Windows.Media.Drawing クラス
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.drawing -
System.Windows.Media.DrawingGroup クラス
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.drawinggroup -
System.Windows.Media.GeometryDrawing クラス
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.geometrydrawing -
System.Windows.Media.ImageDrawing クラス
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.imagedrawing -
System.Windows.Media.VideoDrawing クラス
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.videodrawing -
System.Windows.Media.GlyphRunDrawing クラス
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.glyphrundrawing
-
-
Microsoft Learn > Windows Presentation Foundation > グラフィックスとマルチメディア
- Drawing オブジェクトの概要
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/graphics-multimedia/drawing-objects-overview - 方法:GeometryDrawing を作成する
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/graphics-multimedia/how-to-create-a-geometrydrawing - 方法:ImageDrawing を使用してイメージを描画する
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/graphics-multimedia/how-to-draw-an-image-using-imagedrawing - 方法:VideoDrawing を使用してメディアを再生する
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/graphics-multimedia/how-to-play-media-using-a-videodrawing
- Drawing オブジェクトの概要
-
Visual オブジェクトの命令リスト
| 項番 | 命令リスト | 説明 |
|---|---|---|
| 1 | ベクタ グラフィックス (GeometryDrawing) |
Geometry、Pen、Brush などのベクタ グラフィックス データ |
| 2 | イメージ (ImageDrawing) |
デジタル メディア ファイル内の各種イメージ |
| 3 | モニター (VideoDrawing) |
デジタル メディア ファイル内の各種ビデオ |
| 4 | グリフ (GlyphRunDrawing) |
テキストとフォントで表わされるグリフ (特定のフォントにおける文字の物理表現) |
-
System.Windows.Media.Geometry クラス
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.geometry -
System.Windows.Media.Pen クラス
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.pen -
System.Windows.Media.Brush クラス
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.brush -
レンダリング データのレンダリング
Visual オブジェクトの描画コンテンツであるベクタ グラフィックスのレンダリング データは、- DrawingVisual・DrawingImage オブジェクトの Drawing プロパティに設定された
DrawingGroup オブジェクト内の1つ以上の Drawing オブジェクト、 - もしくは Drawing オブジェクトの Pen・Brush プロパティ
として表される。
なお、
- DrawingVisual・DrawingGroup オブジェクトに、DrawingGroup、Drawing オブジェクトを
設定するには、DrawingContext オブジェクトを取得して行う。 - DrawingImage オブジェクトに DrawingGroup、Drawing オブジェクトを設定するには、
コンストラクタを使用する。 - DrawingVisual・DrawingImage オブジェクトが「レンダリング スレッド」により
レンダリングされるとき、
DrawingGroup オブジェクトは、自身に設定されているプロパティを次の順に適用する。- Children
- OpacityMask
- Opacity
- BitmapEffect
- ClipGeometry
- GuidelineSet
- Transform
- DrawingVisual・DrawingImage オブジェクトの Drawing プロパティに設定された
移行メモ(
BitmapEffectは廃止されている): 適用順に挙げられている
BitmapEffectは .NET Framework 3.5 SP1 で非推奨となった。【理由】 BitmapEffect は【常にソフトウェア レンダリング】だった → 極端に遅い → ハードウェア アクセラレーションを無効化してしまう 【後継】 UIElement.Effect(Effect クラス)★ ・BlurEffect、DropShadowEffect ・【GPU(ピクセル シェーダー)で実行される】 ・ShaderEffect を継承して独自のエフェクトも作れる<!-- ✗ 非推奨 --> <Button BitmapEffect="{StaticResource ...}" /> <!-- ○ 現在 --> <Button> <Button.Effect><DropShadowEffect BlurRadius="8" /></Button.Effect> </Button>
GuidelineSetについても補足しておく。【WPF はベクタ描画= 座標が実数】 → 1 ピクセルの線が【2 ピクセルにまたがってぼやける】★ 【対策】 ・GuidelineSet でピクセル境界に吸着させる ・または UseLayoutRounding="True"(後述の FrameworkElement) ・SnapsToDevicePixels="True" → 「線がぼやける」「文字がにじむ」という WPF の典型的な見た目の問題への対処
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.uielement
UIElement は、WPF コア レベル実装の基本クラス
UIElement は、WPF コア レベル実装の基本クラスで、下記を目的とする。
- 派生クラスで、オーバーライドする仮想メソッドを公開する。
- 派生クラスで、仮想メソッドをオーバーライドすることによって、
中心的なサブシステムを定義する。- 「レイアウト」
- 「ルーティング イベント」
- 自要素と子要素の「レイアウト」を操作する。
- キーボード、マウス、およびスタイラスなどによる入力操作に
対する「ルーティング イベント」と、関連するプロパティが含まれる。
- 子要素の「レイアウト」(サイズ指定・配置)
- ユーザ入力への応答、「ルーティング イベント」
- 「アニメーション」システムを部分的にサポート
- IsEnabled : UI 要素の有効・無効
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.uielement.isenabled - Visibility : UI 要素の可視性
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.uielement.visibility - AllowDrop : D&D の有効・無効
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.uielement.allowdrop - Focusable : フォーカスの有効・無効
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.uielement.focusable - RenderTransform : 変換(アニメーションや一時効果の追加)
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.uielement.rendertransform
補足(レイアウトの 2 パス ── UIElement の中核): 「レイアウトを操作する」
の中身は、Measure→Arrangeの 2 パスである。
カスタム パネルを作る際に必ず必要になる知識なので補っておく。【① Measure パス(測定)】 親が子に「使える最大サイズ」を渡し、 子が「必要なサイズ(DesiredSize)」を返す MeasureOverride(Size availableSize) → Size 【② Arrange パス(配置)】 親が子に「実際に使ってよい矩形」を渡し、 子がその中に自分を配置する ArrangeOverride(Size finalSize) → Size → 【子のサイズは親が決めるのではなく、交渉で決まる】★ → だから Width/Height を書かなくてもレイアウトが成立する → Windows Forms の絶対座標との根本的な違い【性能上の注意】★ ・レイアウトは【変更のたびに再計算される】 ・深いツリー、大量の要素で重くなる ・InvalidateMeasure / InvalidateArrange を多用しない ・仮想化(VirtualizingStackPanel)を有効にする → ItemsControl で数千件を表示する場合は必須
Visibilityの 3 値も実務で効く。Visible … 表示し、レイアウト領域も占める Hidden … 【非表示だが、レイアウト領域は占める】★ Collapsed … 非表示で、レイアウト領域も占めない → Windows Forms の Visible(bool)にはない区別
RenderTransformとLayoutTransformの違い(後述の
FrameworkElement と対になる):RenderTransform(UIElement)… 【レイアウト後】に変換する → 周囲の要素は動かない。重なることがある → 【速い】(アニメーション向き)★ LayoutTransform(FrameworkElement)… 【レイアウト前】に変換する → 変換後のサイズでレイアウトが再計算される → 周囲が押しのけられる。遅い → [XAMLの書き方(2)](MS_XAMLWriting2) で図付きで扱われている
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.frameworkelement
UIElement に、「レイアウト」、「スタイル」を中心とした機能を追加する基本クラス。
FrameworkElement は、UIElement の仮想メンバへ、以下を導入することを目的とする。
- ポリシー・カスタマイズの導入
- 「レイアウト」
- 動的「リソース」
- 「スタイル」
「テンプレート」は、FrameworkElement ではなく、コントロール クラスによって導入される。 - 「アニメーション」
アニメーションは、UIElement で定義されているが、FrameworkElement では、
FrameworkElement.BeginStoryboard メソッドと、
その関連メンバを実装することによって拡張できる。 - 「データ バインディング」
- 新しいサブシステムの導入
- 追加の「レイアウト」特性の定義
- 動的「リソース」
- 「スタイル」のサポート
- 「アニメーション」のサポートの強化
- 「データ バインディング」
- Style : 使用されるスタイル
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.frameworkelement.style - UseLayoutRounding : サイズと位置の丸め要・否
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.frameworkelement.uselayoutrounding - LayoutTransform : 変換(回転、拡大縮小、傾斜、平行移動)
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.frameworkelement.layouttransform
移行メモ(
LayoutTransformのリンク): 原文では
LayoutTransformの URL がUseLayoutRoundingと同じになっていたため、
正しい URL に修正した。補足(この層で「宣言的に書ける」ようになる): FrameworkElement が
導入する機能は、いずれも XAML から宣言的に扱うためのものである。【UIElement までで揃うも】 描画・入力・レイアウトの【仕組み】 【FrameworkElement が足すもの】★ ・Style … 見た目をまとめて指定する ・DataContext … データ バインディングの起点 ・Resources … リソース辞書 ・Name / FindName … 名前で引ける ・Loaded / Unloaded イベント ・Margin / HorizontalAlignment / VerticalAlignment ・Width / Height / MinWidth / MaxWidth … → 「XAML で書く WPF」の実体は、 ほぼこの層から上である ★
UseLayoutRoundingは実務で重要である。【問題】 ベクタ描画のため、要素の境界が非整数ピクセルに来る → 線がぼやける、1px ずれる 【対策】 <Window UseLayoutRounding="True"> → レイアウト結果を整数ピクセルに丸める → .NET 4 以降、既定で True の場面が増えた
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.controls.control
「テンプレート」を使用して外観を定義する UI 要素の基本クラス
Control の最も重要な機能は、「テンプレート」である。
この「テンプレート」により、
- コントロールの「外観」を宣言型マークアップでカスタマイズ可能になる
(「テンプレート」を複数の子要素から構成する)。 - また、「イベント ハンドラ」や「イベント トリガ」などもこの「テンプレート」により
定義可能で、
「テンプレート」を「スタイル」化することで、任意の型のコントロールに、
これらの「テンプレート」の定義の適用を強制できる。
補足(テンプレートが WPF の最大の特徴): 「Control の最も重要な機能は
テンプレートである」という原文の評価は正しい。
これが Windows Forms との決定的な差である。【Windows Forms でボタンの見た目を変える】 ・Button を継承する ・OnPaint をオーバーライドして自前で描く ・→ 【描画コードを書く】。状態(押下・ホバー)も自分で管理 【WPF でボタンの見た目を変える】★ ・ControlTemplate を差し替える ・→ 【マークアップを書くだけ】 ・→ Click イベント、コマンド、フォーカス、アクセシビリティは 【そのまま動く】(Button の振る舞いは変わらない)<Button Content="OK"> <Button.Template> <ControlTemplate TargetType="Button"> <Border x:Name="bd" CornerRadius="4" Background="{TemplateBinding Background}"> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" /> </Border> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="bd" Property="Background" Value="LightBlue" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Button.Template> </Button>【「振る舞い」と「見た目」の分離】★ Button … 「クリックできる」という【振る舞い】 Template … 「どう見えるか」という【見た目】 → 見た目を丸ごと変えても、振る舞いは壊れない → デザイナと開発者の分業が可能になる (これが Expression Blend の存在意義だった)
TemplateBindingとContentPresenterはテンプレートの必須要素である。TemplateBinding … テンプレートの外から設定された値を引き込む ContentPresenter … Content の中身をここに描画する ItemsPresenter … ItemsControl の項目をここに並べる → [XAMLの書き方(1)](MS_XAMLWriting1) で図付きで詳説されている
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.controls.contentcontrol
-
ContentControl は、Content プロパティを持つコントロールである。
-
Content プロパティには、文字列に限らず、様々な子要素を 1 つだけ設定可能である。
-
代表的な ContentControl 型の(ContentControl クラスから派生した)コントロールには、
Button、CheckBox、RadioButton などがある。 -
なお、プロパティへ子要素を設定する XAML 構文を、
- 「プロパティ属性構文」
- 「プロパティ要素構文」
と呼び、この中で、特に Content プロパティに子要素を設定する XAML 構文を
「コンテンツ構文」と呼ぶ。
-
-
ContentControl も、Content プロパティの表示に特化した「テンプレート」を持つ。
-
ItemsControl は、1つのコンテンツを設定する ContentControl に対し、
複数のコンテンツを設定できる Items コレクション プロパティを持つコントロールである。 -
Items コレクション プロパティには、様々な子要素を複数設定可能である。
- 代表的な ItemsControl 型の(ItemsControl クラスから派生した)コントロールには、
ComboBox、ListBox、TabControl、TreeView などがある。 - なお、同様に Items コレクション プロパティに子要素を設定する XAML 構文を
「コンテンツ構文」と呼ぶ。
- 代表的な ItemsControl 型の(ItemsControl クラスから派生した)コントロールには、
-
ItemsControl も、Items コレクション プロパティの表示に特化した「テンプレート」を持つ。
補足(
Contentに「何でも入る」ことの意味): 「文字列に限らず、
様々な子要素を 1 つだけ設定可能」——
これが WPF の柔軟さの源である。<!-- ボタンの中に画像とテキストを入れられる --> <Button> <StackPanel Orientation="Horizontal"> <Image Source="save.png" Width="16" /> <TextBlock Text="保存" Margin="4,0,0,0" /> </StackPanel> </Button> <!-- ボタンの中にボタンすら入る(意味はないが、可能) --> <Button><Button Content="入れ子" /></Button>【Content が object 型であることの帰結】★ ・文字列 → そのまま TextBlock として描画 ・UIElement → その要素をそのまま描画 ・それ以外の型 → 【DataTemplate を探して描画】 → 見つからなければ ToString() の結果を表示 → これが [WPFのコントロール](MS_WPFControls) で述べた 「ViewModel を差し替えれば View が切り替わる」仕組みの土台
ItemsとItemsSourceの使い分け(実務で頻出):Items … 直接追加する(XAML に書く場合) ItemsSource … 【コレクションをバインドする】★ 【注意】 両方を同時に使うと例外になる 「Items コレクションは空である必要があります。 ItemsSource を使用する前に」
ItemsControlの 4 つのテンプレートは
XAMLの書き方(1) の主題である。ItemTemplate … 1 件をどう描くか ItemsPanelTemplate … 項目をどう並べるか(縦・横・グリッド) ItemContainerStyle … 各項目の入れ物(ListBoxItem)の見た目 Template … コントロール全体の枠
WPF は、ビジュアル要素や、CLR オブジェクトによる「要素ツリー」を構築して、
それを処理することでディスプレイへの表示を行う。
「要素ツリー」は、XAML やプログラムにより、構築される。
- Microsoft Learn > Windows Presentation Foundation > 方法:要素を動的に追加する
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/data/how-to-add-and-remove-list-items
例えば XAML は、次の「プロパティ要素構文」の暗黙的、または明示的な記述で、
ツリー構造の CLR オブジェクトをインスタンス化する。
- DockPanel.Children(Panel.Children)プロパティ
- ListBox.Items(ItemsControl.Items)プロパティ
<DockPanel>
<!--implicit: <DockPanel.Children>--> → プロパティ要素構文(省略可能)
<ListBox DockPanel.Dock="Top">
<!--implicit: <ListBox.Items>--> → プロパティ要素構文(省略可能)
<ListBoxItem>
<TextBlock>Dog</TextBlock>
</ListBoxItem>
<ListBoxItem>
<TextBlock>Cat</TextBlock>
</ListBoxItem>
<ListBoxItem>
<TextBlock>Fish</TextBlock>
</ListBoxItem>
<!--implicit: </ListBox.Items>--> → プロパティ要素構文(省略可能)
</ListBox>
<Button Height="20" Width="100" DockPanel.Dock="Top">Buy a Pet</Button>
<!--implicit: </DockPanel.Children>--> → プロパティ要素構文(省略可能)
</DockPanel>-
ただし、「要素ツリー」は、実体そのものではない「メタファ」であるため、
XML DOM のような XML ツリー操作用の API を使用して直接操作することはない。 -
これは、WPF の UI サブシステムのアーキテクチャ、
UI フレームワークの構造を理解する上で役立つ。 -
「要素ツリー」には、「論理ツリー」・「ビジュアル ツリー」の2つの解釈方法がある。
- Microsoft Learn > WPF の基礎 > WPF のツリー
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/advanced/trees-in-wpf
- Microsoft Learn > WPF の基礎 > WPF のツリー
「論理ツリー」は、CLR オブジェクトの要素のツリーを表し、
「プロパティ継承」や、「ルーティング イベント」を理解する上で役立つ。
以下の XAML とコードは、同じ「論理ツリー」(CLR オブジェクトの要素のツリー)を
生成するため、同じ UI を表示する。
<DockPanel>
<ListBox Width="100">
<ListBoxItem>Dog</ListBoxItem>
<ListBoxItem>Cat</ListBoxItem>
<ListBoxItem>Fish</ListBoxItem>
</ListBox>
<Button Click="OnClick">OK</Button>
</DockPanel>DockPanel dp = new DockPanel();
ListBox lbx = new ListBox();
lbx.Width = 100;
ListBoxItem lbxItem = null;
lbxItem = new ListBoxItem();
lbxItem.Content = "Dog";
lbx.Items.Add(lbxItem);
lbxItem = new ListBoxItem();
lbxItem.Content = "Cat";
lbx.Items.Add(lbxItem);
lbxItem = new ListBoxItem();
lbxItem.Content = "Fish";
lbx.Items.Add(lbxItem);
dp.Children.Add(lbx);
Button btn = new Button();
btn.Content = "OK";
btn.Click += new RoutedEventHandler(this.OnClick);
dp.Children.Add(btn);
this.AddChild(dp);「ビジュアル ツリー」は、「論理ツリー」と異なり、ビジュアル要素のツリーを表し、
コントロールの「テンプレート」が展開される。
このため、アプリケーションの UI で使用する、全ての Visual オブジェクト(描画内容)が
含まれる。
「ビジュアル ツリー」階層の最上位の要素(ルート ビジュアル)から、
左から右に幅優先で走査・レンダリングされる。
ルート ビジュアルは、「ビジュアル ツリー」階層の最上位の要素で、
ほとんどのアプリケーションでは、ルート ビジュアルは以下のいずれかになる。
-
Window : 「Window 画面」
-
NavigationWindow : 「ナビゲーション フレームワーク」
-
Win32 アプリケーションにホストされる場合、
Win32 ウィンドウにホストされる最上位のビジュアル要素がルート ビジュアルになる。Microsoft Learn > Windows Presentation Foundation > チュートリアル:
Win32 アプリケーションでのビジュアル オブジェクトのホスト
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/advanced/tutorial-hosting-visual-objects-in-a-win32-application
補足(2 つのツリーの違いが実務で効く場面): 「論理ツリー」と
「ビジュアル ツリー」の区別は、トラブル時に必ず必要になる。【同じ Button の 2 つの見え方】 【論理ツリー】 【ビジュアル ツリー】 Button Button └ "OK"(文字列) └ ButtonChrome(テンプレートの中身)★ └ ContentPresenter └ TextBlock └ "OK"
論理ツリー ビジュアル ツリー 含むもの XAML に書いた要素 テンプレートの中身も含む全描画要素 要素数 少ない 非常に多い 関わる機能 プロパティ値の継承、ルーティング イベント、リソース検索 描画、ヒット テスト、 RelativeSource FindAncestorヘルパー LogicalTreeHelperVisualTreeHelper【実務での場面】★ ・「ListBox の中の TextBox を探したい」 → ItemTemplate で生成された要素は【論理ツリーに現れない】 → VisualTreeHelper で辿る ・「Style が効かない」 → リソース検索は【論理ツリーを遡る】 → テンプレート内の要素からは辿れないことがある ・「イベントが飛んでこない」 → ルーティングは【論理ツリー寄り】の経路をとる (厳密にはビジュアル ツリーだが、論理親も考慮される)
実際に、「要素ツリー」の内容を確認したい場合は、
以下のヘルパ クラスの GetChildren メソッドを再帰的に使用して、
各ツリーを出力すると良い。
-
LogicalTreeHelper:「論理ツリー」の子要素を照会
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.logicaltreehelper -
VisualTreeHelper:「ビジュアル ツリー」の子要素を照会
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.media.visualtreehelper
上記のヘルパ クラスの使用例を、サンプルコードとして以下に示す。
LogicalTreeHelper を使用する場合は、以下の PrintLogicalTree メソッドを実行する。
private void PrintLogicalTree() {
Debug.WriteLine("PrintLogicalTree");
PrintLogicalTree(0, this);
}
// 論理ツリーを出力する。
// DependencyObjectの場合は、子要素も再帰的に表示する
private void PrintLogicalTree(int level, DependencyObject obj) {
PrintObject(level, obj);
foreach (var child in LogicalTreeHelper.GetChildren(obj)) {
if (child is DependencyObject) {
PrintLogicalTree(level + 1, (DependencyObject)child);
}
else {
PrintObject(level + 1, child);
}
}
}VisualTreeHelper を使用する場合は、以下の PrintVisualTree メソッドを実行する。
private void PrintVisualTree() {
Debug.WriteLine("PrintVisualTree");
PrintVisualTree(0, this);
}
// ビジュアル ツリーを表示する。
// DependencyObjectの場合はビジュアル ツリー上の子要素も再帰的に出力していく
private void PrintVisualTree(int level, DependencyObject obj) {
PrintObject(level, obj);
foreach (var child in GetVisualChildren(obj)) {
if (child is DependencyObject) {
PrintVisualTree(level + 1, (DependencyObject)child);
}
else {
PrintObject(level + 1, child);
}
}
}
// ビジュアル ツリーの子要素の列挙を返す
private IEnumerable<object> GetVisualChildren(DependencyObject obj) {
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++) {
yield return VisualTreeHelper.GetChild(obj, i);
}
}結果をインデントつきで出力
// ToStringの結果をインデントつきで出力
private void PrintObject(int level, object obj) {
Debug.WriteLine(new string('\t', level) + obj);
}補足(ツリーを見る現在の手段): 自前で出力する方法は今も有効だが、
現在はツールで見るのが速い。
ツール 内容 WPF ライブ ビジュアル ツリー Visual Studio に標準搭載 ★(デバッグ中に見られる) XAML ホット リロード 実行中に XAML を書き換えて即反映 Snoop 老舗の OSS。実行中のアプリに外部からアタッチできる WPF Inspector 同種 【ライブ ビジュアル ツリー】 デバッグ実行中に [デバッグ] → [ウィンドウ] → [ライブ ビジュアル ツリー] ・要素を選択すると【画面上でハイライト】される ・[ライブ プロパティ エクスプローラー] で 【そのプロパティ値がどこから来たか】が分かる ★ → 依存関係プロパティの優先順位の確認に最適
VisualTreeHelper.GetChildrenCountの注意:・【テンプレートが適用される前は 0 を返す】★ → コンストラクタや Loaded 前に呼んでも子が見つからない → Loaded イベント以降、または ApplyTemplate() を呼んでから使う ・仮想化されている ItemsControl では、 【画面に出ていない項目は存在しない】
本項では、DependencyObject により実装される「WPF プロパティ システム」の
- 「依存関係プロパティ」
- 「添付プロパティ」
- 「プロパティ値の継承」
について説明する。
なお、それぞれのプロパティの定義方法などについても触れるが、
実際にこれらのプロパティを独自に定義する必要がある場合は、
カスタム コントロール作成時にあたる。
- Microsoft Learn > Windows Presentation Foundation
コントロール > コントロールのカスタマイズ > コントロールの作成の概要
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/controls/control-authoring-overview
「依存関係プロパティ」は、「WPF プロパティ システム」によってサポートされるプロパティで、
WPF に管理される一種の辞書構造を用いて値を保持する。
「依存関係プロパティ」では、変更監視・有効値検証などの機能が使用可能である。
- Microsoft Learn > WPF の基礎 > プロパティ > 依存関係プロパティの概要
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/properties/dependency-properties-overview
「依存関係プロパティ」は、下記で使用されている。
-
動的な「リソース」参照
- 「スタイル」
- 「アニメーション」
-
「データ バインディング」
-
「添付プロパティ」
-
「プロパティ値の継承」
「依存関係プロパティ」は、一般的なプロパティである CLR プロパティのように
定義するのではなく、「WPF プロパティ システム」に対し
「依存関係プロパティ」として登録する必要がある。
-
定義方法
以下、「依存関係プロパティ」の定義方法について説明する。- public static readonly(VB では、Public Shared ReadOnly)な
DependencyProperty 型の変数として宣言する。 - 「依存関係プロパティ」名の末尾に「Property」を付与する(WPF の名称付与基準)
- 変数宣言時か、静的コンストラクタを初期化する。
DependencyProperty.Register メソッドを使用し
「依存関係プロパティ」として「WPF プロパティ システム」に登録する。
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.dependencyproperty.register - 「依存関係プロパティ」は、プロパティの設定・取得方法が特殊なので、
CLR プロパティでラップし、プロパティにアクセスし易いようにする。
- public static readonly(VB では、Public Shared ReadOnly)な
-
実装例
以下、「依存関係プロパティ」の定義の実装例を示す。
// 依存関係プロパティを実装するクラス
public class ConcreteDependencyObject : DependencyObject {
// 依存関係プロパティ
public static readonly DependencyProperty CaptionProperty;
// 静的コンストラクタ
static ConcreteDependencyObject(){
// 静的コンストラクタで依存関係プロパティを登録
CaptionProperty = DependencyProperty.Register(
"Caption", // プロパティ名
typeof(string), // プロパティの型
typeof(ConcreteDependencyObject), // プロパティの所有者
new PropertyMetadata()); // 各種メタデータ
}
// 依存関係プロパティは設定・取得方法が特殊であるので、
// 以下のように、CLRプロパティでラップする。
public string Caption {
get { return this.GetValue(ConcreteDependencyObject.CaptionProperty) as string; }
set { this.SetValue(ConcreteDependencyObject.CaptionProperty, value); }
}
}- Microsoft Learn > WPF の基礎 > プロパティ
補足(CLR プロパティ ラッパーの重大な注意点): 実装例は正しいが、
ここに WPF 最大の落とし穴がある。【ラッパーには GetValue / SetValue 以外を書いてはならない】★ public string Caption { get => (string)GetValue(CaptionProperty); set { SetValue(CaptionProperty, value); DoSomething(); // ✗ ここに処理を書いてはいけない } } 【理由】 ・XAML から設定される場合、 【ラッパーを経由せず SetValue が直接呼ばれる】 ・バインディング、スタイル、アニメーションも同様 ・→ DoSomething() が【呼ばれないことがある】 【正しい書き方】 処理は【PropertyChangedCallback に書く】★現在の簡略化(.NET Community Toolkit):
// 手書きは冗長なので、スニペット(propdp)か // ソース ジェネレーターを使うのが現在の実務 [DependencyProperty] // ← Toolkit 等が生成 public partial string Caption { get; set; }
「依存関係プロパティ」では、以下のような機能が使用可能になる。
- プロパティの既定値の設定
- ソース プロパティの変更監視コールバックの設定
- プロパティ値の強制コールバックの設定
- プロパティ値の有効値検証コールバックの設定
なお、以下それぞれの機能の説明と、DependencyProperty.Register メソッドを使用して
「依存関係プロパティ」を登録する際の第 4 引数に指定する PropertyMetadata クラスと、
そのコールバックのコード例を示す。
- プロパティの既定値の設定
プロパティの既定値を設定する。
new PropertyMetadata("DefaultValue")-
Microsoft Learn > .NET Framework クラス ライブラリ > System.Windows.PropertyMetadata クラス
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.propertymetadata -
ソース プロパティの変更監視コールバックの設定
「依存関係プロパティ」が、有効なプロパティ値に変更された時に呼び出される
コールバックを設定する。
例えば、値を「最小値 ~ 最大値の間に強制する」などの用途で使用するコールバック。
new PropertyMetadata("DefaultValue",
new PropertyChangedCallback(OnPropertyChangedCallback))
// プロパティ値の変更監視コールバック
private static void OnPropertyChangedCallback(
DependencyObject d, DependencyPropertyChangedEventArgs e) {
// プロパティ値の変更監視の例:
string oldValue = (string)e.OldValue;
string newValue = (string)e.NewValue;
DependencyProperty dp = e.Property;
}-
Microsoft Learn > .NET Framework クラス ライブラリ > System.Windows.PropertyChangedCallback デリゲート
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.propertychangedcallback -
プロパティ値の強制コールバックの設定
「依存関係プロパティ」の値が再評価されたり、強制が明示的に要求されたりした場合に
呼び出されるコールバックを設定する。
new PropertyMetadata("DefaultValue",
new PropertyChangedCallback(OnPropertyChangedCallback),
new CoerceValueCallback(OnCoerceValueCallBack))
// 依存関係プロパティ値の強制コールバック
private static object OnCoerceValueCallBack(DependencyObject d, object value) {
// プロパティ値の強制の例:
// Captionプロパティ値に「"ChangeValue"」が設定されない限り
// Captionプロパティ値は「"DefaultValue"」を強制する。
if (value.ToString() == "ChangeValue") return value;
return "DefaultValue";
}-
Microsoft Learn > .NET Framework クラス ライブラリ > System.Windows.CoerceValueCallback デリゲート
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.coercevaluecallback -
プロパティ値の有効値検証コールバックの設定
「依存関係プロパティ」の値が再評価された際、有効値を検証するコールバックを設定する。
new PropertyMetadata("DefaultValue",
new PropertyChangedCallback(OnPropertyChangedCallback),
new CoerceValueCallback(OnCoerceValueCallBack),
new ValidateValueCallback(OnValidateValueCallback));
// プロパティ値の有効値検証コールバック
private static bool OnValidateValueCallback(object value) {
// プロパティ値の有効値検証の例:
// Captionプロパティ値に「" DengerousValue "」が設定されるとエラーになる。
if (value.ToString() == "DengerousValue") return false;
return true;
}- Microsoft Learn > .NET Framework クラス ライブラリ > System.Windows.ValidateValueCallback デリゲート
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.validatevaluecallback
移行メモ(
ValidateValueCallbackはPropertyMetadataの引数ではない): 最後の
コード例は、PropertyMetadataのコンストラクタに
4 つ目の引数としてValidateValueCallbackを渡す形になっているが、
PropertyMetadataにそのようなコンストラクタは存在しない。// ○ 正しい形:ValidateValueCallback は Register の【第 5 引数】★ DependencyProperty.Register( "Caption", typeof(string), typeof(ConcreteDependencyObject), new PropertyMetadata("DefaultValue", new PropertyChangedCallback(OnPropertyChangedCallback), new CoerceValueCallback(OnCoerceValueCallBack)), new ValidateValueCallback(OnValidateValueCallback)); // ← ここ3 つのコールバックの呼ばれる順序と違いを整理しておく。
【値が設定されるときの流れ】★ SetValue(dp, value) ↓ ① ValidateValueCallback(object value) → bool ・【インスタンスを受け取らない】(static な検証) ・false を返すと【ArgumentException】 ・「そもそもあり得ない値」を弾く(負の長さ等) ↓ ② CoerceValueCallback(DependencyObject d, object value) → object ・【インスタンスを見て値を補正する】 ・例: Value を Minimum ~ Maximum に丸める ・他プロパティとの関係で決まる制約に使う ↓ ③ PropertyChangedCallback(d, e) ・【確定した後】に呼ばれる ・副作用(再描画、他プロパティの更新)を書く場所 ★【使い分け】 Validate … 「この値は不正だ」→ 例外にする Coerce … 「この値はこう直す」→ 黙って補正する Changed … 「変わったので、これをする」→ 副作用移行メモ: 原文のサンプル中の「DengerousValue」は
DangerousValueの綴り誤りと読めるが、
サンプル内で一貫しているため、原文のまま残した。
「添付プロパティ」は、「WPF プロパティ システム」によってサポートされるプロパティで、
「添付プロパティ」のほとんどが「依存関係プロパティ」として実装される。
- Microsoft Learn > WPF の基礎 > プロパティ > 添付プロパティの概要
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/properties/attached-properties-overview
-
「添付プロパティ」は、親要素で定義されるプロパティに対し、
子要素がそれぞれ別の値を指定するなど、
任意のオブジェクトに対して設定可能なグローバル プロパティとして機能する。 -
「添付プロパティ」の代表的な使用例には、
「パネル要素」などの子要素を格納する親要素が、
子要素のレイアウトを制御するための各プロパティがある。 -
ここでは、
- 親要素として、Canvas 要素
- 子要素として、TextBlock 要素
を例に挙げて説明する。
TextBlock のレイアウトを制御するため、
- TextBlock に(仮に)CanvasTop、CanvasLeft プロパティを持たせた場合、
- TextBlock が、Canvas 要素の子要素ではない場合、
無駄なプロパティとデータを持つことになる。
この問題を解決するため、「添付プロパティ」は、
親要素が子要素の動作を制御するためのプロパティを
子要素から指定可能な一種のグローバル プロパティとして実装する。
以下は、レイアウトを制御する Canvas 要素の「添付プロパティ」である
Canvas.Top、Canvas.Left プロパティへ、
XAML から TextBlock 要素から値を設定する方法である。
<Canvas>
<TextBlock Canvas.Top="10" Canvas.Left="20">
Hello World!
</TextBlock>
</Canvas>子要素から親要素への「添付プロパティ」設定の際、
内部的には「添付プロパティ」のアクセッサ メソッド(後述)のキーに
子要素が指定される。
「添付プロパティ」を定義する場合は、「依存関係プロパティ」と異なり、
- DependencyProperty.Register メソッドではなく、
- DependencyProperty.RegisterAttached メソッドを使用し
「添付プロパティ」として登録する。
- Microsoft Learn > WPF の基礎 > プロパティ > 方法:添付プロパティを登録する
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/properties/how-to-register-an-attached-property- Microsoft Learn > .NET Framework クラス ライブラリ > System.Windows.DependencyProperty.RegisterAttached メソッド
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.dependencyproperty.registerattached
- Microsoft Learn > .NET Framework クラス ライブラリ > System.Windows.DependencyProperty.RegisterAttached メソッド
また、「添付プロパティ」は CLR プロパティ ラッパではなく、
- Get(プロパティ名)
- Set(プロパティ名)
の名称付与基準に従った、専用の静的 get、set アクセッサ メソッドを実装する必要がある。
public static void Setプロパティ変数名(UIElement element, Boolean value) {
element.SetValue(this.プロパティ変数, value);
}
public static Boolean Getプロパティ変数名(UIElement element) {
return (Boolean)element.GetValue(this.プロパティ変数);
}これらのアクセッサ メソッドは、XAML リーダが「添付プロパティ」を
XAML の属性として認識し、適切な型を解決できるようにするために必要となる。
コードビハインドから、これらのアクセッサ メソッドを使用して
「添付プロパティ」を設定する際は、キーに子要素を指定する。
<Canvas x:Name="myCanvas">
</Canvas>TextBlock textBlock = new TextBlock();
textBlock.Text = "Hello World!";
// 添付プロパティを設定
Canvas.SetTop(textBlock, 10);
Canvas.SetLeft(textBlock, 20);
this.myCanvas.Children.Add(textBlock);移行メモ(サンプル中の
this.): アクセッサ メソッドは
staticであるため、
this.プロパティ変数とは書けない(staticメンバからthisは参照できない)。// ○ 正しい形 public static void SetIsHighlighted(UIElement element, bool value) => element.SetValue(IsHighlightedProperty, value); // ← this なし ★ public static bool GetIsHighlighted(UIElement element) => (bool)element.GetValue(IsHighlightedProperty);補足(添付プロパティのもう 1 つの用途 ── ビヘイビア): 原文が挙げる
「レイアウト制御」以外に、現在よく使われる用途がある。【添付ビヘイビア(Attached Behavior)】★ 添付プロパティの PropertyChangedCallback を使って、 【既存のコントロールに振る舞いを後付けする】 例: 「TextBox にフォーカスが入ったら全選択する」 → TextBox を継承せずに実現できるpublic static class TextBoxBehavior { public static readonly DependencyProperty SelectAllOnFocusProperty = DependencyProperty.RegisterAttached( "SelectAllOnFocus", typeof(bool), typeof(TextBoxBehavior), new PropertyMetadata(false, OnChanged)); public static void SetSelectAllOnFocus(DependencyObject d, bool v) => d.SetValue(SelectAllOnFocusProperty, v); public static bool GetSelectAllOnFocus(DependencyObject d) => (bool)d.GetValue(SelectAllOnFocusProperty); static void OnChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is TextBox tb && (bool)e.NewValue) tb.GotFocus += (s, _) => ((TextBox)s!).SelectAll(); } }<TextBox local:TextBoxBehavior.SelectAllOnFocus="True" />【なぜ重要か】 ・MVVM では【コードビハインドを書きたくない】 ・添付ビヘイビアなら、XAML だけで振る舞いを付けられる ★ ・継承しないので、どのコントロールにも適用できる
-
「WPF プロパティ システム」では、包含継承
(HTML コードと同様に、親要素に書いた属性値が子要素に継承される。
HTML では、例えば、body 要素に対して style 属性や font 属性でフォント・サイズを
指定すると、body 要素内のすべての要素のフォント・サイズが変化する。
※ 注:オブジェクト指向プログラミングにおける継承
(派生クラスがその基本クラスからメンバ定義を継承する)とは異なる概念。)を
サポートしている。 -
これを実現するのが「プロパティ値の継承」の機能である。
- 「プロパティ値の継承」がされる一部のプロパティは、
「要素ツリー」の最も近い親要素から特定のプロパティの値を継承し、
親要素のプロパティ値が変更された場合、自動的に子要素に反映する。 - この動作は、子要素から親要素のプロパティを設定する「添付プロパティ」と
逆の働きであるため、
「プロパティ値の継承」の仕組みも、「添付プロパティ」と同様の方式で
実装されていることが分かる
(そのため、「添付プロパティ」と同様、プロパティの登録に
DependencyProperty.RegisterAttached メソッドを使用する)。
- 「プロパティ値の継承」がされる一部のプロパティは、
-
Microsoft Learn > WPF の基礎 > プロパティ > プロパティ値の継承
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/properties/property-value-inheritance
-
包含継承の機能は、親要素、例えば、ルート要素(Window or Page)に
一度だけ定義することで、画面全体のポリシーを統一するようなプロパティに適している。 -
「プロパティ値の継承」がされるプロパティの一例として、以下のものがある。
- Control.FontSize プロパティ
- FrameworkElement.FlowDirection プロパティ.etc
DependencyProperty.RegisterAttached メソッドの第 4 引数に指定する
PropertyMetadata(FrameworkPropertyMetadata)クラスのオプション設定で、
「プロパティ値の継承」を可能に設定する。
- Microsoft Learn > WPF の基礎 > プロパティ > プロパティ値の継承 - カスタム プロパティを継承可能にする
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/properties/property-value-inheritance
なお、「プロパティ値の継承」のブロッキング境界として、Frame などのコントロールを使用できる。
補足(設定は
FrameworkPropertyMetadataOptions.Inherits): 「オプション設定で」
の具体形を示しておく。public static readonly DependencyProperty ThemeProperty = DependencyProperty.RegisterAttached( "Theme", typeof(string), typeof(MyClass), new FrameworkPropertyMetadata( "Light", FrameworkPropertyMetadataOptions.Inherits)); // ← これ ★【FrameworkPropertyMetadataOptions の主なもの】 Inherits … 【値が子要素に継承される】★ AffectsMeasure … 変更時に Measure をやり直す AffectsArrange … 変更時に Arrange をやり直す AffectsRender … 変更時に再描画する BindsTwoWayByDefault … 既定で TwoWay バインディング NotDataBindable … バインディング不可 → 【カスタム コントロールでは AffectsMeasure / AffectsRender を 正しく指定しないと、値を変えても画面が変わらない】★
FlowDirectionが継承される意味は
国際化対応項目 と関わる。・アラビア語・ヘブライ語は右から左(RTL) ・Window の FlowDirection を RightToLeft にすると 【配下の全要素が反転する】★ ・国際化対応で「レイアウトを 2 通り作る」必要がなくなる
WPF では、
- ビュー(XAML:データの表示)と
- モデル(コードビハインド:チェック処理や、データアクセス処理など
アプリケーションの処理)
を分離するための仕組みとして、「データ バインディング」という機能を提供している。
- 連載 WPF/Silverlight UIフレームワーク入門:
第2回 データの表示と入力に必要な知識 (1/5) - @IT
https://atmarkit.itmedia.co.jp/ait/articles/0905/19/news145.html
これには、前述の「依存関係プロパティ」を使用している。
-
「データ バインディング」を利用すれば、
- ビューとモデルの接点を、任意のプロパティ接続だけに限定できる。
- このため、ビューの内部にビューとモデルを接続するロジックを持つ必要が無くなり、
ビュー内のコード量を削減すると同時に疎結合を実現できる。
-
「データ バインディング」により、
2つのオブジェクトを結合するが、通常、- ビュー側の XAML で生成された CLR オブジェクトを「バインディング ターゲット」(=結合先)
- モデル側のコードビハインドで生成された CLR オブジェクトを「バインディング ソース」(=結合元)
と呼ぶ。
この2つのオブジェクトを結合するために Binding オブジェクトを使用する。
-
なお、
- 「バインディング ターゲット」に入力されたデータや、
- 「バインディング ソース」の更新通知によるデータの、
妥当性検証の機能は、(継承する)
「WPF プロパティ システム」を実装する DependencyObject により提供される。 -
また、この「バインディング ソース」からの更新通知の機能は、
(実装する)INotifyPropertyChanged インターフェイスにより提供される。
それぞれ、これに合わせた実装が必要になる。
このため、「データ バインディング」の機能は、
以下の3つの要素によって提供されると言って良い。
- DependencyObject オブジェクト
- INotifyPropertyChanged インターフェイス
- Binding オブジェクト
ビュー側「バインディング ターゲット」の DependencyObject オブジェクト
- System.Windows.DependencyObject クラス
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.dependencyobject
モデル側「バインディング ソース」の INotifyPropertyChanged インターフェイス
- System.ComponentModel.INotifyPropertyChanged インターフェイス
https://learn.microsoft.com/ja-jp/dotnet/api/system.componentmodel.inotifypropertychanged
上記2つの要素を結び付ける Binding オブジェクト
- System.Windows.Data.Binding クラス
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.data.binding
補足(コレクションには
INotifyCollectionChangedが要る): 3 要素の
説明は正確だが、コレクションを扱う場合にもう 1 つ必要である。【要素の追加・削除を画面に反映するには】 INotifyPropertyChanged … 【プロパティの変更】を通知する INotifyCollectionChanged … 【コレクションの変更】を通知する ★ → List<T> をバインドしても、Add/Remove が画面に反映されない → 【ObservableCollection<T>】を使う// ✗ 追加しても画面が変わらない public List<Item> Items { get; } = new(); // ○ 追加・削除が反映される public ObservableCollection<Item> Items { get; } = new();【注意点】★ ・ObservableCollection は【UI スレッドからのみ変更する】 → 別スレッドから Add すると例外 → [Control.Invoke、.BeginInvoke](MS_ControlInvoke) と同じ問題 → .NET 4.5 以降は BindingOperations.EnableCollectionSynchronization で緩和可 ・要素【自身】のプロパティ変更は、 要素の型が INotifyPropertyChanged を実装している必要がある ・大量件数の一括追加は【1 件ずつ通知が飛んで重い】 → 一旦 ItemsSource を外す、または別コレクションを作って差し替える
Binding クラスが持つ主要なプロパティ
| 項番 | プロパティ | 説明 |
|---|---|---|
| 1 | Mode | データが反映される方向を、以下の列挙型から選択して指定する。 ・OneWay:「バインディング ソース」変更時、「バインディング ターゲット」を更新する。 ・OneWayToSource:「バインディング ターゲット」変更時、「バインディング ソース」を更新する。 ※ WPF のみで、「Silverlight」には無いモード ・TwoWay:「バインディング ソース」、「バインディング ターゲット」が変更された場合、対応する「バインディング ターゲット」、「バインディング ソース」を更新する。 ・OneTime:アプリケーション起動時のみ、「バインディング ターゲット」のデータを更新(StaticResource に接続する場合などに使用する) ・Default:「バインディング ターゲット」の既定の Mode となる。 ※ WPF のみで、「Silverlight」には無いモード |
| 2 | Source | 「バインディング ソース」を指定する。 |
| 3 | Path | 「バインディング ソース」のプロパティ名を指定する(名前空間のフルパスで指定可)。 |
補足(
Modeの既定と、UpdateSourceTrigger): 表に加えて、
実務で必ず必要になる 2 点を補う。【Mode の既定(Default)が何になるか】★ ・依存関係プロパティごとに決まっている ・TextBox.Text、CheckBox.IsChecked 等 → 【TwoWay】(BindsTwoWayByDefault が付いている) ・TextBlock.Text、Label.Content 等 → 【OneWay】 → 「明示しなくても双方向になる」ものと 「明示しないと片方向のまま」のものがある【UpdateSourceTrigger ── いつソースへ書き戻すか】★ PropertyChanged … 【入力するたび】に即座に反映 LostFocus … フォーカスが外れたとき(TextBox.Text の既定) Explicit … UpdateSource() を呼んだとき → 「入力しても ViewModel の値が変わらない」の典型的な原因は LostFocus のままになっていること<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
Source以外の指定方法も重要である。Source … 明示的にオブジェクトを指定 ElementName … 【他の要素】を指定({Binding ElementName=slider1, Path=Value}) RelativeSource … 【相対的に探す】 ・Self → 自分自身 ・TemplatedParent → テンプレートの適用先 ・FindAncestor → 【ビジュアル ツリーを遡って型で探す】★ ・PreviousData → 前の項目 (指定しない) … DataContext を使う(後述)<!-- ItemTemplate の中から、外側の ViewModel のコマンドを呼ぶ(頻出) --> <Button Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=ListBox}}" CommandParameter="{Binding}" />
「データ バインディング」は、XAML 上の要素のプロパティ値として
「バインディングのマークアップ拡張」である
「{Binding ・・・}」という記述を使用して Binding オブジェクトを生成・初期化することで実装する。
-
Microsoft Learn > .NET Framework クラス ライブラリ > FrameworkElement.DataContext プロパティ
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.frameworkelement.datacontext -
「バインディング ソース」と「バインディング ターゲット」の数が増え、
それぞれのオブジェクト間のマッピングが面倒な場合、
Binding オブジェクトの規定のターゲットである DataContext プロパティに
「バインディング ソース」を設定することで、
Binding オブジェクトへの「バインディング ソース」の指定が不要になる。 -
「データ バインディング」では、「バインディング ターゲット」と
「バインディング ソース」が、リフレクションによるプロパティ名にて結び付けられている。
このため、FrameworkElement.DataContext プロパティに、任意のカスタム型を渡すことができる。 -
なお、DataContext プロパティは、「プロパティ値の継承」に対応している。
このため、指定した「バインディング ソース」は、親要素から子要素に継承されるので、
親要素(もしくは自身)の DataContext プロパティに
「バインディング ソース」を1回指定すればよい。
補足(
DataContextが MVVM の土台): 「プロパティ値の継承に対応している」
という点が決定的である。【これにより】 Window の DataContext に ViewModel を 1 回設定すれば、 配下のすべての要素が【同じ ViewModel を見る】★ <Window DataContext="{Binding MainViewModel, Source=...}"> <Grid> <TextBox Text="{Binding Name}" /> ← ViewModel.Name <Button Command="{Binding SaveCommand}" /> </Grid> </Window> → これが MVVM が成立する理由 → View は ViewModel の【型を知らない】(リフレクションで解決)【DataContext がずれる典型例】★ ・ItemsControl の中 → DataContext は【その項目】になる ・ContextMenu / Popup / ToolTip → 【ビジュアル ツリーが別】なので DataContext が継承されない → PlacementTarget 経由で辿る必要がある → 「バインディングが効かない」ときは まず【今の DataContext が何か】を疑う【バインディングのデバッグ】★ ・失敗しても【例外にならない】(出力ウィンドウに警告が出るだけ) ・PresentationTraceSources.TraceLevel を使うと詳細が見える <TextBlock Text="{Binding Name, diag:PresentationTraceSources.TraceLevel=High}" /> ・または Visual Studio の [出力] ウィンドウで "System.Windows.Data Error" を探す
-
「データ バインディング」では、
- 「バインディング ターゲット」と
- 「バインディング ソース」で
プロパティ型が異なる場合、暗黙的な型変換が行われる。
-
型変換の動作のカスタマイズの必要性
-
暗黙の型変換が失敗する場合
-
入力数値から背景色を変更するような処理を実装したい場合。
-
-
型変換の動作のカスタマイズの実装
-
暗黙の型変換(または値変換)が失敗する場合、null が返される。
この対応として IValueConverter インターフェイスを実装した
「値コンバータ」を実装することで明示的な型変換(または値変換)が可能になる。 -
なお、「値コンバータ」で型変換できない場合も同様に、null を返すように実装する。
null が返された場合、通常は何も表示されないが、これをカスタマイズする場合は、
Binding.TargetNullValue プロパティを実装し、
ソース値が null のときに返される値を指定する。
-
補足(値コンバータの実装と注意点):
public class BoolToVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => (value is bool b && b) ? Visibility.Visible : Visibility.Collapsed; public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => value is Visibility v && v == Visibility.Visible; }【実装上の注意】★ ① 【CultureInfo を無視しない】 → 数値・日付の表示は文化圏で変わる → [カルチャ](MS_Culture) / [国際化対応項目](MS_InternationalizationItems) ② 【例外を投げない】 → バインディングは例外を握りつぶすため、原因が分からなくなる → 変換できないときは DependencyProperty.UnsetValue を返す ③ 【ConvertBack を実装しないなら】 → NotImplementedException ではなく Binding.DoNothing を返す → OneWay で使う前提なら、それを明示する ④ 【ステートレスにする】 → 1 つのインスタンスが複数箇所で共有される【値コンバータを使わずに済む場合】 ・BooleanToVisibilityConverter は【標準で用意されている】 ・{Binding X, StringFormat='{}{0:N0} 円'} で書式指定できる ・DataTrigger でも同等のことができる → [XAMLの書き方(1)](MS_XAMLWriting1) 参照 → 【コンバータを増やしすぎない】方が保守しやすい ★
-
コードビハインドからのデータ バインディング
「データ バインディング」の基本的な動作を理解するために、
Binding オブジェクトをコードビハインドで自作し、
「バインディング ソース」と「バインディング ターゲット」を
OneWay モードで結び付け、「データ バインディング」を行う。- 連載 WPF/Silverlight UIフレームワーク入門:第2回 データの表示と入力に必要な知識 (1/5) - @IT
https://atmarkit.itmedia.co.jp/fdotnet/vblab/uiframework_02/uiframework_02_01.html
- 連載 WPF/Silverlight UIフレームワーク入門:第2回 データの表示と入力に必要な知識 (1/5) - @IT
-
「バインディング ソース」に DataContext を使用して、
「バインディング ソース」と「バインディング ターゲット」を
OneWay モードで結び付け、「データ バインディング」を行う。- 連載 WPF/Silverlight UIフレームワーク入門:第2回 データの表示と入力に必要な知識 (2/5) - @IT
https://atmarkit.itmedia.co.jp/fdotnet/vblab/uiframework_02/uiframework_02_02.html
- 連載 WPF/Silverlight UIフレームワーク入門:第2回 データの表示と入力に必要な知識 (2/5) - @IT
-
(1)に以下を加えて、入力値の自動計算アプリケーションを作成できる。
- (1)と同じ、OneWay モードで結び付け、「データ バインディング」を行う。
- OneWayToSource モードで結び付け、「データ バインディング」を行う。
- 変更通知を追加し、計算された値を「バインディング ターゲット」に反映する。
-
変更通知を追加方法
- OneWay の「バインディング ソース」には、INotifyPropertyChanged インターフェイスを
実装して、プロパティ値の変更通知処理を実装する必要がある。 - なお、UI 要素の表示に関するプロパティは、基本的に「依存関係プロパティ」として
実装されており、既定で変更通知をサポートしている。このため、既定で双方向接続が可能である。
- OneWay の「バインディング ソース」には、INotifyPropertyChanged インターフェイスを
-
連載 WPF/Silverlight UIフレームワーク入門:第2回 データの表示と入力に必要な知識 (3/5) - @IT
https://atmarkit.itmedia.co.jp/fdotnet/vblab/uiframework_02/uiframework_02_03.html
TwoWay モードで結び付け、「データ バインディング」を行う。
以下に値コンバータを適用できる。
-
OneWay モードを使用した「データ バインディング」
TwoWay モードに対応した「値コンバータ」は、Convert メソッドを実装する必要がある。 -
OneWayToSource モードを併用した「データ バインディング」
TwoWay モードに対応した「値コンバータ」は、ConvertBack メソッドを実装する必要がある。 -
TwoWay モードを使用した「データ バインディング」
TwoWay モードに対応した「値コンバータ」は、Convert、ConvertBack メソッドの双方を
実装する必要がある。
移行メモ(3 項目の記述が「TwoWay モードに対応した」で始まっている): それぞれ
OneWay / OneWayToSource / TwoWay に対応する説明のはずだが、
3 つとも「TwoWay モードに対応した」で始まっている(コピー時の誤りと読める)。
内容としては以下が正しい。OneWay を使う値コンバータ … Convert のみ実装すればよい OneWayToSource を使う値コンバータ … ConvertBack のみ実装すればよい TwoWay を使う値コンバータ … 【両方】実装する必要がある
-
ItemsSource へのデータ バインディング
ItemsControl から派生した要素の ItemsSource 属性にコレクションを
「データ バインディング」する場合、
対象オブジェクトは反復処理をサポートしている必要がある。 -
インデクサによるデータ バインディング
DataGrid の列に DataTable の指定の列をデータバインディングする場合、
XAML の「バインディングのマークアップ拡張」にて、Binding プロパティである
Path 属性に角括弧を指定することで、インデクサを使用して接続可能である。
補足(インデクサ バインディングの書き方): 実務で使う機会が多いので
具体例を示しておく(ASP.NET MVCでDataTableを使用する。 の
WPF 版に当たる)。<!-- DataTable の列名でバインドする --> <DataGrid ItemsSource="{Binding MyDataTable}" AutoGenerateColumns="False"> <DataGrid.Columns> <DataGridTextColumn Header="名前" Binding="{Binding [Name]}" /> <DataGridTextColumn Header="金額" Binding="{Binding [Price], StringFormat={}{0:N0}}" /> </DataGrid.Columns> </DataGrid>【DataTable が扱える理由】 DataView が ITypedList / IBindingList を実装しており、 WPF がそれを解釈する → List<T> と違い、【列の追加・削除も反映される】 → ただし型情報が弱く、コンパイル時に検証されない 【現在の推奨】 ・可能なら【POCO のコレクション】にする ・列が動的に決まる画面なら DataTable も選択肢 ([ASP.NET MVCでDataTableを使用する。](MS_ASPNETMVCDataTable) と同じ判断)
イベントを生成した UI コントロール上だけでなく、
「ビジュアル ツリー」内の複数のリスナ上でイベント ハンドラを呼び出すことができる
WPF のイベント。
例えば、Button コントロールが「テンプレート」を使用し、
複数のコントロールから構成される場合、
下位のコントロールのイベントを上位の Button コントロールでもハンドルできる。
- Microsoft Learn > WPF の基礎 > イベント(WPF) > ルーティング イベントの概要
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/advanced/routed-events-overview
「ルーティング イベント」は、3つのルーティング方法のいずれかを使用する。
| 項番 | 区分 | 機能 |
|---|---|---|
| 1 | トンネル | ツリーを下方向へ辿る。 最初に、「ビジュアル ツリー」のルートのイベント ハンドラが呼び出され、次に、イベントを発生させた要素に到達するまで、「経路」沿いの「トンネル ルーティング イベント」のイベント ハンドラを呼び出す。 なお、「トンネル ルーティング イベント」のイベント名は、先頭に「Preview」を付与するルールとなっている。 |
| 2 | バブル | ツリーを上方向へ辿る。 最初に、イベントを発生させた要素のイベント ハンドラが呼び出され、次に、「ビジュアル ツリー」のルートに到達するまで、「経路」沿いの「バブル ルーティング イベント」のイベント ハンドラを呼び出す。 |
| 3 | 直接 | イベントを発生させた要素のみに、イベント ハンドラを呼び出す機会が与えられる。 これは、Windows フォームのイベントと似ている。ただし、標準の CLR イベントとは異なり、「クラス処理」をサポートする。 |
- Microsoft Learn > WPF の基礎 > イベント
ルーティング イベントの処理済みとしてのマーキング、およびクラス処理
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/advanced/marking-routed-events-as-handled-and-class-handling
多くの場合、WPF から提供される入力イベントは、
「トンネル ルーティング イベント」 / 「バブル ルーティング イベント」ペアとして
実装される。
イベントを発生元の要素で処理する限り、「ルーティング イベント」の動作は
あまり表面には見えないので、
イベントが「ルーティング イベント」として実装されていることに
気を配る必要はない。
これは、「ルーティング イベント」は、
- 共通ハンドラを定義する場合や、
- カスタム コントロールを複合化する場合などの、
「特定のシナリオ」で効果を発揮するためである。
例えば、以下は、Button コントロールのイベントを発生元の要素で処理する
イベント ハンドラを実装する例である。
この場合、WPF のデザイナに Button コントロールを D & D し、
これをダブル クリックすることで、
発生元の要素で処理する Button.Click イベント ハンドラを実装できる。
<Grid>
<Button Height="23" Name="button1" Click="button1_Click">Button</Button>
</Grid>public partial class Window1 : Window {
public Window1() {
InitializeComponent();
}
private void button1_Click(object sender, RoutedEventArgs e) {
// button1_Click
}
}XAML では、イベント リスナである要素の属性にイベント ハンドラを指定することで、
「型コンバータ」により、イベント ハンドラを設定できる。
上記の Click="button1_Click" が、それに該当する。
- コードビハインド
なお、イベントの指定を XAML の「型コンバータ」からではなく、
コード (C#) から指定する場合は、
次の 2 種類の方法(メソッド、演算子)で行うことができる。-
UIElement.AddHandler メソッドを利用する場合:
this.button1.AddHandler(Button.ClickEvent, new RoutedEventHandler(button1_Click));
-
オーバーロードされた演算子を利用する場合:
this.button1.Click += new RoutedEventHandler(button1_Click);
-
補足(
AddHandlerにしかできないこと): 2 つは等価に見えるが、
AddHandlerには第 3 引数がある点が重要である。// handledEventsToo: true にすると、 // 【e.Handled = true にされた後でも】ハンドラが呼ばれる ★ this.button1.AddHandler(Button.ClickEvent, new RoutedEventHandler(button1_Click), handledEventsToo: true);【使う場面】 ・TextBox の PreviewMouseDown が内部で Handled にされていて、 自前のハンドラが呼ばれない ・ListBoxItem の選択処理に割り込みたい → 「イベントが飛んでこない」ときの最終手段 ★
下記の XAML は、
- 「トンネル ルーティング イベント」:PreviewMouseDown イベント
- 「バブル ルーティング イベント」:MouseDown イベント
の動作を確認するためのサンプル。
StackPanel - Border - Rectangle
<StackPanel x:Name="stackPanel1"
Height="100" Width="100" Orientation="Vertical"
MouseDown="stackPanel1_MouseDown"
PreviewMouseDown="stackPanel1_PreviewMouseDown">
<Border x:Name="border1"
MouseDown="border1_MouseDown"
PreviewMouseDown="border1_PreviewMouseDown">
<Rectangle x:Name="rect1"
Height="100" Width="100" Fill="Black"
MouseDown="rect1_MouseDown"
PreviewMouseDown="rect1_PreviewMouseDown">
</Rectangle>
</Border>
</StackPanel>/// <summary>stackPanel1のMouseDownイベント</summary>
private void stackPanel1_MouseDown(object sender, MouseEventArgs e) {
System.Diagnostics.Debug.WriteLine("→ stackPanel1_MouseDown");
}
/// <summary>stackPanel1のPreviewMouseDownイベント</summary>
private void stackPanel1_PreviewMouseDown(object sender, MouseEventArgs e) {
System.Diagnostics.Debug.WriteLine("→ stackPanel1_PreviewMouseDown");
}
/// <summary>border1のMouseDownイベント</summary>
private void border1_MouseDown(object sender, MouseEventArgs e) {
System.Diagnostics.Debug.WriteLine("→ border1_MouseDown");
}
/// <summary>border1のPreviewMouseDownイベント</summary>
private void border1_PreviewMouseDown(object sender, MouseEventArgs e) {
System.Diagnostics.Debug.WriteLine("→ border1_PreviewMouseDown");
}
/// <summary>rect1のMouseDownイベント</summary>
private void rect1_MouseDown(object sender, MouseEventArgs e) {
System.Diagnostics.Debug.WriteLine("→ rect1_MouseDown");
}
/// <summary>rect1のPreviewMouseDownイベント</summary>
private void rect1_PreviewMouseDown(object sender, MouseEventArgs e) {
System.Diagnostics.Debug.WriteLine("→ rect1_PreviewMouseDown");
}このコードを実装して、画面上の Rectangle をクリックすると、
以下のような Debug 出力を確認でき、この出力から
「トンネル ルーティング イベント」 / 「バブル ルーティング イベント」ペアが
正しく動作していることを確認できる。
stackPanel1_PreviewMouseDown
→ border1_PreviewMouseDown
→ rect1_PreviewMouseDown
→ rect1_MouseDown
→ border1_MouseDown
→ stackPanel1_MouseDown
なお、各イベント ハンドラで「e.Handled = true」を実行すると、ルーティングは停止する。
例えば、「トンネル ルーティング イベント」でルーティングを停止すれば、
「バブル ルーティング イベント」も発生しなくなる。
補足(このサンプルが示す実務上の教訓): 出力順が
**「上から下(Preview)→ 下から上(本イベント)」**になることを
実測で示している点が有用である。【設計上の含意】★ ① 【Preview で握りつぶせる】 親が Preview で e.Handled = true にすると、 子は【イベントを受け取れない】 → 入力の一括禁止、ショートカットの横取りに使える ② 【子の処理を親で拾える】 Button のテンプレート内の Border をクリックしても、 Button.Click が発生する → これがテンプレートを自由に組める理由 ③ 【共通処理を 1 箇所に書ける】 Window で TextBox.GotFocus をまとめて拾い、 全 TextBox を全選択する、等【e.Handled の落とし穴】★ ・Handled にされたイベントは、以降のハンドラに【届かない】 ・WPF の組み込みコントロールは【内部で Handled にしている】 → 例: TextBox は PreviewKeyDown の一部を消費する → 「なぜかキー イベントが来ない」の原因 ・対策: AddHandler(..., handledEventsToo: true)(前述)移行メモ: サンプルのイベント ハンドラの引数は
MouseEventArgsとなっているが、
MouseDown/PreviewMouseDownの実際のハンドラ型は
**MouseButtonEventHandler(MouseButtonEventArgs)**である。
MouseButtonEventArgsはMouseEventArgsを継承しているため
概念的には誤りではないが、この通りに書くとコンパイルは通らない。// ○ 正しいシグネチャ private void rect1_MouseDown(object sender, MouseButtonEventArgs e) { }
Tags: 移行, .NET開発, UIサブシステム, WPF/Silverlight, XAML