NotesとQtでWindows、Mac OS X、Ubuntuのデスクトップアプリ(その6 - LMBCS変換・応用編)

LMBCSを扱うにあたり、前回のコードでは、LMBCSの区切り位置を計測できずにいました。

APIを詳しく見ていくと、LMBCS文字列を含む言語サービスが用意されています。ヘッダファイルnls.hで提供されるNational Language Services(NLS)には、多国語のキャラクタセットに対応した文字列処理関数が用意されていて、OSTranslate関数の代替となるNLS_Translate関数もあります。 LMBCSの文字区切り判定についても、文字数を返すNLS_string_chars、バイト数を返すNLS_string_bytesを組み合わせることで、LMBCSを正しい位置で区切ることが可能になり、WORD幅を超えるLMBCS文字列も、範囲ごとに変換してつなぎ合わせていけば処理が可能になります。

それでは、NLSを活用した新しいLmbcsクラスを紹介します。ヘッダファイルは変更がないので、ソースファイルのみ紹介します。

<lmbcs.cpp>

#include "lmbcs.h"

#include <QString>

#if defined(NT)
#pragma pack(push, 1)
#endif

#include <osmisc.h>
#include <nls.h>

#if defined(NT)
#pragma pack(pop)
#endif

namespace ntlx {

const WORD UNICODE_BYTE = (sizeof(ushort) / sizeof(char));
const WORD LMBCS_DELTA = (1024 * 31);
const int UNICODE_DELTA = (1024 * 42 / UNICODE_BYTE);

Lmbcs::Lmbcs()
  : QByteArray()
{
}

Lmbcs::Lmbcs(const char *s, int len)
  : QByteArray(s, len)
{
}

Lmbcs::Lmbcs(STATUS status)
  : QByteArray()
{
  char buffer[MAXWORD] = "";
  WORD lmbcsLen = OSLoadString(0, Status(status).error(), buffer, MAXWORD);
  *this = Lmbcs(buffer, lmbcsLen);
}

Lmbcs::Lmbcs(const Lmbcs& other)
  : QByteArray(other)
{
}

Lmbcs& Lmbcs::operator=(const Lmbcs& other)
{
  if (this == &other) return *this;
  QByteArray::operator=(other);
  return *this;
}

QString Lmbcs::toQString() const
{
  // 変換後の文字列をセットする変数
  QString result;
  char buffer[MAXWORD] = "";

  // キャラクタセットを設定する
  NLS_PINFO pLmbcsInfo = OSGetLMBCSCLS();
  NLS_PINFO pUnicodeInfo;
  NLS_load_charset(NLS_CS_UNICODE, &pUnicodeInfo);

  // 現在の文字列のポインタとサイズ(バイト数)を計算する
  BYTE* ptr = (BYTE*)constData();
  int restSize = size();

  while (restSize > 0)
  {
    // 想定する境界値を計算する
    WORD delta = restSize < (int)LMBCS_DELTA ? (WORD)restSize : LMBCS_DELTA;

    // 境界値から文字数を割り出す
    WORD chars = 0;
    NLS_string_chars(ptr, delta, &chars, pLmbcsInfo);

    // 文字数から実際のバイト数を割り出す
    WORD bytes = 0;
    NLS_string_bytes(ptr, chars, &bytes, pLmbcsInfo);

    // LMBCSからUNICODEに変換する
    WORD retLen = MAXWORD;
    NLS_STATUS status = NLS_translate(
          ptr, bytes
          , (BYTE*)buffer, &retLen
          , NLS_NONULLTERMINATE | NLS_SOURCEISLMBCS | NLS_TARGETISUNICODE
          , pUnicodeInfo
          );
    Q_ASSERT(status == NLS_SUCCESS);

    // UNICODE配列からQStringを作成してストリームに追加する
    result += QString::fromUtf16(reinterpret_cast<ushort*>(buffer)
                                 , (int)retLen / UNICODE_BYTE
                                 );

    // ポインタを進め、残りサイズを減らす
    ptr += (int)bytes;
    restSize -= bytes;
  }

  // ロードしたUNICODEキャラクタセットをアンロードする
  NLS_unload_charset(pUnicodeInfo);

  // ストリームを介して作成したQString文字列を返す
  return result;
}

Lmbcs Lmbcs::fromQString(const QString &qs)
{
  // 変換後の文字列をセットする変数
  QByteArray result;
  char buffer[MAXWORD] = "";

  // キャラクタセットを設定する
  NLS_PINFO pLmbcsInfo = OSGetLMBCSCLS();

  // 開始文字位置とサイズ(文字数)を計算する。
  int index = 0, restSize = qs.size();

  while (restSize > 0)
  {
    // 定義済みのUNICODE処理文字数と比較して変換する文字数を決める
    int chars = restSize < (int)UNICODE_DELTA ? restSize : (int)UNICODE_DELTA;

    // 開始位置と変換文字数から文字列を抜き出す
    QString input = qs.mid(index, chars);

    // 抜き出した文字列をUTF-16の配列に変換する
    const ushort* unicode = input.utf16();

    // 変換するバイト数を計算する
    WORD unicodeLen = (WORD)input.size() * UNICODE_BYTE;

    // UNICODEからLMBCSに変換する
    WORD retLen = MAXWORD;
    NLS_STATUS status = NLS_translate(
          reinterpret_cast<BYTE*>(const_cast<ushort*>(unicode)), unicodeLen
          , (BYTE*)buffer, &retLen
          , NLS_NONULLTERMINATE | NLS_SOURCEISUNICODE | NLS_TARGETISLMBCS
          , pLmbcsInfo
          );
    Q_ASSERT(status == NLS_SUCCESS);

    // LMBCS配列をQByteArrayに追加する
    result.append(buffer, (int)retLen);

    // 開始文字位置を進め、残りサイズを減らす
    index += chars;
    restSize -= chars;
  }

  // ストリームを介して作成したバイト列をLmbcsオブジェクトにして返す
  return Lmbcs(result.constData(), result.size());
}

} // namespace ntlx

