2024年5月11日土曜日

WPF用縦書きテキストブロック Tategaki ver.3.2.2

Tategakiをアップデートしました。

Github:

Nuget:
https://www.nuget.org/packages/Tategaki/

今回の変更点

今回もマイナーチェンジです。

  • フォント読み込み時のパフォーマンス改善
  • 必要最低限のフォントファイルのみを読み込むように修正

説明していきます。

フォント読み込み時のパフォーマンス改善

前回のソフト更新で、縦書きフォントの検索時のパフォーマンス改善というものがありました。 GSUBテーブルのみを読み込むようにすることでパフォーマンスを飛躍的に向上させたというものでした。しかし、この検索のためにファイルの読み込みルーチンに2種類の実装ができてしまいました。イケてないです。プログラマーというのはコピペコードを嫌がるものです。ですので、実装を統一する作業を実施しました。

これにより、検索時以外にフォントファイルの中身を読み込む際のパフォーマンスも向上しました。1~2割程度の改善だと思います。

古い実装ではいったんすべてFile.ReadAllBytesメソッドでメモリに読み込んだうえで解析していましたが、フォントファイル内の多くのテーブルを読み込む場合でもStreamでファイルの必要な部分のみを都度読み込むほうが速いみたいです。まあ、ファイルのランダムアクセスみたいになってしまいますので、もしかしたらHDDとかなら重くなるのかもしれませんが、最近システムドライブがHDDのパソコンもそうそう無いでしょうからこれで良いでしょう。

必要最低限のフォントファイルのみを読み込むように修正

今回実施したかった改善はまさにこれです。

前回のソフト更新で「特定のフォントファイルを読み込むと落ちる不具合の修正」というものがありました。行儀の悪いフォントファイルがあると落ちるというものです。この時に思ったのですが、潜在的なリスクとして、ほかにも想定外のフォントファイルにより例外が発生するということは充分にあり得ます。私のパソコンでは落ちないけど誰かほかの人のパソコンに入っている特定のフォントなら落ちる…そんなのは検証しきれませんね。

前バージョンまででは、アプリの立ち上げ時に(=staticクラスのコンストラクタで)全フォントを読み込むようにしていました。そのため、行儀が悪いフォントファイルを持っている人の環境では、このライブラリを参照する限りアプリが一切立ち上げられなくなってしまいます。たとえそのフォントを使わなかったとしてもです。それはあまりにもよろしくない挙動で、初めてこのライブラリを使ってくださった方の印象を悪くさせかねません。

そこで、全フォント読み込みをできる限り行わないように修正しました。

具体的には、TategakiText.FontFamilyプロパティに指定されたフォントが縦書きを持っているフォントの場合、全探索は行いません。そのフォントのみを読み込みます。全フォントを読み込むのは以下の2パターンのみです。

  • TategakiText.FontFamilyプロパティに設定されたフォントが存在しない/縦書きに対応しないフォントだった場合
  • TategakiText.AvailableFontsプロパティを読みに行ったとき

後者は単純です。すべての縦書き対応フォントを参照してきたのですから全フォントを読み込むことになります。

前者については、フォールバック処理が入っているためです。使えないフォントだった場合、類似の使えるフォントを探すことになるのですが、そのために全フォントを読み込んだうえで類似フォントを探すような処理にしております。

いずれにせよ、「とりあえずTategakiTextを使ってみよう」レベルでは上記2パターンに抵触することは無いので、仮に行儀の悪いフォントファイルがあったとしてもアプリが立ち上がらない状況までは回避できるようになります。

余談:Tategakiのダウンロード数

NugetでTategakiのダウンロード数が見られるのですが、約1週間ごとにリリースしてきたver.3.0.0以降の各バージョンがだいたい75~85ダウンロードされているみたいです。

まあ、有名ライブラリなどに比べれば全然なのですが、80も使ってくれている人がいるんだ、というのが正直な感想です(ありがとうございます)。使ってくださった方からのフィードバックみたいなのはなかなか無いので、こちらとしてはあまり実感が無いというのが正直なところなのですがね。

引き続きよろしくお願いいたします。何かご意見・ご要望等あればいつでもコメントください。

2024年5月3日金曜日

WPF用縦書きテキストブロック Tategaki ver.3.2.1

前回の記事で「今度こそこんなものですかね」と言ったにもかかわらずまたアップデートしました。まあ前回よりインターバル長いしセーフ。

Github:

Nuget:
https://www.nuget.org/packages/Tategaki/

今回の変更点

今回は下一桁のバージョンアップで、マイナーチェンジです。新たな機能の追加は無しです。Readmeにも書いていますが以下の点をアップデートしました。

  • 縦書きフォントの検索パフォーマンスを改善
  • 下線/中線/上線の配置を改善
  • 特定のフォントファイルを読み込むと落ちる不具合を修正

一つひとつ説明していきます。

縦書きフォントの検索パフォーマンスを改善

