2014年8月2日土曜日

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

さて、前回につづいて縦書き用テキストブロックTategakiの記事です。

[バグフィックス]
  • 比較的短い長さのテキストを表示させようとするとバグる件を修正
  • フォントファミリ名やサイズなどをXAMLで指定しないと表示されないバグを修正
[機能追加]
  • 太字、斜体に対応
  • 複数行(自動折り返し)の縦書きコントロール"TategakiMultiline"を実装
こんな感じですかね。いろいろ問題点があったりバグがまだ眠っていそうだったりしますが、とりあえず公開します。(最初からベータ版とかにしとけばよかったかな…)

まず最初の、比較的短い長さのテキストを入れるとバグる件ですが、これはどうも短いテキストだとグリフインデックスが取得できないようですね…。なぜかよくわかりませんが、Uniscribe側の問題っぽいです。なので、非常にアホらしい回避方法ではありますが、あらかじめ10文字ほど足してからグリフインデックスを取得し、その足した分のグリフインデックスを無視するということをやっています。

ushort[] glyphs = Uniscribe.GetGlyphs("ああああああああああ" + Text, FontFamily.Source, (int)FontSize).Skip(10).ToArray();

そしてフォントの設定をしないと正常に表示されなかった問題は、コントロールのコンストラクタでシステムフォントを適当に代入してあげています。

this.FontSize = SystemFonts.MessageFontSize;
this.FontFamily = SystemFonts.MessageFontFamily;

そして、太字、斜体の対応はこれでおkです。

protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
    switch(e.Property.Name) {
        case "FontWeight":
        case "FontStyle":
            this.glyphs1.StyleSimulations =
                ((FontWeight != FontWeights.Normal) ? StyleSimulations.BoldSimulation : StyleSimulations.None) |
                ((FontStyle != FontStyles.Normal) ? StyleSimulations.ItalicSimulation : StyleSimulations.None);
            break;
    }
    base.OnPropertyChanged(e);
}


さて、今回のメインディッシュの複数行対応縦書きコントロールです。コントロールのXAMLはStackPanelを1つ定義するのみで、コードビハインドのほうで1行の縦書きコントロールを複数生成してそのStackPanelに並べてあげているだけです。ですが、これをコントロールのサイズに合わせて適切に文章を区切り、分散させてやらねばならず、これがとても面倒くさいのです。
Win32APIならGetTextExtentPoint32関数などを使えば文字列の幅が取得できますが、グリフに関してはそのようなものが(多分)無いので、実際にコントロールを作ってその大きさを測定することで文字列の幅を取得しています。1文字ずつ増やしては長さを測って折り返すべき長さに達したらそこで文字列を切り離して~なんてやると文字数分だけ時間が掛かってしまいますが、最初に全体の長さを取得して文字数で割ることで1文字当たりの幅を計算し、おおよそ目標の長さにいきなり合うようにして、後はそこから実際にコントロールの長さを測って調整するという処理をすれば行数に比例する程度の時間で済みます。しかし、それでも結構この処理は重く、このコントロールをリサイズしたりすると結構カクカクします。そういう意味で改善の余地がありそうですが、なんか根本的な方法が無いともうどうしようもないんですよね…。

というわけで結構複雑な処理をしているのでコードがごちゃごちゃしてわかりにくくなっていますが、その部分のコードを貼ります。

void RedrawText()
{
    double height = ParentHeight;

    if((beforeHeight == null) || (beforeHeight.Value != height) || !object.Equals(Text, beforeText)) {    //これをやらないと無限ループに陥る
        stackPane1.Children.Clear();

        if(!string.IsNullOrEmpty(Text)) {
            string[] Lines = Text.Split('\n');
            Size infinitySize = new Size(double.PositiveInfinity, double.PositiveInfinity);
            Thickness margin = new Thickness(0, 0, LineMargin, 0);

            if((height <= 0) || (Lines.Length == 0)) {
                stackPane1.Children.Add(new TategakiText() { Text = Text, FontFamily = FontFamily, FontSize = FontSize, Spacing = Spacing, Margin = margin });
            } else {
                foreach(string line in Lines) {
                    string text = line;

                    if(line.Length == 0) {
                        TategakiText tategaki = new TategakiText() { Text = string.Empty, FontFamily = FontFamily, FontSize = FontSize, Spacing = Spacing, Margin = margin };
                        stackPane1.Children.Insert(0, tategaki);
                    } else {
                        while(text.Length > 1) {
                            TategakiText tategaki = new TategakiText() { Text = text, FontFamily = FontFamily, FontSize = FontSize, Spacing = Spacing, Margin = margin };

                            tategaki.UpdateLayout();
                            tategaki.Measure(infinitySize);
                            double charHeight = tategaki.DesiredSize.Height / text.Length;    //1文字の平均長(縦書きはだいたい等幅)

                            int i;
                            if(charHeight == 0)    //ゼロ除算回避
                                i = text.Length;
                            else {
                                for(i = (int)(height / charHeight) + 1; i < text.Length; i++) {    //平均長から1行のおよその文字長を割り出す
                                    tategaki.Text = text.Substring(0, i);
                                    tategaki.UpdateLayout();
                                    tategaki.Measure(infinitySize);
                                    if(tategaki.DesiredSize.Height > height)    //長さが超えていたらブレーク
                                        break;
                                }
                                i = Math.Max(Math.Min(i - 1, text.Length), 1);    //長さが超えたらその1つ小さくなったものから調べればよく、また、最初に決め打った長さがtext.Lengthを超えてる可能性があるのでそれを合わせ込むが、1より小さくはしない。
                            }
                            for(; i > 0; i--) {
                                tategaki.Text = text.Substring(0, i);
                                tategaki.UpdateLayout();
                                tategaki.Measure(infinitySize);
                                if(tategaki.DesiredSize.Height <= height)    //減らしていって長さが切ったらブレーク
                                    break;
                            }
                            stackPane1.Children.Insert(0, tategaki);

                            text = text.Substring(Math.Max(i, 1));    //iが0になってきたらそれは1文字
                        }
                        if(text.Length == 1) {    //文字列が1文字だったら強制的に書きだす
                            TategakiText tategaki = new TategakiText() { Text = text, FontFamily = FontFamily, FontSize = FontSize, Spacing = Spacing };
                            stackPane1.Children.Insert(0, tategaki);
                            text = string.Empty;
                        }
                    }
                }
            }
        }
        beforeText = Text;
        beforeHeight = height;
    }
}

