2014年12月4日木曜日

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

昨日のリリースに引き続いてさっそく次のバージョンを作ってしまいました。

昨日の記事でまた細かいテストは追々とか言っていましたが、さっそくフラグを回収してしまいました。というのも、Meiryo UIの縦書きフォントが取得できない環境で正常に表示できないバグを持っていたんですね。UIの標準のフォントがMeiryo UIになっている環境がほとんどだと思うのですが、最初起動時にそのフォントがどうしても読み出されて、その時にフォントが見つからなくて例外を吐くようでした。

というわけで、フォントが見つからなかったら例外を吐くのではなく、勝手にMSゴシック→MS明朝の優先順位でフォントを勝手に変更するようにしました。もしもそのどちらも読み込めなかったら、読み込めるフォントのうちの最初のものを読み込むようにしました。

そのほか、例の改行位置の計算にかかわるプログラムの整理をしました。高速化はあまりできていないとは思いますが、少なくとも遅くはなっていないし、コードの綺麗さは一気に上がったと思っています。
並列化をしようかと思ったのですが、GlyphsなどのUIに係るクラスは単一のスレッドでしか動かないのでしようがありませんでした…。

//Redrawすべきかどうかを判定するための以前の状態を保持する変数
string beforeText = null;
double? beforeHeight = null;
double? beforeFontSize = null;
double? beforeSpacing = null;
FontFamily beforeFontFamily = null;
string beforeLastForbiddenChars = null;
string beforeHeadForbiddenChars = null;
string beforeLastHangingChars = null;
double AverageCharHeight = 0;

/// <summary>
/// 必要があったらテキストを再描画するメソッド
/// </summary>
void RedrawText()
{        
    double height = ParentHeight;    //コスト削減

    if((beforeHeight != height) || (Text != beforeText) || (beforeSpacing != Spacing) ||
       (beforeLastForbiddenChars != LastForbiddenChars) || (beforeHeadForbiddenChars != HeadForbiddenChars) || (beforeLastHangingChars != LastHangingChars) ||
       (beforeFontFamily != FontFamily) || (beforeFontSize != FontSize)) {
        if((beforeFontSize != FontSize) || (beforeSpacing != Spacing) || (beforeFontFamily != FontFamily))    //この条件が揃えば文字の高さを再計算
            AverageCharHeight = CalcAverageCharHeight(Text.Split('\n').OrderByDescending(p => p.Length).FirstOrDefault());    //最も長い行を実行する

        try {
            if(!string.IsNullOrEmpty(Text) && (height > 0)) {    //空文字じゃなくてウィンドウの高さがあったら実行する
                string[] DisplayText = Text.Split('\n').SelectMany(p => SplitLine(p, height)).Reverse().ToArray();
                //※SelectManyの前にAsParallel入れたくなるけど、GUI関係はGUIのスレッドじゃないとダメよ

                while(stackPane1.Children.Count < DisplayText.Length)    //StackPanelが少なかったら
                    stackPane1.Children.Add(new TategakiText());        //足りない分を追加する
                
                if(stackPane1.Children.Count > DisplayText.Length)    //StackPanelが多かったら
                    stackPane1.Children.RemoveRange(0, stackPane1.Children.Count - DisplayText.Length);    //多い分を捨てる

                Thickness margin = new Thickness(0, 0, LineMargin, 0);
                for(int i = 0; i < DisplayText.Length; i++) {
                    TategakiText tategaki = (TategakiText)stackPane1.Children[i];

                    tategaki.FontFamily = FontFamily;
                    tategaki.FontSize = FontSize;
                    tategaki.Spacing = Spacing;
                    tategaki.Margin = margin;
                    tategaki.Text = DisplayText[i];
                }
            }
        }
        catch(ArgumentException) {
            stackPane1.Children.Clear();    //フォントがダメなときはクリアする
        }
        finally {
            beforeText = Text;
            beforeHeight = height;
            beforeFontSize = FontSize;
            beforeSpacing = Spacing;
            beforeFontFamily = FontFamily;
        }
    }
}