TategakiText.AvailableFontsプロパティを呼び出すとき、全フォントを読み込んで、縦書きに対応しているか、すなわちGSUBテーブルのvertタグを持つデータがあるか確認します。しかし、ここに意外と処理に時間がかかります。特に久々にノートパソコンを出してきてこのプロパティにアクセスしてみたところ、制御が返ってくるまでに大体4秒くらいかかることがわかりました。これじゃあさすがに遅すぎて困りますね。

いろいろと原因を調査してみたところ、改善の余地として以下の3点が見つかりました。

  • GSUBテーブル以外のテーブルのテーブルも解析している
  • フォントファイルのバイナリを最初にいったんすべてメモリに読み込んでいる
  • 並列処理をしていない

一つ目は、GSUBテーブル以外のテーブルも解析しているという点です。実際に縦書き描画をする上ではそれ以外の情報も多数必要になるので最終的には読み込むことになるのですが、縦書きに対応しているフォントかどうかを調べるだけなら全体を解析する必要はありません。というわけで、縦書きに対応しているかどうかを調べるだけの専用パーサーを別途実装しました。

二つ目は、フォントファイルはいったんすべてFile.ReadAllBytesメソッドでメモリに読み込んだうえで処理をかけている点です。この読み込むだけの操作が意外と重いようで、処理時間の半分くらいを占めていたようです。読み込んだデータの大部分を解析するならメモリに置いても損は無いとは思いますが、GSUBテーブルがvertタグを持っているかどうかを調べるだけなら全体をわざわざ読み込む必要はありません。というわけで、一つ目の点と併せて専用パーサーでは必要な部分だけをFileStreamでSeekしながら読み込むようにしました。

三つ目は単純です。それぞれのフォントファイルの読み込みを並列に処理するようにしました。環境にもよりますが、これで2割くらい処理速度の向上がされるようです。

これら3つの改善をすることで、読み込みが0.8秒くらいまで縮まりました。5倍くらいの改善ですね。実用的なレベルまで圧縮することができました。

実は、まだ現状はGSUBテーブルすべてを解析しているのですが、ここをFeatureTagの解析だけにするともう少し改善が見込めるかもしれません。ただ、めんどくさいので似たような実装を増やすのもあまりよくないので、それはまた必要になったときにでも。

下線/中線/上線の配置を改善

実は今までのTategakiは下線/中線/上線の場所があまりイケていませんでした。


こちらが今回の更新前(ver.3.2.0)で下線を引いたときの画像です。下線は引けているのですが、左隣の行のほうが近いですね。イケてないです。

この下線の位置はGlyphTypeface.UnderlinePositionプロパティからもらっていたのですが、まあ、標準ライブラリにあることからもわかる通りこれは横書き用の位置なんですね。それをそのまま縦書きで使っちゃそりゃイケてませんわ。

ということで、ちゃんとフォントデータのBASEテーブルを読み込むようにしました。ここでは縦書き用のベースラインなどの寸法情報が格納されていて、それをもとに下線/中線/上線などを描画することで、イケている位置に線を引けるようになります。

良い感じですね。離れすぎず、重なるわけでもなく、良い位置に線を引けるようになりました。

ちなみに、TategakiTextでは「Underline(下線)」と言うと文字列の左側、「Overline(上線)」と言うと右側に描画するのですが、これは縦書きは横書きを90度時計回りに回して描画しているという考えに基づくものです。ですが、Microsoft Wordでは縦書きで下線を設定すると右側に線が引かれるのですね。あまり深く意識したことが無かったですが、昔、国語のテストとかで線が引かれていたのも右側でしたっけ。まあ、TategakiTextで右側に線が欲しい人はTextDecorationsでOverlineを指定してください。

Wordで下線を指定したもの

特定のフォントファイルを読み込むと落ちる不具合を修正

行儀の悪いフォントファイルがあったときに落ちるバグみたいなのは…なかなか対処が難しいですね。これも私のノートパソコンでTategakiTextを使用したときに発覚した不具合でした。

OpenTypeフォントは、Headテーブルにて作成日時/更新日時情報を持っています(ファイルシステムのタイムスタンプとは別です)。これは1904年1月1日午前0時0分からの経過秒数を64bit型で記録されているのですが、この数値が大きすぎたとき、.NETのTimeSpan構造体に入りきらず例外を吐くようです。まあ実際にそんな日時は遠い未来ですのであり得ないのですが、お行儀が悪いフォントファイルだとこういう例外の原因になってしまうようでした。

この不具合は、境界チェックを入れるを入れることで回避するよう修正しました。

余談:WPFのContent

余談ですが、WPFにはContentという仕組みがあり、コントロールの内部に別のコントロールを入れ込むことができます。もちろんTategakiTextもWPFのコントロールですので、別のコントロールに内包することができます。

<Button>
    <tg:TategakiText Text="縦書きのボタン" />
</Button>

素晴らしいですね。WPFに縦書きコントロールがあるだけで、いとも簡単にこういったものも作れてしまう表現力がWPFのすごいところです。

