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で連絡ください。検討します。