かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

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を組み合わせて使う。ということでした。