今回のアップデートは、WPFでこういうことができることに気が付いて実際にコーディングしてみたところ、左右の余白の大きさが違って非常に不格好になってしまっているのに気が付いたところから始まりました。

その原因には実は心当たりがあって、Underlineを描画する際に、その座標を前述のとおりGlyphTypeface.UnderlinePositionから貰ったところ、値がマイナスにまで触りきってしまっていたので描画領域をはみ出て見切れるという事象があったのです。ですので、Underlineをの分を見越して左側に大きめの余白を用意していました。ですが、それでバランスが崩れるのはやはりイケていないです。そもそも上で述べた通り、その座標が左に寄りすぎていました。なぜだ?と調べていったところ、たどり着いたのが、その下線の座標は横書きを前提にしたものということでした。

コンピューターの文字の描画の原点は英語、すなわちアルファベットですからね。ご存じのとおり、アルファベットをきれいに書く練習を人間がするときは、4本の横線の中に字を書く練習をします。そして、下から2番目の線が基準線となっているわけです。ですので、文字と被ることがない下線を引こうと思ったら、マイナスの座標にまで入ることになるのです。

日本語ではそうではありません。日本語は、手書きの練習をするときは田の字のマスに字を書く練習をします。Underlineと言えば迷わず字の外殻に沿うように線を引けばいいだけなんですね。

そんな字体の違いから始まる文字描画の違い、それがこのTategakiを開発するうえでプログラミングの枠を超えて面白いところですね。

2024年4月21日日曜日

WPF用縦書きテキストブロック Tategaki ver.3.2.0

前回の記事で「まあこれくらいで良いかな」と言ったにもかかわらずまたアップデートしました。

Github:

Nuget:
https://www.nuget.org/packages/Tategaki/

今回の変更点

今回は目新しい新機能の追加等々はそんなに無いのですが、フォント周りの処理を一新しました。

今まではTypeLoaderというライブラリを使用して読み込んでいたのですが、これを自前のコードで実装しました。TypeLoaderで実装されていない情報を使用したかったのですが、やはり他人の書いたコードに手を入れるのは好きになれず…と言うよりあまり中身を理解しないまま触るのに抵抗感があり、自分での実装に踏み切りました。

それに伴って、以下の機能が実装されています。

  1. プロポーショナルフォントに対応
  2. 使用できるフォントが増えた
  3. 代替描画機能を実装

 順に説明していきます。

プロポーショナルフォントへの対応

世の中には二種類のフォントがある。等幅フォントとプロポーショナルフォントだ。

とまあ大げさに言うほどではないのですが、皆さんよくご存じと思います。

等幅フォントはプログラミングの際のテキストエディターなどでもよく使われていて、すべてのフォントの幅が同じものです。それに対してプロポーショナルフォントは、字によって幅が異なるフォントです。状況に応じて使い分けられるものですが、まあ、実装する側としては等幅フォントのほうが扱いやすいのは言うまでもありません。

プロポーショナルフォントには何種類か実装方法があるようですが、最も近代的な方法は、フォントファイルに含まれているVertical Proportional Alternateと呼ばれる情報をもとに文字の位置や幅を調整するものです。Windowsパソコンだと游ゴシックや游明朝などが対応しているようです。

このオプションを実装しました。

↓等幅フォント

↓プロポーショナルフォント

オプションで有効/無効を切り替えられるので、好みに合わせて使えば良いでしょう。

ちなみに、Vertical Proportional Alternateが含まれないフォントは、このオプションを有効にしてもプロポーショナルフォントにならないだけです。対応しているフォントはそんなに多くはなさそうでした。

使用できるフォントの増加

縦書きを実現するには、かっこや句読点など横書きと縦書きで異なる字体を取るものを置き換えて表示しなければなりません。

そもそも文字コードから字体を得るには、グリフインデックスと呼ばれるIDに変換したうえで、そのグリフインデックスをもとにフォントファイル内の描画情報を取得せねばなりません。縦書きの字体を得るためには、グリフインデックスを縦書きのグリフインデックスに読み替えたうえで縦書きの字体を得る必要があります。

この縦書きに変換するテーブルは、フォントファイル内のGSUBテーブル(Glyph Substitution Table)と呼ばれるものの中に含まれていて、縦書き以外にも様々な変換がこのテーブルに含まれています。例えばアラビア語は複数の文字がつながって一体になって描画されるため、複数のグリフインデックスを別の一つのグリフインデックスに変換するテーブルなどもあるそうです。そのため、テーブルの実装が8種類くらいあって、すべての機能を使用するならばすべての実装をせねばなりません。

ただ、縦書き変換に使用するのはGSUBの中でも特にSingle Substitutionと呼ばれる種類のテーブルだけですので、もともと使っていたTypeLoaderはこれを含む限定的な種類のテーブルにのみ対応していました。

しかし、実際にはExtension Substitutionと呼ばれるテーブルにも縦書きが格納されることがあるようです。と言うよりも入れ子になっていて、Extension Substitutionの中にSingle Substitutionテーブルが入っているという構造になっています。TypeLoaderはこのExtension Substitutionに対応しておらず、例えばこれを使用するYu Gothic UIなどでは縦書きを表示することができませんでした。

