2015年1月21日水曜日

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

なんてういか、公開時に迂闊ににバージョンを1にしてしまったせいで、今回早速バージョン2になってしまいました。

今回は、Uniscribeのロジック以外ほぼゼロベースで作りなおしています。そのため、一部互換性が無かったり仕様が変更されていたりするところがあるので注意してください。


今までのTategakiTextとTategakiMultilineはUserControlクラスを使って作っておりました。UserControlだとUIデザイナーの支援が受けられるなどのメリットはあるのですが、まあ一番の理由は自分が詳しい知識がなかったというところですかね。

代わりに、Controlクラスを継承した、「カスタムコントロール」としてTategakiTextとTategakiMultilineを実装しました。 そのため、FontFamilyやFontSize等のプロパティを設定しなければそのまま親の値が引き継がれるなど、コントロールとして非常に自然な振る舞いをできるようになりました。

また、グリフインデックスのキャッシング機能を付けて、Uniscribe周りの関数の呼び出しを最小に抑え、高速化しました。これはとりあえず実装したものの、あまり高速化には効果が無かったかもしれませんね…。
他にも、今までは、Glyphsクラスは空文字列を渡すと例外を吐くため、TategakiTextではTextプロパティが空になるとダミーで半 角スペースをGlyphsクラスに渡すようにしていました。そのため、文字が無いはずなのにTategakiTextは半角スペース分の大きさを持ったコ ントロールになってしまうという現象がありました。そこで、Textが空文字列になったら、そのままVisibilityプロパティをVisibility.Collapsedにしてコントロールそのものを消滅させるようにしました。これによって、TextBlockにより近い挙動ができるようになりました。

一方、TategakiMultilineは、今までは1行の表示量を計算して横方向のStackPanelに行ごとのTategakiTextを積み重ねていくような設計にしていましたが、今回は1文字ずつTategakiTextにしてWrapPanelに入れて自動的に折り返させるようにしました。

<ItemsControl x:Name="PART_ItemsControl" RenderTransformOrigin="0.5,0.5" >
    <ItemsControl.Resources>
        <vc:LineMarginConverter x:Key="MarginConverter" />
    </ItemsControl.Resources>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <ItemsControl ItemsSource="{Binding}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <local:TategakiText Text="{Binding}" RenderTransformOrigin="0.5,0.5"
                                            Spacing="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:TategakiMultiline}}, Path=Spacing}"
                                            Margin="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:TategakiMultiline}}, Path=LineMargin, Converter={StaticResource MarginConverter}}"
                                            x:Name="tategaki">
                            <local:TategakiText.RenderTransform>
                                <ScaleTransform ScaleX="-1" />
                            </local:TategakiText.RenderTransform>
                        </local:TategakiText>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel Orientation="Vertical" />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
            </ItemsControl>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.RenderTransform>
        <ScaleTransform ScaleX="-1" />
    </ItemsControl.RenderTransform>
</ItemsControl>                        

このように、ItemsControlの中にItemsControlを入れたような構造になっていて、外側は水平方向StackPanel、内側は縦方向のWrapPanelになっています。段落ごとにWrapPanelを生成し、それをStackPanelに入れてるという形です。文字は基本的に1文字ずつTategakiTextのインスタンスにしますが、禁則処理によって改行が入っちゃいけないところはその前後の文字をつなげて1つのTategakiTextにすることで、WrapPanelに入れるだけで改行しては行けないところの前で改行が入るようにしています。
ちなみに、当初から使っているやり方ですが、StackPanelもWrapPanelも左から右へ積み重ねられる(日本語の縦書きとは逆)ので、表示テ キストを左右反転させてからWrapPanelやStackPanelに入れて、そのパネルを再び左右反転させるという手法で右から左へ積み重ねているよ うに見せています。 

void SetText()
{
    if(itemsctl != null) {
        if(!string.IsNullOrEmpty(Text))
            Uniscribe.GetGlyphs(Text, FontFamily.Source, FontSize);    //1回呼び出しておくとキャッシュされる

        IEnumerable<string> splited = (Text ?? string.Empty).Split('\n').Select(p=>string.IsNullOrEmpty(p) ? " " : p);

        itemsctl.ItemsSource = splited.Select(p => {
            if(p.Length == 0)
                return new string[] { };
            else if(p.Length == 1)
                return new string[] { p.First().ToString() };
            else {
                List<string> ret = new List<string>(p.ToCharArray().Select(p1 => p1.ToString()));

                for(int i = 0; i < ret.Count; i++) {
                    if(i > 0 && HeadForbiddenChars.Contains(ret[i].First())) {
                        ret[i - 1] = ret[i - 1] + ret[i];
                        ret.RemoveAt(i);
                        i -= 2;
                        continue;
                    }
                    if(i < ret.Count - 1 && LastForbiddenChars.Contains(ret[i].Last())) {
                        ret[i] = ret[i] + ret[i + 1];
                        ret.RemoveAt(i + 1);
                        i -= 1;
                        continue;
                    }
                }

                return ret.AsReadOnly().AsEnumerable();
            }
        }).ToArray();
    }
}

さきほどのXAMLに対応するメソッドはこんな感じになっていて、行ごとに区切った文字列を、さらに改行可能な文字単位に分割している様子がわかるかと思います。これで、文字単位でWrapPanelに入れることで実現するTategakiMultilineが作れました。

今回のTategakiMultilineではこのような実装をしたため、リサイズ時の負荷は、リサイズされるごとにTategakiTextのインスタンスを生成していた以前のものに比べて非常に軽くなっていて、特に縦方向の表示領域が狭いときにその速さが顕著に現れます。しかし、Spacingと文字のぶら下げが実装できなくなったのと、1文字ごとにTategakiTextインスタンスを生成しているので(しかもItemsControlをXAML+C#でWPFらしい書き方をしたので、すでに存在するインスタンスの再利用すらできていない)、表示テキスト変更時の負荷が信じられないほど大きくなってしまうという問題もまた新たに生まれました。しかし、例えば将来的に「半角文字だけ90°回転の横書きで表現する」とか、そういった拡張を考えた場合WrapPanelのほうが幾分か実装が楽そうな気もしますよね。
まあ、結局今までの実装と今回の実装のどちらがいいかって考えると、それは一長一短だということになるので、従来の手法のTategakiMultilineも実装しようかと思いました。が、まあ、個人的なモチベーションの都合上、それは延期することにしました。必要になったらやるかもしれません。


機能の対応表はこんな感じですかね。まあTategakiTextだけ使うなら更新しておいて損は無いかなと思います。


Spacingが削除されたので調整用スライダーを無効状態にしていますが、コントロールが作り直しにもかかわらずこのように以前のTategakiText等と同じように使えていることがわかるかと思います。

ダウンロードはこちら
WPF用縦書きテキストブロック Tategaki ver.2.0.0

1 件のコメント:

  1. 記事読ませてもらいました。
    非常に助かりました。

    ちょうど、WPFによる縦書き対応の内容で欲しいものであったので、V2のプロジェクトと元記事(Indicesの件)を参考にしたいと思います。

    ただし企業の案件なので、若干Indices周りの部分は注意してテストをすると思います。
    必要でしたら、テストコードを追加したりすることも出来るかと思いますので、貢献出来ればと存じます。
    よろしくお願いいたします。

    返信削除