ホーム >プログラム >Delphi 6 ローテクTips

UnicodeとAnsiStringの可逆変換

ウムラウトのようなUnicode文字を含んだファイル名をTStringsで管理したいと思いました。
それができればTListBoxやTStringGridなどのコンポーネントで利用できるので便利ですし。

始めに

文字列の管理に便利なTStringsやTStringListですがUnicode(WideString)を直接扱えません。
単純に代入することはできますがウムラウトのようなUnicode文字が代替文字に変換されてしまいます。
たとえばQueensrÿche.mp3がQueensryche.mp3に変わってしまいます。
そうなるとÿとyは違う文字なのでリストに格納されているQueensryche.mp3をファイル名に与えても「ファイルが存在しない」と言われてしまうわけです。
でもなんとかしたいと。

まず思い付いたのがURLに日本語などを渡すときにやる%の後にその文字の16進数値を書くURLエンコードのようなやり方です。
HTMLにUnicode文字を埋め込む場合にも同じようなやり方をしますね。
この二つを足して2で割ったら1余っちゃった…みたいな変換方法でいってみたいと思います。
具体的にはWideStringの文字列を頭からみていってAnsiStringで扱えないUnicode文字に出くわしたらその文字だけを$nnnnと置き換えて(nは0〜Fの16進数値の文字列)AnsiStringに変換しましょうと。
逆にAnsiStringからWideStringにする時は、$に出くわしたら続く4文字を16進の数値文字列であるとみなしてUnicode文字に置き換えれば良いと。
これでたとえば「Queensrÿche.mp3」は「Queensr$00FFche.mp3」となり可読性は劣りますが可逆変換なので元に戻せばちゃんと「Queensrÿche.mp3」になります。
ちなみに「冬馬由美 - そして我愛你.wma」は「冬馬由美 - そして我愛$4F60.wma」となります。

リストに入れる時はgfnsWideToAnsiEx関数でAnsiStringに変換させ、使うときは逆にgfnsWideToAnsiEx関数でWideStringに変換させて取り出す必要があるのでややこしいのが難点ですが、まぁ背に腹は変えられないということで。

Unicode → Shift-JIS

//--- WideString⇔AnsiString
//WideStringとAnsiStringの可逆変換関数
const
  l_csDEFAULT_CHAR = #1;
  l_csPREFIX_CHAR  = '$';
function gfnsWideToAnsiEx(const sWSrc: WideString): AnsiString;
{
WideString→AnsiString
WideStringをAnsiStringに変換
キャストするだけやWideCharToString関数と違ってウムラウトなどのUnicode文字を$+16進に変換して返す

http://yokohama.cool.ne.jp/chokuto/urawaza/api/WideCharToMultiByte.html
0x00000400 (WC_NO_BEST_FIT_CHARS)
Windows 2000/XP: 直接マルチバイト文字に変換できない文字をデフォルトキャラクタに置きかえます。
}

var
  i, li_Pos, li_Len: Integer;
  lb_Bool: LongBool;
  ls_PStr: PAnsiChar;
begin
  Result := '';
  if (sWSrc <> '') then begin
    li_Len  := WideCharToMultiByte(CP_ACP, $400, PWideChar(sWSrc), -1, nil, 0, l_csDEFAULT_CHAR, @lb_Bool);
    if (lb_Bool = False) and (Pos(l_csPREFIX_CHAR, sWSrc) = 0) then begin
      //ウムラウトのようなUnicode文字がなくプリフィクス文字($)もない場合は変換する必要なし
      //$は2バイト文字の第一バイトにも第二バイトにも現れないのでPosでよい
      Result := sWSrc;
    end else begin
      ls_PStr := AllocMem(li_Len + 1);
      try
        li_Len := WideCharToMultiByte(CP_ACP, $400, PWideChar(sWSrc), -1, ls_PStr, li_Len, l_csDEFAULT_CHAR, nil);
        if (li_Len > 0) then begin
          Dec(li_Len);
          i := 0;       //ls_PStr のインデックス
          li_Pos := 1;  //sWSrc   のインデックス
          repeat
            if (ls_PStr[i] = l_csDEFAULT_CHAR) or (ls_PStr[i] = l_csPREFIX_CHAR) then begin
              //デフォルトキャラクタとプリフィクスキャラクタは $xxxx の書式に変換
              Result := Result + l_csPREFIX_CHAR + Format('%0.4x', [Ord(sWSrc[li_Pos])]);
            end else begin
              Result := Result + ls_PStr[i];
            end;
            if (IsDBCSLeadByte(Byte(ls_PStr[i]))) then begin
              //2バイト文字の先頭バイトなのでもう1バイトスキップ
              Result := Result + ls_PStr[i + 1];
              Inc(i, 2);
            end else begin
              Inc(i);
            end;
            Inc(li_Pos);
          until (i >= li_Len);  //untilは条件がTrueで終了
        end;
      finally
        FreeMem(ls_PStr);
      end;
    end;
  end;