TategakiではこのExtension Substitutionにも対応させましたので、Yu Gothic UIを含むあらゆる縦書き対応フォントで描画することができるようになりました。

代替描画機能の実装

さて、気付いている人もいたかもしれませんが、実は、MS P明朝やMS PゴシックでTategakiTextを使用すると、若干表示が乱れます。

よく見ると、例えば1行目の「太刀の鞘(さや)」の部分を見るだけでも「あっ…」となりますね。

「の」と「鞘」も少しかぶっていますし、「(」は完全に「さ」とかぶっています。

これの原因は正直よくわからないのですが、DrawGlyphRunメソッドを使わずに、グリフをジオメトリに変換してDrawGeometryメソッドで描画することで回避することができるようです。

この機能をなんと名付けようか少し悩んだのですが、結局は「代替描画機能」としました。プロパティ名としてはEnableAlternateRenderingで、これを有効にするとGlyphRunを使わずにジオメトリで描画します。

見ての通り、見違えるほどきれいに描画できています。

ただ、描画処理は少し重くて、このサンプルアプリでウィンドウをリサイズしたりフォントサイズなどのスライダーを動かすとカクツキを感じます。あくまでもMS P明朝やMS Pゴシックをきれいに描画するための限定的なものと考えておきたいです。

ちなみにですが、代替描画ではなくても、フォントサイズを大きくすれば MS P明朝やMS Pゴシックでもきれいに描画されるようです。その境目で、フォントがビットマップからベクターに変化したように見えるので、もしかしたらそのあたりの不具合なのかもしれませんね。

フォントファイルの構造のお勉強

さて、最初にも述べた通り、今回のバージョンからフォントファイルを読み込むのに自前のコードを使用しています。

現代のWindowsパソコンなどで使われるフォントはOpenTypeと呼ばれるフォーマットになっていて、 これを読み取る必要があります。このフォーマットを勉強する必要があるのですが、結局は以下の2つのサイトが中心となりました。

前者はOpenTypeフォーマットの開発者の一人であるMicrosoftの公式ドキュメントで、網羅的に仕様が書いてあります。後者は、その中から特に日本語フォントで必要な内容をピックアップして日本語で解説されているサイトです。どちらも有用で大変お世話になりました。

もう一つ、GlyphLoaderです。

おそらくTypeLoaderと同じ作者が作ったライブラリで、TypeLoaderの後継と思われます。このソースコードが大変参考になりました。 

ところで、このようなバイナリーデータを読み込むにはうってつけの機能がSpan<T>で、ファイル内のデータをいったんすべてメモリに読み込みさえすれば、その先は部分部分を切り出して、わかりやすく、かつ高速にデータを切り出せます。さらに、BinaryPrimitivesというクラスがあり、ReadOnlySpan<byte>から任意のサイズ/エンディアンのデータを取り出すことができるので、ushortやintなどへの変換も簡単です。あとはIndex / Rangeさえ使えれば言うことは無かったのですが…この機能は.NET Frameworkでは使えないようですね…。Tategakiを.NET Frameworkで使ってくださっている方もいるようなので、ひたすらSliceしまくりました。



今度こそこんなものですかね。だいたいやりたいことはやり切った気がします。

ここのところ本業から帰宅した後、夜の時間をひたすらこのソフトの開発やフォントファイルのお勉強に使っていたので、寝不足気味なうえ疲れもあまり抜けていませんでした。

もう週末もほぼほぼ終わりになってしまいましたが、アニメでも消化しながらゆっくりするとしますか。

2024年4月13日土曜日

WPF用縦書きテキストブロック Tategaki ver.3.1.0

 最近Tategaki熱が再燃しています。大幅に機能を追加しました。

Github:

Nuget:
https://www.nuget.org/packages/Tategaki/

追加した機能

TategakiTextのプロパティ

追加した機能はReadmeのほうにも書いてありますが、TextBlockにある主要なプロパティを TategakiTextにも追加し実装しました。追加したプロパティは以下の通りです。

  1. TextWrapping
  2. TextDecorations
  3. LineHeight
  4. TextAlignment
  5. Padding
  6. LastForbiddenChars / HeadForbiddenChars / LastHangingChars
  7. EnableHalfWidthCharVertical

1番から5番まではTextBlockにもあるプロパティで、その挙動も極力TextBlockに似せているので特段使い方の説明はいらないと思います。

TextWrappingが実装されたことにより、今まで1行表示しかできなかったTategakiTextが複数行表示もできるようになりました。改行文字も認識しますので、下のデモソフトのように小説のような長い文章でもちゃんと折り返して正確に表示できます。TategakiMultilineは多数のTategakiTextをItemsPanelで並べて折り返しを実現していたので、それに比べればだいぶ動作が軽快になりました。

禁則文字 / ぶらさげ文字

TategakiMultilineには禁則文字(文末禁止文字 / 文頭禁止文字)が設定できました。TategakiTextでも同様の設定ができるようになったうえ、文末ぶらさげ文字も設定できるようになっています。文末に来た場合、はみ出し前提で下にぶらさげる文字ですね。

また、TextWrapping列挙型にはWrapとWrapOverflowという2種類のオプションがあります。

TextWrapping.Wrap


TextWrapping.WrapOverflow

長い一単語があって幅が入りきらなくなったとき、単語の途中で折り返すのがWrap、単語を右側にはみ出させるのがWrapOverflowです。もちろんTategakiTextもこの機能に対応しています。禁則文字の処理の一環として実装されています。

EnableHalfWidthCharVertical

半角の文字を縦書きにするかどうかのオプションです。

上に2つのテキストがありますが、左がこのオプションがOFF、右がONです。ちなみにフォントにvrt2が含まれている場合はこのオプションにかかわらず左側のスタイルになります。

フォント読み込み処理回り

従来はEnvironment.GetFolderPathメソッドを使ってシステムのフォントファイルを読み込んで縦書きが有効なフォントを抽出していました。

string FontDir = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);

