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;
- ウムラウトのような直接マルチバイト文字に変換できないUnicode文字をデフォルトキャラクタに置きかえるオプション($400)をつけて変換。
WideCharToMultiByte(CP_ACP, $400, PWideChar(sWSrc), -1, ls_PStr, li_Len, l_csDEFAULT_CHAR, @lb_Bool)
- 変換後の文字列を頭から見ていき、デフォルトキャラクタが出てきたら元の文字列の該当文字(ウムラウトのような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倍くらい速くなった…