LMBCS_DELTAとUNICODE_DELTAは、変換元文字列の単位を表します。 LMBCSをUNICODEに変換する場合は、半角英数字が最大2倍になるので、WORD幅の半分、より気持ち少なめをLMBCSの変換バイト単位にしています。 UNICODEをLMBCSに変換する場合は、全角文字や半角カナが最大1.5倍になるので、WORD幅の約2/3をUNICODEの変換バイト単位にしています。

次に静的メソッドfromQStringを見ていきます。

NLSの関数を使う場合、目的のキャラクタセット(CS)に応じた構造体NLS_INFOへのポインタを用意しておきます。LMBCSとネイティブ文字列(日本語ならShift-JISか?)については、OSGetLMBCSCLSとOSGetNativeCLSで取得できます。それ以外のCSを使う場合については、後述します。

与えられたQString文字列を、UNICODE_DELTAごとに区切りながら、LMBCSに変換していきます。NLSの文字列変換の使い方は、おおよそOSTranslate関数と同じです。変換元、変換先の指定は、5番目の引数で指定します。NLS_SOURCEISUNICODE | NLS_TARGETISLMBCSという組み合わせが、UNICODEからLMBCSへの変換を示します。NLS_NONULLTERMINATEは、変換結果にNULL文字を付加しなくてもいいことを指示します。6番目の引数には、変換後のCSを表すNLS_INFOを指定します。戻り値はNLS特有のNLS_STATUS型のステータス値で、NLS_SUCCESSであれば成功です。

最終的に、つなぎ合わせたLMBCSをLmbcsオブジェクトにして返します。

最後にtoQStringメソッドです。

このメソッドは、変換後のCSがUNICODEになるので、UNICODE用のNLS_INFOを用意します。関数NLS_load_charsetでは、任意のCSをロードして、そのNLS_INFOへのポインタを取得できます。使い終わったらNLS_unload_charsetでアンロードしておきます。

LMBCS文字列も、fromQString同様に変換バイト単位に区切りながら変換、つなぎを繰り返します。その際の区切り位置は、前述の通りNLS_string_charsとNLS_string_bytesを組み合わせて算出します。まず、区切りたいバイト位置まででNLS_string_charsを使って文字数を計算します。区切り位置に次の文字のプレフィックス¥x10が引っかかっていればこの文字は計算に入りません。次に文字数からバイト数を求めるNLS_string_bytesを使ってバイト数を計算します。こうすることで、LMBCS文字列でも正しく区切って処理することが可能になります。

LmbcsクラスはQByteArrayの派生クラスなので、Lmbcsクラス、QStringクラスともに文字数としては、理論上int幅の2Gバイトまで使うことができます。対してOSTranslate関数、NLS_Translate関数ともに処理可能なバイト数はWORD幅の64kバイトです。私が見つけられていないだけかもしれませんが、処理可能なバイト数についてのAPIレベルでの改善が望まれます。(続く)