var uris = Directory.GetFiles(FontDir, "*.ttf").Concat(Directory.GetFiles(FontDir, "*.otf")).Select(p => new Uri(p))
    .Concat(Directory.GetFiles(FontDir, "*.ttc").SelectMany(p => {
        using(var fs = new FileStream(p, FileMode.Open, FileAccess.Read)) {
            return Enumerable.Range(0, TypefaceInfo.GetCollectionCount(fs)).Select(i => new UriBuilder("file", "", -1, p, "#" + i).Uri);
        }
    })
);

これはグリフレベルで描画をする際にどうしてもフォントのURIが必要だからです。ですが、いわゆる「游ゴシック」などのフォントファミリー名からURIを取得する手法がわからず、逆にURIからフォントファミリー名を取得してテーブルとして保持していました。

しかし、フォントはWindowsの特定のユーザーのみにインストールすることもでき、その場合はシステムフォルダ(C:\Windows\Fonts;GetFolderPathで取得できるフォルダ)にフォントファイルは入りません。ですので今までそのようなフォントは読み込めませんでした。

ユーザー用フォントのフォルダを足すのは簡単ですが、Windowsの仕様変更があればまたソフト側でも対応する必要が出てきます。ですので、やはり何かOSやフレームワークが提供する何らかの方法でフォントを取得したいですよね。

そしていろいろと調べていった結果、最終的に以下のような処理にたどり着きました。

var fonttable = new Dictionary<string, VerticalFontInfo>();
var namelist = new List<string>();

foreach(var ff in Fonts.SystemFontFamilies) {
    var tf = new Typeface(ff.Source);
    if(!tf.TryGetGlyphTypeface(out var gtf))    // GlyphTypefaceが取得できなければ用無し
        continue;

    int num = gtf.FontUri.Fragment == "" ? 0 : int.Parse(gtf.FontUri.Fragment.Replace("#", ""));
    var tfi = new TypefaceInfo(gtf.GetFontStream(), num);

    VerticalConverterType convtype = VerticalConverterType.None;
    if(tfi.GetVerticalGlyphConverter().Count > 0)
        convtype |= VerticalConverterType.Normal;
    if(tfi.GetAdvancedVerticalGlyphConverter().Count > 0)
        convtype |= VerticalConverterType.Advanced;
    if(convtype == VerticalConverterType.None)    // 縦書きコンバーターが取得できなければ用無し
        continue;

    var vfi = new VerticalFontInfo(gtf, ff.Source, convtype);
    namelist.Add(vfi.OutstandingFamilyName);
    foreach(var name in vfi.FamilierFamilyNames.Select(p => p.familyname).Distinct())
        fonttable[name] = vfi;
}

namelist.Sort();

まず、Fonts.SystemFontFamiliesでシステムに存在するフォントファミリー名を取得します。その後、そのフォントファミリー名をもとにTypefaceクラスをインスタンス化し、そこからGlyphTypefaceを取得することで、そのメンバからURIを取得することができるのです。

ただ、Fonts.SystemFontFamiliesで取得したフォントファミリー名とGlyphTypeface内にあるフォントファミリー名が違うことがあるようです。この辺のWindowsの挙動がほんとよくわからないですが、Tategakiではどのファミリー名でも目的のURIにたどり着けるように一生懸命キャッシュしています。

 

さて、ここまで作ったら、まあだいたいの用途は満たせそうなのでこんなもんで良いかな…。 TextTrimmingの実装とかをやっても良いかもしれませんが、本家TextBlockって実はHTMLみたいな結構強力な表現ができるみたいで、いろいろ実装していたらきりが無いですからね。まあ、複数フォントを混ぜたかったら複数TategakiTextを並べれば良いだけですし。

2024年4月6日土曜日

WPFでFrameworkElementを直接継承したコントロールを作成する

