読者です 読者をやめる 読者になる 読者になる

かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

UWPで𩹉のような4バイト文字を扱う

Vistaからでしょうか。追加された4バイト文字とか2バイトに入りきらない文字たち。こいつら、stringとcharではうまく扱えません。

例を見てみましょう。

こんなXAMLを用意します。

<Page x:Class="App37.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App37"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBox x:Name="TextBox"
                 TextChanged="TextBox_TextChanged" />
        <TextBlock x:Name="TextBlock" />
        <TextBlock x:Name="TextBlockLength" />
    </StackPanel>
</Page>

そして、TextChangedに以下のようなコードを書きます。テキストの長さと、1文字ずつばらした内容を出力するといった感じです。

using Windows.UI.Xaml.Controls;

namespace App37
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            var result = "";
            for (int i = 0; i < this.TextBox.Text.Length; i++)
            {
                result += $"[{this.TextBox.Text[i]}]";
            }

            this.TextBlock.Text = result;
            this.TextBlockLength.Text = this.TextBox.Text.Length.ToString();
        }
    }
}

実行すると以下のような感じに動くプログラムです。

f:id:okazuki:20160227004314p:plain

ここに𩹉とか🐑とか🍖とかを書き込むと残念な結果になります。文字数も出力もだめだめです。

f:id:okazuki:20160227004457p:plain

🐑とかがcharでいう2つぶんのサイズになってるっぽいですね。こういうのを正しく扱うためにStringInfoというクラスがあります。 このクラスでstringをくるんでやることで、文字数を正しくカウントできるようになります。LengthInTextElementsプロパティがそれになります。コードを書き換えてみましょう。

using System.Globalization;
using Windows.UI.Xaml.Controls;

namespace App37
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            var stringInfo = new StringInfo(this.TextBox.Text);
            var result = "";
            for (int i = 0; i < stringInfo.LengthInTextElements; i++)
            {
                result += $"[{this.TextBox.Text[i]}]";
            }

            this.TextBlock.Text = result;
            this.TextBlockLength.Text = stringInfo.LengthInTextElements.ToString();
        }
    }
}

f:id:okazuki:20160227004757p:plain

文字数は改善しましたが、1文字単位に分割するところがまだうまくいってません。これを改善するには、StringInfoのGetNextTextElementメソッドを使えばよさそうに見えます。これを使うと文字列とインデックスを指定するといい感じに、文字を返してくれます。

以下のように修正してみましょう。

using System.Globalization;
using Windows.UI.Xaml.Controls;

namespace App37
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            var stringInfo = new StringInfo(this.TextBox.Text);
            var result = "";
            for (int i = 0; i < stringInfo.LengthInTextElements; i++)
            {
                result += $"[{StringInfo.GetNextTextElement(this.TextBox.Text ,i)}]";
            }

            this.TextBlock.Text = result;
            this.TextBlockLength.Text = stringInfo.LengthInTextElements.ToString();
        }
    }
}

実行すると惜しい感じになります。

f:id:okazuki:20160227005350p:plain

このとき指定するindexは、何文字目かではなく、素のstringのindexなので4バイト文字とかがあるとずれてしまいます。正しいindexを取得するには、StringInfoクラスのParseCombiningCharactersメソッドを使って取得します。 このメソッドの戻り値に、何番目の文字列がstringのindexの何個目かという情報が入っています。こんな感じで使います。

using System.Globalization;
using Windows.UI.Xaml.Controls;

namespace App37
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            var stringInfo = new StringInfo(this.TextBox.Text);
            // 1文字単位に分割するのに必要なindexを取得
            var index = StringInfo.ParseCombiningCharacters(this.TextBox.Text);
            var result = "";
            for (int i = 0; i < stringInfo.LengthInTextElements; i++)
            {
                // ParseCombiningCharactersの戻り値からindexを取得する
                result += $"[{StringInfo.GetNextTextElement(this.TextBox.Text ,index[i])}]";
            }

            this.TextBlock.Text = result;
            this.TextBlockLength.Text = stringInfo.LengthInTextElements.ToString();
        }
    }
}

これで実行するとめでたく正しい表示が出来るようになります。

f:id:okazuki:20160227005823p:plain

まとめ

4バイト文字とかが入ってても正しい文字数を取得するにはStringInfoでくるんでLengthInTextElementsプロパティを使う。1文字単位で文字を取得するにはStringInfo.ParseCombiningCharactersとStringInfo.GetNextTextElementを組み合わせて使う。ということでした。