ちなみに、コントロールのサイズを測るには、まずUpdateLayoutメソッドを呼び、そのあとにMeasureレイアウトを呼ぶことで、DesiredSizeが更新されます。これが、要するにその1行の縦書きを表示するのに要求されるサイズですね。こうやって測っています。なんかコントロールを一旦インスタンス化して測定してるあたり、結構時間の掛かりそうな処理ですよね…。

何かいい方法知ってる人がいたらぜひ教えてください。

ちなみに、この辺苦労しただけあって(?)割と簡単に複数行の書き出しはできます。

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="100" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" Name="TategakiRow" />
    </Grid.RowDefinitions>

    <TextBox Grid.Row="0" Name="textBox1" HorizontalScrollBarVisibility="Auto"  VerticalScrollBarVisibility="Visible" AcceptsReturn="True"
             Text="{Binding ElementName=tategakiMul1, Path=Text, UpdateSourceTrigger=PropertyChanged}" />
    <Slider Grid.Row="1" Name="slider1" Minimum="0" Maximum="30" Value="10"/>
    <StackPanel Grid.Row="2" Orientation="Horizontal" FlowDirection="RightToLeft" Margin="0,0,0,10">
        <tg:TategakiText Text="羅生門" 
                        FontFamily="メイリオ" FontSize="36" Spacing="200" FontWeight="Bold"
                        HorizontalAlignment="Center" VerticalAlignment="Center"
                        RenderTransformOrigin="0.5,0.5">
            <tg:TategakiText.RenderTransform>
                <ScaleTransform ScaleX="-1" />
            </tg:TategakiText.RenderTransform>
        </tg:TategakiText>
        <tg:TategakiText Text="芥川龍之介"
                        FontFamily="メイリオ" FontSize="20" Spacing="100"
                        HorizontalAlignment="Center" VerticalAlignment="Bottom"
                        RenderTransformOrigin="0.5,0.5">
            <tg:TategakiText.RenderTransform>
                <ScaleTransform ScaleX="-1" />
            </tg:TategakiText.RenderTransform>
        </tg:TategakiText>
        <tg:TategakiMultiline Text="すると、老婆は、見開いていた眼を、一層大きくして、じっとその下人の顔を見守った。まぶたの赤くなった、肉食鳥のような、鋭い眼で見たのである。それから、皺で、ほとんど、鼻と一つになった唇を、何か物でも噛んでいるように動かした。細い喉で、尖った喉仏(のどぼとけ)の動いているのが見える。その時、その喉から、鴉(からす)の啼くような声が、喘(あえ)ぎ喘ぎ、下人の耳へ伝わって来た。&#xa;「この髪を抜いてな、この髪を抜いてな、鬘(かずら)にしようと思うたのじゃ。」&#xa; 下人は、老婆の答が存外、平凡なのに失望した。そうして失望すると同時に、また前の憎悪が、冷やかな侮蔑(ぶべつ)と一しょに、心の中へはいって来た。すると、その気色(けしき)が、先方へも通じたのであろう。老婆は、片手に、まだ死骸の頭から奪った長い抜け毛を持ったなり、蟇(ひき)のつぶやくような声で、口ごもりながら、こんな事を云った。" 
                            FontFamily="MS 明朝" FontSize="18" Spacing="100" LineMargin="{Binding ElementName=slider1, Path=Value}" FontStyle="Normal" FontWeight="Normal" Foreground="Black"
                            RenderTransformOrigin="0.5,0.5" Name="tategakiMul1">
            <tg:TategakiMultiline.RenderTransform>
                <ScaleTransform ScaleX="-1" />
            </tg:TategakiMultiline.RenderTransform>
        </tg:TategakiMultiline>
    </StackPanel>
</Grid>

XAMLの中のテキストで改行をするには、\nではなく&#xa;を使えば良いようです。改行コードそのままです。例によってStackPanelは右から左に並べるとコントロールの中身が左右反転してしまうので(おそらくアラビア語とかのためのコントロール)、あらかじめコントロールをRenderTransformで左右反転してからStackPanelに与えています。

さて、 ダウンロードはこちらからどうぞ。
WPF用縦書きテキストブロック Tategaki ver.1.1.0
((2015/1/22)都合により削除しました。ver1系はver.1.1.2を使ってください)

0 件のコメント:

コメントを投稿