kazuakix の日記

Windows Phone とか好きです

Windows Phone 8.1 でシフト JIS を読み込む

台風で外出できないのでアプリのユニバーサル化の作業をしていたのですが、シフト JIS を読み込むところで手が止まりました。

ちなみにストア アプリでのコードはこんな感じ、StreamReader に Encoding クラスを指定するだけです。簡単ですね。 ( エラー処理は全無視してます )

using (var req = new HttpClient())
using (var res = await req.GetAsync("http://hogehoge/fugafuga/"))
{
    if (res.IsSuccessStatusCode)
    {
        var sjis = Encoding.GetEncoding("Shift_JIS");
        using (var stream = await res.Content.ReadAsStreamAsync())
        using (var reader = new StreamReader(stream, sjis))
        {
            while (!reader.EndOfStream)
            {
                // 読み込み
            }
        }
    }
}

 
Windows Phone 7 の頃はシフト JIS が読み込めなかったので自前で変換テーブルを用意したりしていたのですが、Phone 8 になってこの辺は...あ、改善してないんですね。

でも まったく進歩していない訳ではなく、Windows Phone が持っている機能を呼び出すことができるようになっています。詳細は @biac さんが解説してくださっていますが、懐かしい Win32API を呼び出すことができるようです。*1

 
まず、ランタイム コンポーネント用のプロジェクトを追加します。

今回はユニバーサル アプリなので [Visual C++] - [ストアアプリ] - [Windows Phone アプリ] の中の「 Windows ランタイム コンポーネント (Windows Phone) 」を選びます。「 Windows ランタイム コンポーネント (Windows Phone Silverlight) 」というのもあるので間違えないようにしましょう。

f:id:kazuakix:20140810212002j:plain

プロジェクトが追加できたら上記記事の内容で Class1.h と Class1.cpp を書き換えてやります。 ( ファイル名も変えたりします ) ちなみに名前空間 ( プロジェクト名 ) を SjisEncodingComponent 、クラス名を SjisEncoding としました。

SjisEncoding.h

#pragma once
#include <ppltasks.h>

#define  CP_SJIS  932

namespace SjisEncodingComponent
{
	public ref class SjisEncoding sealed
	{
	public:
		static Platform::String^ MultiByteToWideChar(const Platform::Array<byte>^ buff);
	};
}

 
SjisEncoding.cpp

#include "pch.h"
#include "SjisEncoding.h"

using namespace SjisEncodingComponent;
using namespace Platform;

Platform::String^ SjisEncoding::MultiByteToWideChar(const Platform::Array<byte>^ buff)
{
	LPCSTR pBuff = (LPCSTR)(buff->Data);
	if (pBuff == NULL)
		return ref new Platform::String();  // 空文字を返す

	const int nSize = ::MultiByteToWideChar(CP_SJIS, 0, pBuff, -1, NULL, 0);
	BYTE* buffUtf16 = new BYTE[nSize * 2 + 2];

	::MultiByteToWideChar(CP_SJIS, 0, pBuff, -1, (LPWSTR)buffUtf16, nSize);

	Platform::String^ result = ref new Platform::String((LPWSTR)buffUtf16);
	delete[] buffUtf16;  // *で受けたオブジェクトは従来どおり自前で解放する

	return result;
}

 
このプロジェクトを Windows Phone プロジェクトの参照設定に追加します。

f:id:kazuakix:20140810213612j:plain

これで、SjisEncodingComponent.SjisEncoding.MultiByteToWideChar() にシフト JIS のバイト列を渡すことで文字コード変換をしてくれるようになります。

...でも、ちょっとストア アプリのコードを大きく書き直さないといけないですね。よろしくないですね。なので、StreamReader クラスに渡せるように Encoding クラスの派生クラスを作ります。

Encoding の派生クラスに必要なメソッドのうち、シフト JIS のバイト列から文字列への変換に必要な部分だけを実装しました。読み込むだけならこれでいけます。*2

class SjisEncoding : Encoding
{
    public override string WebName { get { return "Shift_JIS"; } }

    // ShiftJIS バイト列 → Unicode 文字列
    public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
    {
        var result = SjisEncodingComponent.SjisEncoding.MultiByteToWideChar(bytes.Skip(byteIndex).Take(byteCount).ToArray());

        var idx = 0;
        foreach (var c in result)
        {
            chars[charIndex + idx] = c;
            idx++;
        }

        return result.Length;
    }

    // 文字数
    public override int GetCharCount(byte[] bytes, int index, int count)
    {
        var result = SjisEncodingComponent.SjisEncoding.MultiByteToWideChar(bytes.Skip(index).Take(count).ToArray());

        return result.Length;
    }

    // 最大文字数 (ざっくりバイト数でいいよね?)
    public override int GetMaxCharCount(int byteCount)
    {
        return byteCount;
    }

    // 以下、必要になったら
    public override int GetByteCount(char[] chars, int index, int count)
    {
        throw new NotImplementedException();
    }

    public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex)
    {
        throw new NotImplementedException();
    }

    public override int GetMaxByteCount(int charCount)
    {
        throw new NotImplementedException();
    }

}

 
ついでに適切な Encoding クラスを返すヘルパを作りました。
Windows Phone なら上記の SjisEncoding を、それ以外なら標準の Encoding のインスタンスを返すだけです。

public class MyEncoding
{
    public static Encoding GetSjisEncoding()
    {
#if WINDOWS_PHONE_APP
        return new SjisEncoding();
#else
        return Encoding.GetEncoding("Shift_JIS");
#endif
    }
}

 
これで元のコードにほとんど手を入れることなく Windows Phone 対応ができました。

using (var req = new HttpClient())
using (var res = await req.GetAsync("http://hogehoge/fugafuga/"))
{
    if (res.IsSuccessStatusCode)
    {
        var sjis = MyEncoding.GetSjisEncoding();
        using (var stream = await res.Content.ReadAsStreamAsync())
        using (var reader = new StreamReader(stream, sjis))
        {
            while (!reader.EndOfStream)
            {
                // 読み込み
            }
        }
    }
}

*1:これができるなら Encoding クラス対応してくれてもいいんじゃない? と思うのですが...

*2:よく考えたら、書き込みのときに逆も必要になりますね...