end;
  1. ウムラウトのような直接マルチバイト文字に変換できないUnicode文字をデフォルトキャラクタに置きかえるオプション($400)をつけて変換。
    WideCharToMultiByte(CP_ACP, $400, PWideChar(sWSrc), -1, ls_PStr, li_Len, l_csDEFAULT_CHAR, @lb_Bool)
  2. 変換後の文字列を頭から見ていき、デフォルトキャラクタが出てきたら元の文字列の該当文字(ウムラウトのようなUnicode文字)を$+4桁の16進値に変換。
    //デフォルトキャラクタ(#1)とプリフィクス($)は変換対象
    if (ls_PStr[i] = l_csDEFAULT_CHAR) or (ls_PStr[i] = l_csPREFIX_CHAR) then begin
      Result := Result + l_csPREFIX_CHAR + Format('%0.4x', [Ord(sWSrc[li_Pos])]);
    プリフィクスをデフォルトキャラクタである#1と同じにしてしまえば#1だけの検索でよくなるのでもっと速くすることができますが、一応テキストファイルに保存することも考えてこのような仕様にしています。

この方法はShift-JISにちゃんと変換できない文字だけを変換するようにしているURLエンコードもどきなのでUTF-7を利用した場合よりも可読性に優れます。
フォームの設計時やTStringGridのセルに文字列をセットするときも変換させずにそのまま代入OKというメリットがあります。
また"$+4桁のUnicodeのコード番号"という形式なので文字コード表からコード番号を拾って記述すればソースコード中にUnicode文字を埋め込むことも比較的簡単にできます。

Shift-JIS → Unicode

次に独自変換を施した文字列を元に戻す関数を作ります。

function gfnsAnsiToWideEx(const sSrc: AnsiString): WideString;
//gfnsWideToAnsiExの逆
//2010-06-06:プリフィクス文字を % から $ に変更。
//2008-05-30:速度向上のため書き換え。200倍くらい速くなった…
var
  li_Pos, li_Len: Integer;
  ls_WStr, ls_WCnv: WideString;
begin
  Result := '';
  if (sSrc <> '') then begin
    ls_WStr := WideString(sSrc);
    li_Pos  := Pos(l_csPREFIX_CHAR, ls_WStr);
    if (li_Pos = 0) then begin
      //2008-02-12:プリフィクス文字($)がなければ変換する必要はない
      Result := ls_WStr;
    end else begin
      li_Len  := Length(ls_WStr);
      repeat
        //'$'までの文字列を足しこむ
        Result := Result + Copy(ls_WStr, 1, li_Pos - 1);
        try
          //'$'に続く(はずの)4文字の数値をUnicode文字に変換
          ls_WCnv := WideString(WideChar(StrToInt(Copy(ls_WStr, li_Pos, 5))));
          Result  := Result + ls_WCnv;
          ls_WStr := Copy(ls_WStr, li_Pos +4 +1, li_Len);  //4文字の数値の次からコピー開始
        except
          //イレギュラー
          Result  := Result + WideString(l_csPREFIX_CHAR);  //イレギュラー時はそのままにする
          ls_WStr := Copy(ls_WStr, li_Pos +1, li_Len);        //'$'の次からコピー開始
        end;
        //次の'$'までの位置を取得
        li_Pos := Pos(l_csPREFIX_CHAR, ls_WStr);
        Application.ProcessMessages;
      until (li_Pos = 0);  //untilは条件がTrueで終了
      Result := Result + ls_WStr;
    end;
  end;
end;