double CalcAverageCharHeight(string Text)
{
    if(Text != null) {
        try {
            Size size = TategakiText.CalcTextSize(Text, FontFamily.Source, FontSize, FontWeights.Normal, FontStyles.Normal, Spacing);
            return size.Height / Text.Length;    //1文字の平均長(縦書きはだいたい等幅)を計算する
        }
        catch(ArgumentException) {
            return 0;
        }
    } else
        return 0;
}

/// <summary>
/// 行を最大の高さ以内に分割するメソッド
/// </summary>
/// <param name="LineText">行のテキスト</param>
/// <param name="MaxHeight">最大高さ</param>
/// <returns>分割した行</returns>
/// <exception cref="ArgumentException">フォントが読み込めるものではない</exception>
string[] SplitLine(string LineText, double MaxHeight)
{
    List<string> ret = new List<string>();

    if(string.IsNullOrEmpty(LineText))
        ret.Add(string.Empty);
    else {
        while(LineText.Length > 1) {
            int i;
            if(AverageCharHeight <= 0)    //ゼロ除算回避
                i = LineText.Length;
            else {
                for(i = (int)(MaxHeight / AverageCharHeight) + 1; i < LineText.Length; i++) {    //平均長から1行のおよその文字長を割り出す
                    Size size = TategakiText.CalcTextSize(LineText.Substring(0, i), FontFamily.Source, FontSize, FontWeights.Normal, FontStyles.Normal, Spacing);

                    if(size.Height > MaxHeight)    //長さが超えていたらブレーク
                        break;
                }
                i = Math.Max(Math.Min(i - 1, LineText.Length), 1);    //長さが超えたらその1つ小さくなったものから調べればよく、また、最初に決め打った長さがLineText.Lengthを超えてる可能性があるのでそれを合わせ込むが、1より小さくはしない。
            }
            for(; i > 0; i--) {
                Size size = TategakiText.CalcTextSize(LineText.Substring(0, i), FontFamily.Source, FontSize, FontWeights.Normal, FontStyles.Normal, Spacing);
                if(size.Height <= MaxHeight)    //減らしていって長さが切ったらブレーク
                    break;
            }
            i = Math.Max(i, 1);    //0になってたら1にする


            //禁則処理等をする
            while((i < LineText.Length) && LastHangingChars.Contains(LineText[i]))    //次の行の行頭がぶら下げ文字じゃないか?
                i++;    //次の行を取り込む

            while((i > 1) && (i < LineText.Length) && HeadForbiddenChars.Contains(LineText[i]))    //次の行の行頭が禁則文字じゃないか?
                i--;    //行末を差し出す
            
            while((i > 1) && LastForbiddenChars.Contains(LineText[i - 1]))    //文末に禁則文字が含まれていないか?
                i--;    //行末を差し出す


            ret.Add(LineText.Substring(0, i));
            LineText = LineText.Substring(i);
        }
        if(LineText.Length == 1) {    //文字列が1文字だったら強制的に書きだす
            ret.Add(LineText);
            LineText = string.Empty;
        }
    }

    return ret.ToArray();
}

大幅に整理したコードはこんな感じになっています。
テキストをまずは\nで分割し、その分割された行をまた表示領域の高さに合わせて分割します。従来は分割しつつコントロールに転送していましたが、今回は分割するだけでまだstringの配列に入れるだけにしています。これをSelectManyでつなげれば最終的に表示する行が1つのコレクションになりますね。縦書きの行は右から順に並べますがStackPanelは左から並べるので、Reverseでその行の順序を逆にしています。

そして出来上がった行の個数に合わせてコントロールをインスタンス化したり消したりして、最後にまとめて行の内容を登録しています。

結構整理できたと思っています。あの忌々しく何段も続くインデントが無くなったので、見通しが良くなったのは確かです。

ちなみに、各行を分割する処理をするSplitLineメソッドの中で禁則処理もしています。ぶら下げ組み、行頭の禁則処理、行末の禁則処理の順にしています。その処理自体はシンプルですが、何も考えずにその文字が文頭もしくは文末にあるかだけをチェックすると文字数ゼロの行ができて無限ループに陥るのでそこは気を使ってあげました。


というわけで、 羅生門を表示するとこんな感じになっています。


ぶら下げ組み、行頭の禁則処理、行末の禁則処理がしっかりできていることが分かるかと思います。

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

0 件のコメント:

コメントを投稿