かのろぐ

日常と、なかよく。

邪道式 Windows Presentation Foundation

世間ではUWPだ、Electronだという声が大きくなっていますが、今更WPFの話をしようと思います。 というのも最近は、WPFのレイアウトに関する長年の悔恨をそろそろ晴らしておきたいと思いまして、とある難題を調べ漁る毎日を送っていたからです。 あと、最近ブログにプログラミングの話題を書いていなかった上に、そもそもブログに文章を書いていなかったので、長文記述力を取り戻すためにもここらで何か書いておくかという思惑もあります。

※この内容は続き物な気がしますが、次回投稿があるかどうかは主に気力がないので不明です。ということで、第n回みたいなタイトルは付けないでおこうと思います。

やっていること

拙作のTwitterクライアント「Krile StarryEyes」にはタイムラインのスクロール制御を組み込んでいるのですが、こいつが動いたり動かなかったりと 中々適当な挙動を見せてくれるうえに、ついに最近(.NET Framework 4.6以降?)は全く動かなくなったとの話も聞きます。

KrileのタイムラインはWPFのリスト、より詳しく言えばピクセルベースのスクロールとコンテナリサイクルを有効にしたVirtualizingStackPanelを レイアウトに使用するItemsControlで構成されています。こいつが思い通りに動いてくれないわけです。

これをなんとかするため、VirtualizingStackPanelとItemsControl、ScrollViewerの関係を探っています。主にReference Sourceを読み漁り、たまにMSDNを読んでいます。 各種クラスの関係とか内部の実装とかはほとんどドキュメント化されてないので、MSDNだけですべてを探るのはけっこう厳しいものがあります。 そういう意味ではReference Sourceはとても参考になりますが、やっぱり読みづらいです。。。

そもそも論:WPFレンダリングプロセス

昔々、WinFormsのコントロールは、最悪自前で全部描いてしまえばなんとかなる、みたいなことがありました。

しかしWPFXAMLに予め定義された要素を置いて、あとはスタイルなりビヘイビアなりで制御して、という形がほぼすべてです。 WinFormsではよく見た、完全にカスタムしたコントロールをコードで作るという例はWPFではあまり聞いたことがないです。

というのも、WPFXAMLでちょいちょいっと書くだけで、かなり複雑なカスタマイズにも対応可能だからです。 XAMLで見た目を定義するのはとても楽ちんですし、今更レイアウトを計算するコードを書くなんてダサいし非効率的だしバグの温床になるというわけで、 自分で何から何まで面倒を見るコントロールを作るという文化はWPFではほとんど廃れてしまったように感じています。

私はこれまで、WinFormsのコントロールとくらべてWPFのコントロールはカスタムできる範囲に限度があり、そしてそれは最悪自分で全部レンダリングできる WinFormsと、レンダリングの要素が予め用意されているものに限られるWPFの構造に起因している と思い込んでいました。そして、それは事実ではありませんでした。

つまり、やりようによってはWinFormsの流儀である DO IT YOURSELFWPFにおいても通じるようです!それができるなら、もう既存のコントロールの 不快な動作にやきもきする必要もなく、自分で全責任を持って楽しい毎日を過ごすことができそうです。

そんな楽しい毎日を過ごすために、ここでWPFがどうやってUIを組み上げているかの説明をしておきたいと思いますおきたいと思いましたが、中途半端に終わってしまいました。

反省の弁

そして、何より今まで私は、WPFがどうやって要素をレイアウトしているか、どうやって要素を描いているか、そんなことに気を配らずにアプリケーションを書いてきました。これは大変に恥ずべきことです。。。

どうやって動いているかが分かれば、あとはそれをなぞるなり逆手に取るなり無視するなりの対策が打てるようになるので、もっとマシなアプリケーションが書けそうな気がします。 反省も含めて、WPFのレイアウトシステムがどうなっているかを簡単になぞりたいと思います。

レイアウトとレンダリング

そもそも、WPFレンダリングは2つのツリーによって行われています。

ひとつは、「LogicalTree」。XAMLで記載する内容に近いデータが保持されています。たとえば、Windowの下にGridがあり、その中にListBoxがあり、その中にはさらにListBoxItemがぶら下がっている、そんな構造が そのまま現れます。

そしてもう一つが「VisualTree」。こちらは、画面に描画するコンポーネントが登録されます。ListBoxの中にはScrollViewerがあったり、さらにその中にVirtualizingStackPanelがあったり、…。

おおよそ見当が付く通り、WPFはこの「VisualTree」を使って描画します。そして、VisualTreeに参加する資格があるのは「System.Windows.Media.Visual」以下のクラスです。

そしてMSDNにはVisualについて「WPF でヒット テスト、座標変換、境界ボックス計算などの描画をサポートします。」との記載があります。 つまるところ、WPFで何かレンダリングするためには、以下の条件が達成できれば良いわけです。

  • ヒットテスト - 指定した座標がVisual内にあるか判定
  • 座標変換 - 指定したVisualに含まれる子Visualの相対座標の制御
  • 境界ボックス - bounding box、指定したVisualが画面中で占める領域を計算

ところで、Visualではヒットテストや座標変換を制御できるvirtualメソッドはありますが、Bounding Boxの言葉は一言も出てきません。

それもそのはず、Bounding Box周りのvirtualメソッドはすべてinternalになっています: Visual.cs - Microsoft Reference Source

ではどうするかというと、その一層下の UIElement を見なければなりません。

UIElementは「MeasureCore」「ArrangeCore」などのレイアウトに関係するほかにも、膨大なvirtual methodを提供してくれます。詳しくはMSDNをご覧ください。

そう、実際には、ユーザが何か独自のコントロールを生み出したい場合、UIElementが提供する機能以上のことは行えません。 しかし、UIElementは十分すぎる機能を提供してくれます。自分で実装する気力さえあれば、きっとあなたのソリューションを見つけることができます。

さて

だいぶお酒が回ってきたので、今日はここまでにしたいと思います。

この記事について

基本要素の概要 - MSDNを見るとよくわかるかもしれません。