文字列中に$に続く4桁の数値文字列('0000'〜'FFFF')があれば変換

  try
    //'$'に続く(はずの)4文字の数値をUnicode文字に変換
    ls_WCnv := WideString(WideChar(StrToInt(Copy(ls_WStr, li_Pos, 5))));
    Result  := Result + ls_WCnv;
    ls_WStr := Copy(ls_WStr, li_Pos + 4 + 1, li_Len);  //4文字の数値の次からコピー開始
  except
    //イレギュラー
    Result  := Result + WideString(l_csPREFIX_CHAR); //イレギュラー時はそのままにする
    ls_WStr := Copy(ls_WStr, li_Pos +1, li_Len);       //'$'の次からコピー開始
  end;

ここでエラーがあればイレギュラーとして変換させずに「$」のままにして処理を続けます。

※この関数はgfnsWideToAnsiEx関数で変換させた文字列に対して使うもので、そうでない文字列に対して使うとおかしなことになります。

使い方

Unicode対応のドラッグアンドドロップを例として。
WindowsMediaPlayerを取り込んで貼り付け。

//ドラッグアンドドロップ
procedure TForm1.WMDropFiles(var Msg: TWMDropFiles);
var
  i, li_Buff: Integer;
  lp_File:    PWideChar;
  lsl_List:   TStrings;
begin
  lsl_List := TStringList.Create;
  try
    //ドロップされたファイルの数だけ処理を行う
    for i := 0 to DragQueryFileW(Msg.Drop, $FFFFFFFF, nil, 0) -1 do begin
      li_Buff := DragQueryFileW(Msg.Drop, i, nil, 0);
      lp_File := AllocMem((li_Buff +1) * 2);
      try
        DragQueryFileW(Msg.Drop, i, lp_File, li_Buff +1);
        //リストへWideStringを追加
        lsl_List.Add(gfnsWideToAnsiEx(WideString(lp_File)));
      finally
        FreeMem(lp_File);
      end;
    end;
    DragFinish(Msg.Drop);  //ドラッグアンドドロップ処理の終了

    //--------------------------------
    //リストのファイルをランダムに取り出して再生
    Randomize;
    //リストからWideStringでランダムに取り出し
    WindowsMediaPlayer1.URL := gfnsAnsiToWideEx(lsl_List[Random(lsl_List.Count)]);
    WindowsMediaPlayer1.controls.play;
    //--------------------------------
  finally
    lsl_List.Free;
  end;
end;

高速版

SetLengthを活用することで20〜30倍速く出来ます。

function gfnsWideToAnsiEx(sSrc: WideString): AnsiString;
//ウムラウトなどのUnicodeな文字だけを$+16進に変換して返す。
var
  i,
  li_Pos,
  li_Len,
  li_Count,
  li_Index : Integer;
  ls_Char  : AnsiChar;
  lb_Bool  : LongBool;
  lp_Buff  : PAnsiChar;
begin
  Result := '';
  if (sSrc <> '') then begin
    //WC_COMPOSITECHECKは指定してはいけない。
    li_Len := WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, PWideChar(sSrc), -1, nil, 0, l_csDEFAULT_CHAR, @lb_Bool);
    if (lb_Bool = False) and (Pos(l_csPREFIX_CHAR, sSrc) = 0) then begin
      //ウムラウトのようなUnicode文字がなくプリフィクス文字($)もない場合は変換する必要なし。
      //'$'は2バイト文字の第一バイトにも第二バイトにも現れないのでPosでよい。
      Result := sSrc;
    end else begin
      lp_Buff := AllocMem(li_Len + 1);
      try
        li_Len := WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, PWideChar(sSrc), -1, lp_Buff, li_Len, l_csDEFAULT_CHAR, @lb_Bool);
        if (li_Len > 0) then begin
          //まず必要なバイト数を求める
          li_Count := li_Len; //必要なバイト数
          i := 0;
          Dec(li_Len);
          //forで回すよりこっちの方が速い。
          repeat
            ls_Char := lp_Buff[i];
            if (ls_Char = l_csDEFAULT_CHAR) //#1→'$xxxx' xxxx は元の文字の4桁の16進値
            or (ls_Char = l_csPREFIX_CHAR)  //$→ '$0024'
            then begin
              Inc(li_Count, 4);
            end;
            Inc(i);
          until (i >= li_Len);  //untilは条件がTrueで終了。
          SetLength(Result, li_Count);

          i        := 0; //lp_Buffのインデックス
          li_Pos   := 1; //sSrc   のインデックス
          li_Index := 1; //Result のインデックス
          repeat
            ls_Char := lp_Buff[i];
            if (ls_Char = l_csDEFAULT_CHAR) //#1→'$xxxx' xxxx は元の文字の4桁の16進値
            or (ls_Char = l_csPREFIX_CHAR)  //$→ '$0024'
            then begin
              Result[li_Index] := l_csPREFIX_CHAR;
              Inc(li_Index);
              StrFmt(@Result[li_Index], '%0.4x', [Ord(sSrc[li_Pos])]);
              Inc(li_Index, 4);
            end else begin
              Result[li_Index] := ls_Char;
              Inc(li_Index);
              //この判定はないといけない。
              if (IsDBCSLeadByte(Byte(ls_Char))) thenbegin
                //2バイト文字の先頭バイトなのでもう1バイトスキップ。
                Inc(i);
                Result[li_Index] := lp_Buff[i];
                Inc(li_Index);
              end;
            end;
            Inc(i);
            Inc(li_Pos);
            //10%近く遅くなるのでコメントアウト
            //Application.ProcessMessages;
          until (i >= li_Len);  //untilは条件がTrueで終了。
          SetLength(Result, li_Index -1);
        end;
      finally
        FreeMem(lp_Buff);
      end;
    end;
  end;
