WPF ― 雑学
WPF(Windows Presentation Foundation)が.Net Frameworkの3.0で導入さて相当に年月が経っているにもかかわらず、小生は使いこなすには程遠い状態である。GDI+に慣れるために要した時間よりはるかに長くかかっている。もちろん、小生の仕事の内容が変わってプログラミングの時間が極端に減ったことも原因だが、文書・資料が少ないことが大きな原因とも考えている。ここでは試行錯誤して得た知識を備忘録も兼ねて共有する。

GUIスレッド上での連続(繰り返し)処理

結論を先に

download NwWPFDispatcherInvokeAsync.zip (ソース・ファイル:VS2022, .Net8)

繰り返したい処理を記述したメソッドをDispatcher.InvokeAsyncで呼び出し、そのメソッドの中で自らをDispatcher.InvokeAsyncする。

DispatcherPriorityは繰り返したい処理の優先度や頻度に応じて適切に設定する。マウス操作を妨げる可能性がある、妨げてはならないならBackGroundより低くするべきである。

繰り返したい処理が短いものであって、繰り返し間隔が十数ミリ秒を超えて(毎秒の繰り返し回数が50回程度までで)良ければ、メソッドにThread.Sleep(1)を加えておく。これでCPU時間を消費せずに済む。

背景

長時間を要する処理を、できればシングルスレッドで記述したい。マルチスレッドの複雑さを避けたいからである。

あるいは、別スレッド上での進捗を定期的にGUIスレッドで監視・表示・制御する「繰り返し」処理が必要なこともある。

何らかのタイマーを使っても良いが間隔の適切な設定が面倒である。長すぎて無駄(無用な待ち時間が生じる)だったり、短すぎて無駄(何も処理することがない呼び出しが多数発生する)だったり。

CPU時間をほとんど消費せず、短く処理を済ませては「自らを呼び出して」待機し、すぐまた呼び出してもらえる方法が欲しい。

解決

WPFであれば、システムに備わるメッセージ・キューの機能を.Net Frameworkや.Netの機能として呼び出すことができる。DispatcherのInvokeAsyncのメソッドによる。

これは本来GUIの操作を別スレッドからの要求に応じて行うためのものである。言い換えると、GUIスレッド上でのメソッドの実行を別スレッドから呼び出す機構である。さらに言い換えると、GUIスレッドで処理すべきことが「メッセージ」としてキューに蓄えられる中に、メソッド呼び出しの「メッセージ」も登録する機能である。これにより、キーボードやマウスによる操作が「メッセージ」として送られGUIスレッド上でイベントが発生する(メソッドが呼び出される)ことと同様に、特定のメソッドの呼び出しも登録し順次実行することが可能である。

DispatcherにはInvokeメソッドもあるが、これは他のスレッドで用いるもので、GUIスレッドでメソッド実行が完了するまで待機する。本件はGUIスレッド上で使うこと、次回処理の「メッセージ」を登録だけすることを意図しているのでInvokeAsyncを使う。

繰り返しをプロセスやスレッドの切り替え無しに行う場合、CPU時間を必ず消費する。プロセスやスレッドの切り替えを伴ってよければ、Thread.Sleepを用いることでCPU時間の消費を避けられる。しかし、プロセスやスレッドを切り替えるタイミングはシステムタイマーに依存しており、タイマーの解像度十数ミリ秒が繰り返し間隔の下限となる。

改訂日 内 容
2025/3/1 BeginInvokeは古いメソッド。新しいInvokeAsyncを使うべきなので修正。

Windowへの描画

結論を先に

download NwWPFVisualOnWindow.zip (ソース・ファイル:VS2022, .Net8)

描画した(実際の描画ではなく、正確には、描画シーケンスを内蔵した)Visualを作って、WindowのVisualの「子」に加える。具体的には、Windowクラス基底のAddVisualChildメソッドを呼び出すとともに、VisualChildrenCountプロパティーをオーバーライドして加えた「子」の数だけ増やしてやり、GetVisualChildrenメソッドもオーバーライドして加えた「子」も呼び出されるようにする。

背景

Windowに描画する方法を調べるとXAMLのことばかり出てくる。XAMLで書けばコードは要らないというわけである。しかし小生はXAMLでは描けない複雑な画をコードで描きたい。しかも軽量に動作させたい。