WPFでコントロールを自作することはさほど多くありません。標準コントロールのほかExtended WPF Toolkitなどのライブラリが充実していることに加え、テンプレートやスタイル、その他もろもろの強力な機能により自作コントロールが無くてもかなりの表現ができてしまうからです。
それでももし何かコントロールが必要になったらカスタムコントロールでいくつかのコントロールを集めたコントロールを作ることができます。これでだいたい事足りてしまうのです。

それでも、もっと原始的なコントロールを作りたいことがあったらどうすれば良いでしょう。そんなことは普通は無いと思っていたのですが、WPF縦書きライブラリを作る過程で必要になってしまったので、そのようなときの手段を今回はまとめておきたいと思います。

 

WPFのレイアウトプロセス

まず最初に理解しなければならないのは、WPFにおけるコントロールの配置の仕組みです。多くの人は、Grid / StackPanel / Canvasの3種類のパネルで、コントロールに与えるパラメーターは同じなのにコントロールのサイズなどの配置のされ方が全く違うことを経験したことがあるでしょう。

Grid

<Grid ShowGridLines="True">
    <Grid.RowDefinitions>
        <RowDefinition Height="1*" />
        <RowDefinition Height="1*" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*" />
        <ColumnDefinition Width="1*" />
    </Grid.ColumnDefinitions>

    <TextBlock Grid.Row="0" Grid.Column="0" Text="Red" Foreground="White" Background="Red" />
    <TextBlock Grid.Row="0" Grid.Column="1" Text="Yellow" Foreground="Black" Background="Yellow" />
    <TextBlock Grid.Row="1" Grid.Column="0" Text="Red" Foreground="White" Background="Blue" />
    <TextBlock Grid.Row="1" Grid.Column="1" Text="Green" Foreground="White" Background="Green" />
</Grid>

StackPanel

<StackPanel Orientation="Vertical">
    <TextBlock Text="Red" Foreground="White" Background="Red" />
    <TextBlock Text="Yellow" Foreground="Black" Background="Yellow" />
    <TextBlock Text="Red" Foreground="White" Background="Blue" />
    <TextBlock Text="Green" Foreground="White" Background="Green" />
</StackPanel>

Canvas

<Canvas>
    <TextBlock Canvas.Left="50" Canvas.Top="50" Text="Red" Foreground="White" Background="Red" />
    <TextBlock Canvas.Left="100" Canvas.Top="50" Text="Yellow" Foreground="Black" Background="Yellow" />
    <TextBlock Canvas.Left="50" Canvas.Top="100" Text="Red" Foreground="White" Background="Blue" />
    <TextBlock Canvas.Left="100" Canvas.Top="100" Text="Green" Foreground="White" Background="Green" />
</Canvas>

それぞれのパネルに4つのTextBlockを配置してみました。一部パネル内のどこに配置するかの添付プロパティは追加していますが、それ以外はどのパネルに対しても同じパラメーターでTextBlockを配置しています。ですが、Gridの場合は右/下方向に引き伸ばされ、StackPanelは右方向のみに引き伸ばされ、Canvasは一切引き伸ばされていません。TextBlockからはパネルのどの位置に配置するのかしか指定しておらず、寸法はパネルの種類によって自動で決まるのです。それ以外にもHorizontalAlignmentプロパティVerticalAlignmentプロパティも配置に影響するのはご存じのとおりです。
これは一体どうやって実現されているのでしょうか。

MeasureOverrideとArrangeOverride

WPFのレイアウトは、Measure(測量)とArrange(配置)という2つのプロセスを経て決定されます。

1. Meausre

親要素が子要素の配置を考えるうえで、子要素に必要な大きさを申告してもらうための手続きです。 

親要素はMeasureが必要になった時に子要素のMeasureメソッドを呼びます。そうすると子要素はDesiredSizeプロパティを更新します。FrameworkElementを継承したクラスを実装するうえでは、MeasureOverrideメソッドをオーバーライドすることでその手続きを実装します。

protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize);

availableSizeは親要素が提供可能なサイズですので、これをもとに必要サイズを計算し、その値を返します。

2. Arrange

親要素はDesiredSizeをもとに各子要素の配置を決定し、子要素に通知する手続きです。

親要素は子要素の配置が決まると、子要素のArrangeメソッドを呼びます。そうすると子要素はRenderSizeプロパティを更新します。FrameworkElementを継承したクラスを実装するうえでは、ArrangeOverrideメソッドをオーバーライドすることでその手続きを実装します。

protected virtual System.Windows.Size ArrangeOverride(System.Windows.Size finalSize);

finalSizeは実際に自分に割り当てられた大きさですので、これをもとに自身の配置を制御します。返却値は実際のサイズになりますが、まあ、普通は割り当てられたサイズそのままになると思いますので、そのような場合はこのメソッドを敢えてオーバーライドする必要はありません。FrameworkElementがこの返却値をもとにRenderSizeプロパティを更新してくれます。

子要素の申告でレイアウト変更を行う