end;

function gfnsAnsiToWideEx(sSrc: AnsiString): WideString;
//gfnsWideToStrExの逆。
var
  ls_Code : array[0..4] of WideChar;
  ls_Char : WideChar;
  li_Read,
  li_Write,
  li_Pos,
  li_Len  : Integer;
begin
  Result := '';
  if (sSrc <> '') then begin
    Result := WideString(sSrc);
    li_Pos := Pos(l_csPREFIX_CHAR, Result);
    //プリフィクス文字($)がなければ独自変換する必要はない。
    if (li_Pos > 0) then begin
      ls_Code[0] := '$';
      li_Len     := Length(Result);
      li_Read    := li_Pos;         //読み込む位置
      li_Write   := li_Read;        //書き込む位置
      repeat
        if (Result[li_Read] = l_csPREFIX_CHAR) then begin
          try
            //'$'に続く(はずの)4文字の数値をUnicode文字に変換。
            Move(Result[li_Read +1], ls_Code[1], 4 * SizeOf(WideChar));
            ls_Char          := WideChar(StrToInt(ls_Code));
            Result[li_Write] := ls_Char;
            Inc(li_Read, 5);
            Inc(li_Write);
          except
            //イレギュラー
            //イレギュラー時はそのままにする。
            Result[li_Write] := Result[li_Read];
            Inc(li_Read);
            Inc(li_Write);
          end;
        end else begin
          Result[li_Write] := Result[li_Read];
          Inc(li_Read);
          Inc(li_Write);
        end;
        //10%近く遅くなるのでコメントアウト
        //Application.ProcessMessages;
      until (li_Read > li_Len);  //untilは条件がTrueで終了。
      SetLength(Result, li_Write -1);
    end;
  end;
end;

  Result := Result + l_csPREFIX_CHAR + Format('%0.4x', [Ord(sWSrc[li_Pos])]);
  SetLength(Result, li_Count);

  Result[li_Index] := l_csPREFIX_CHAR;
  Inc(li_Index);
  StrFmt(@Result[li_Index], '%0.4x', [Ord(sSrc[li_Pos])]);
  Inc(li_Index, 4);

内容的には同じ処理です。
始めのやり方のほうが簡単で分かりやすいと思うのですが足しこむたびにメモリの再割り当てが起きるので速度的に不利になります。
高速版のやり方はちょっとややこしいですがあらかじめ必要なバイト数を計算して確保しておき、その領域に書き込んでいるのでメモリの再割り当てが起きないのでその分高速になります。


2011-04-10:
 高速版を追加。
2010-06-06:
 プリフィクス文字を % から $ に変更。
 IntToStrが$が頭にあると16進数値であるとみなすことから。
2009-02-12:
  gfnsWideToAnsExのlb_BoolをBooleanからLongBoolへ変更。
2008-12-28:
  gfnsStrToWideEx→gfnsAnsiToWideEx gfnsWideToStrEx→gfnsWideToAnsiEx に改名。
2008-06-15:
  1行BBSのアドバイスを糧にUTF-7に変換させるやり方に変更。更に30倍速くなった…
2008-05-30:
  gfnsAnsiToWideExの速度向上を願って書き換え。200倍くらい速くなった…