練習でちょっとした画を描こうにも、FrameworkElementのShapeを使うとコードの量が非常に多くになるし、動作も重い。

GDI+のOnPaintのように描画の本質部分だけをコードで記述したいし、また、WPFのretained modeの描画の恩恵も受けたい。

解決(とりあえず。まだまだ勉強中。)

DrawingVisualクラスから派生させるコーディングとして、コンストラクタにおいてDrawingContextに対する描画の指示(シーケンス)を記述する。これがGDI+のOnPaintに書くことに相当する。

シーケンスを保持したVisualをWindowのVisualの「子」に加える。具体は上述のとおり。

ここまでで、関係するオブジェクトのインスタンス化においてシーケンスの記述が実行されるのだが、あくまで、シーケンスがシステムに登録されるにすぎない。シーケンスに従った実際の描画はシステム(.Net Framework、.NetやWindows)に任される。

シーケンスの記述に、様々なオブジェクトとそのプロパティーが登場する。PenやそのBrush、Geometryやその座標情報、Transform(移動、拡大・縮小、回転)やその値などなど。これらは全てシーケンスを成す「変数」として保持されている。一度描画された後でもコードで「変数」の値を変化させれば、その値を反映して自動的に再描画される。時間経過とともに値を連続的に変化させればアニメーションに見える。

また、条件に応じてある描画を出現させたり消したりしたい。GDI+のOnPaintへの記述では、条件分岐させて描いたり描かなかったりすればよかった。この点で、WPFでは考え方を変える必要がある。「保持されている描画シーケンス」を「部品」に分解して意識すべきであり、条件に応じてどの部品を使うか使わないかというコーディングになる。DrawingContextを開いて描画の指示(シーケンス)をやりなおすコーディングは、実行が重くなるので、避けるべきである。

WPFでは「部品」の作り方に選択肢がある。低いレベルから順にGeometryオブジェクト、Drawingオブジェクト、Visualから派生したオブジェクトとなろう。Geometry、Drawing、VisualいずれもGroupやCollectionが用意されており「部品」を加えたり除いたりできる。この「部品リスト」もまた「保持されている描画シーケンス」なのである。


Windowの「内部」の大きさ

結論を先に

download NwWPFWindowClientSize.zip (ソース・ファイル:VS2022, .Net8)

Windowクラスは基底でVisualオブジェクトを「子」としてひとつ持つ。この「子」の実態がBorderオブジェクトでありWindowの「内部の枠」である。このBorderオブジェクトのRenderSizeプロパティーが、GDI+のFormクラスにあるClientSizeプロパティーに相当する。

背景

WindowクラスはWidthやHeightのプロパティーを持つが、これらはWindowにFrameworkElementオブジェクトを配置したり直接描画したりするときに参照する大きさとしては使えない。Windowの外形の大きさを示しているようである。

FrameworkElementクラスにはRenderSizeプロパティーがある。WindowクラスはFrameworkElementクラスから派生しているのでRenderSizeプロパティーを持つが、これもまたWindowの外形の大きさを示しているようである。

GDI+のFormクラスにあるClientSizeプロパティーに相当するものがWPFのWindowクラスには無いのである。

一方で、Windowオブジェクト上に、あるいは「内部」に、配置されるFrameworkElement(例えばコントロール)は、配置の状況いわゆるレイアウトに応じてRenderSizeの数値を持つ。Windowの「内部」いっぱいの大きさで配置されるFrameworkElementであれば本件で欲しい情報をRenderSizeとして持っているはずである。大抵の場合はこの役割をPanelオブジェクトが担う。

しかし、わざわざPanelのようなFrameworkElementを配置せずとも、Windowに直接描画するときに「内部」の大きさを知りたいのである。

解決(入手に苦労した情報)

巷の多くの情報はWindowに他のFrameworkElementを載せることに言及している。しかし本件を解決する鍵は、Windowクラスが基底でひとつFrameworkEementを内蔵しており、このFrameworkElementにアクセスできることである。

Windowは基底でVisualChildrenCountプロパティーが「1」である。すなわち、ひとつVisualを「子」として持っておりGetVisualChild(0)でアクセスできる。このVisualの実態がBorderであることが知られており、Windowの「内部」いっぱいの大きさで配置されているFrameworkElementなのである。

つまり、このBorderのRenderSizeが欲しい情報、Windowの「内部」の大きさである。