ここまで説明してきたのは、すべて親要素起点のレイアウト変更です。例えばウィンドウサイズの変更による配置変更などですね。ただ、コントロールを実装するうえでは、自身のサイズが変わるなどして親要素にレイアウト変更を依頼しなければならないことが出てきます。例えば表示する文字列が変わった、文字サイズが変わったなどですね。そのような場合はどうすれば良いのでしょうか。

一つの方法としては、InvalidateMeasureメソッドを呼ぶことです。これにより現在のMeasureの結果を無効なものとし、再度レイアウトプロセスを実行することを促せます。
ただしMSDNの説明文にも書いてある通り、このメソッドの頻繁な呼び出しはパフォーマンスに大きな影響を与えるため、可能な限り呼び出しは避けるべきと書かれております。もっと良い方法がほかにあるのでしょうか。

前述したとおり、レイアウト変更が必要になるケースとして、文字列の変更や文字サイズの変更などが考えられます。それらは、一般にコントロールの依存関係プロパティ(Dependency Property)として外に公開されていることが多いです。そこに、そのプロパティがMeasureに影響する項目かどうかを設定する機能があるのです。

public bool AffectsMeasure
{
    get { return (bool)GetValue(AffectsMeasureProperty); }
    set { SetValue(AffectsMeasureProperty, value); }
}
public static readonly DependencyProperty AffectsMeasureProperty =
    DependencyProperty.Register(nameof(AffectsMeasure), typeof(bool), typeof(FrameworkElementDerivedClass),
    new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsMeasure));

依存関係プロパティを実装するときはだいたいこんな形になると思います。
このFrameworkPropertyMetadataに渡している引数に注目です。FrameworkPropertyMetadataOptions.AffectsMeasureというのを渡しています。こうすることで、このプロパティが変化したときに自動的にMeasureプロセスが実施されるようになります。
他にもAffectsArrange、AffectsParentMeasure、AffectsParentArrangeがあり、名前の通りです。

 

描画処理

レイアウトに対応できるようになったところで、次は描画処理です。
これもいろいろな方法があるようですが、一番シンプルでかつ上のレイアウト変更と親和性の高い方法を紹介します。

OnRenderメソッド

FrameworkElementにはOnRenderメソッドがあり、これをオーバーライドするのが最もシンプルです。

protected virtual void OnRender(System.Windows.Media.DrawingContext drawingContext);

このメソッドでDrawingContextが渡されます。これがいわゆるWin32で言うところのデバイスコンテキストハンドルみたいなもので、これに対して描画操作をすることで画面に描画することができます。

DrawingContextが持っているメソッドのうちDrawから始まるものを見ればわかりますが、直線、四角形、楕円、ジオメトリ、画像、文字列、グリフなど一通りの描画メソッドを持っています。
また、Pushから始まるものを見ると、クリッピング、Opacity(透明性)、Transform(図形変換)などもサポートしています。Popメソッドもあある通りスタック構造をしており、PushしてからPopされるまでの間それらの処理が適用されるようです。

再描画指示

さて、上述のレイアウトプロセス(Measure→Arrange)の後に自動的にOnRenderが呼ばれるのは言うまでも無いですが、それ以外のときも再描画したいシチュエーションはあります。
これもレイアウトプロセスと同じで、InvalidateVisualメソッドを呼び出して再描画させることができますが、同様にパフォーマンスに影響を与えるので基本は呼ぶべきではないものです。
描画に影響を与えるプロパティにFrameworkPropertyMetadataOptions.AffectsRenderオプションを渡してやれば良いでしょう。


プロパティ値の継承

WPFは依存関係プロパティの値の継承機能があります。一番身近なのはDataContextで、親要素で設定したDataContextが子要素のDataContextにアクセスすることで触れるのはよく知られていると思います。これはDataContextだけでなく、例えばフォントサイズなども親要素で設定したら子要素にも伝搬します。これを「プロパティ値の継承(Property value inheritance)」と言います。

そのような依存関係プロパティを実装する際は、FrameworkPropertyMetadataのコンストラクタにFrameworkPropertyMetadataOptions.Inheritsを渡せば良いです。こうすることで、親要素で設定されたものが子要素に伝搬してきます。

ちなみに、例えばフォントファミリーやフォントサイズなどの文字列関係のプロパティは、TextElementクラスなんかが使えるようです。このクラスに文字列関係の依存関係プロパティのフィールドがありますので、それを使えば簡単にフォント関係のプロパティ値の継承ができます。

public FontFamily? FontFamily
{
    get { return (FontFamily?)GetValue(FontFamilyProperty); }
    set { SetValue(FontFamilyProperty, value); }
}
public static readonly DependencyProperty FontFamilyProperty = TextElement.FontFamilyProperty.AddOwner(typeof(TategakiText));

この依存関係プロパティはちゃんと上述のAffects***などのオプションもちゃんと実装されているようで、これだけでレイアウトやレンダリングも問題なしです。

 

まとめ

さて、多分滅多に使うことのない、WPFのレイアウトシステムと描画処理に関する実装の仕方をまとめました。滅多に使うことが無いからかちゃんとまとまって解説してくれている資料があまり無いんですよね。この点からも.NETのソースコードが公開されるようになったのは、それを見ることである程度どんな感じか追えるのでとても助かります。

最後に、こうやって作られたのがWPF縦書きライブラリ Tategakiです。こっちもよろしくね!

WPF用縦書きテキストブロック Tategaki ver.3.0.1

昨日3.0.0を公開したばかりですが、早速ver.3.0.1を公開です。
ターゲットに.NET Framework 4.7.2を追加しました。処理は変えていませんが、.NET Frameworkだと最新のC#の機能が一部使えなかったりしますので、ソースコードは一部いじっています。

Github:

Nuget:
https://www.nuget.org/packages/Tategaki/

WPF用縦書きテキストブロック Tategaki ver.3.0.0

先日、ふとTwitter経由でTategakiの不具合についての報告がありました。前回更新からすでに8年以上経過しているのに、こうやって連絡をいただけるのはとても嬉しいことです。 

(ライブラリをダウンロードしたいだけの人、使い方を見たいだけの人は下のほうのGithubリンクからリポジトリにアクセスしてください)

不具合の経緯

不具合の内容ですが、 TategakiText ver.2系はコントロールをカスタムコントロールで作っており、コントロール内部にGlyphsコントロールを配置することで縦書きを実現していました。これがDocumentViewerコントロールと相性が悪いようで、DocumentViewer上でTategakiText上にマウスオーバーすると例外を吐いて落ちるようでした。

報告してくださった方は、対策案として、ControlではなくContentControlでGlyphsをホスティングするということまで提案してくださっていました。
ここまでいろいろとやってくれる方がいるのに知らんぷりするわけにはいかない…!というわけで、本腰を入れて不具合の解消を始めました。

不具合内容

FixedPageに配置したコントロールでマウスオーバーが発生すると、そのコントロールから親要素を辿ってFixedPageまでの関係を確認するようですが、そこでGlyphs→Borderときて、その次Contorlに行けず(LogicalTreHelper.GetParent(border)がnullとなり)ArgumentNullExceptionが発生しているようでした。

.NETはオープンソースでコードが公開されているので、当該部位を見てみましょう。

// We have no uniform way to do random access for this element.
// This should never happen for S0 conforming document.

(◞‸◟)(◞‸◟)(◞‸◟)(◞‸◟)(◞‸◟)

"S0 confirming document"(S0準拠ドキュメント?)が何を指すかよくわかりませんが、「この分岐に来ることは無いから実装は適当で良いよね、エイヤッ!」で実装されて、nullだった場合の対処とかは深く考えられていなかったコードということですね。

対応方針とやりたかったこと

さて、報告してくださった方は前述の通りContentControlにしたらこの不具合は発生しなくなったと連絡してくれているのですが、Tategakiライブラリが目指すのは「TextBlockの縦書き版」です。
ContentControlはTemplateが使えるという特徴があり、WPFではTextBlockというよりかLabelがそれに該当します。ですので、せっかく提案いただいていて申し訳ないのですが、少しコンセプトからずれてしまうなと思い、別の方法で対処できないか検討することとしました。

そもそも、元祖TextBlockはカスタムコントロールを使っているわけでもGlyphsをラッピングしているわけでもなく、直接FrameworkElementを継承したクラスとして実装されています。従って、現在のTategakiTextの実装方法自体がそもそもコンセプトずれしているということで、直接FrameworkElementを継承することで実装するという方法で進めていくこととしました。
事の発端となった不具合も、呼び出し経路の中でマウスオーバーされたのがGlyphsコントロールかどうかの分岐が入っています。実装を変えれば不具合も解消するでしょう。

実装

FrameworkElementを直接継承して描画処理を行うのはドキュメントが少なく四苦八苦したのですが、それ自体が別記事となるような分量ですので、後ほど別記事として紹介します。

公開

今まではこのブログにファイルを添付する形でソフトを公開していましたが、意外と私の見えないところで使ってくださる方もいて、でも不具合を踏んだけど報告もできずに困っている方も実はいたのではないかと今回の連絡から反省しました。

私がプログラミングを始めたのは、いわゆるVector / 窓の杜時代で、個人が作ったソフトがクローズドソースでそういったホスティングサービスを経由して公開されている時代でした。でも今は違います。オープンソースでコミュニティがいろいろとフィードバックをくれる時代です。
ならばやはりそういうやり方で公開するほうが良いのではないか、ということで、Githubのリポジトリを立ち上げました。

 

これならば不具合があればIssueを立てれるし、バグを直してくれる方がいればPullRequestをくれるはず…!
実を言うとGithubは使うのがこれがほぼ初めてです。初心者ですがよろしくお願いします!!!!!

もちろん従来通りNugetでも公開しております。

Tategaki - Nuget

前回はターゲットが.NET Framework 4.0でしたが、今回から.NET 6にしています。.NET Framework系も必要な方がいればコメント欄か、コードを修正してPullRequestで連絡ください。検討します。