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

UTF-8を直接TStringListに持つ

Unicode文字列をUTF-8に変換してTStringListに直接持たせて利用しようというページ。

Delphi 6でUTF-8

Delphi 6はUTF-8を取り扱うことができます。
Utf8Encdoe関数とUtf8Decode関数とでWideString(UTF-16)との相互変換ができます。
とはいえCopyやPos(あるいはAnsiPos)などの文字列関数はUTF-8対応ではないので使われる状況というのはコンバート用程度だと思うのですが、もしかしたらこのUTF-8を利用すればTStringsをUnicode対応にできるのではないかと思いつきました。
というのもDelphi 6の場合UTF-8の文字列はUtf8Stringという型で扱います。
このUtf8Stringというのは実はAnsiStringと同じです。
ということはWideStringをUTF-8に変換したものはそのままTStringsに持てるのではなかろうか?
そう思ったわけです。

結果

実際やってみた結果TStringListやTStringGridには問題なく持てるけれどもTListBoxやTComboBoxでは不都合が出るということが分かりました。

TStringListやTStringGridはDelphiが用意したものにデータを保持するけれども、TListBoxやTComboBoxはWindowsが用意したものにデータを保持することからの違いなのだろうかなぁというところです。

UTF-8に変換した文字列をStringGrid(上段)とListBox(下段)にそれぞれ持たせて表示させてみたもの。
下段のリストボックスは日本語の一部(「シャンティ」、「深紅.」)が文字化けしています。

procedure TForm1.Button2Click(Sender: TObject);
var
  lsl_List: TStrings;
  i: Integer;
begin
  if (OpenDialog1.Execute) then begin
    lsl_List := TStringList.Create;
    try
      lsl_List.LoadFromFile(OpenDialog1.FileName);
      StringGrid1.RowCount := lsl_List.Count-1;
      ListBox1.Items.Clear;
      for i := 0 to lsl_List.Count-1 do begin
        //UTF-8に変換して持たせてみる
        StringGrid1.Cells[0, i] := Utf8Encode(WideString(lsl_List[i]));
        ListBox1.Items.Add(Utf8Encode(WideString(lsl_List[i])));
      end;
    finally
      lsl_List.Free;
    end;
  end;
end;

procedure TForm1.StringGrid1DrawCell(Sender: TObject; ACol, ARow: Integer; Rect: TRect; State: TGridDrawState);
begin
  with StringGrid1.Canvas do begin
    FillRect(Rect);
    //UTF-8に変換済みのデータをWideStringにする
    DrawTextW(Handle, PWideChar(Utf8Decode(StringGrid1.Cells[ACol, ARow])), -1, Rect, DT_NOPREFIX or DT_VCENTER or DT_SINGLELINE);
  end;
end;

procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState);
begin
  with ListBox1.Canvas do begin
    FillRect(Rect);
    //UTF-8に変換済みのデータをWideStringにする
    DrawTextW(Handle, PWideChar(Utf8Decode(ListBox1.Items[Index])), -1, Rect, DT_NOPREFIX or DT_VCENTER or DT_SINGLELINE);
  end;
end;

Unicodeなファイルの読み書き

TListBoxやTComboBoxでは文字化けしてしまうので使えなかったUTF-8ですが、それでお役御免かというとそうでもありません。
というのもUTF-8に変換した文字列をTStringListに持たせておき、そのままSaveToFileするとBOMなしのUTF-8のファイルになるからです。
Unicodeな中身のファイルがお手軽に作れてしまうのです。

  slList.Text := Utf8Encode(sWStr); //sWStrはWideString
  if (SaveDialog1.Execute) then begin
    //保存
    slList.SaveToFile(SaveDialog1.FileName); //BOMなしのUTF-8で保存される
  end;

BOMありのUTF-8ファイルとして保存するには #$EF#$BB#$BF を頭に付け足して保存します。

  slList.Text := Utf8Encode(sWStr); //sWStrはWideString
  if (SaveDialog1.Execute) then begin
    //保存
    slList.Text := #$EF#$BB#$BF + slList.Text; //UTF-8のBOM付加
    slList.SaveToFile(SaveDialog1.FileName);   //BOMありのUTF-8で保存される
  end;

割とお手軽にUnicode文字列を保存できます。

UTF-8で保存されているファイルは普通にLoadFromFileで読み込めばUTF-8にエンコードされている文字列がそのままTStringList格納されます。
WideStringにするにはUtf8Decode関数を使います。

BOMなしのUTF-8ファイルならLoadFromFileで読み込んでUtf8Decode関数でWideStringに変換するだけでOKです。

  if (OpenDialog1.Execute) then begin
    lsl_List := TStringList.Create;
    try
      lsl_List.LoadFromFile(OpenDialog1.FileName); //UTF-8で保存されているファイルを読み込む
      sWStr := Utf8Decode(lsl_List.Text); //sWStrはWideString
    finally
      lsl_List.Free;
    end;
  end;

BOMつきのUTF-8ファイルの場合変換後もBOMが残ってしまいうまくないこともあるので、必要ならBOMを取り除いてからUtf8Decode関数でWideStringに変換します。

  if (OpenDialog1.Execute) then begin
    lsl_List := TStringList.Create;
    try
      lsl_List.LoadFromFile(OpenDialog1.FileName); //UTF-8で保存されているファイルを読み込む
      if  (lsl_List[0][1] = #$EF)
      and (lsl_List[0][2] = #$BB)
      and (lsl_List[0][3] = #$BF)
      then begin
        //BOMつきだったので取り除く
        lsl_List[0] := Copy(lsl_List[0], 4, MAXINT);
      end;

      sWStr := Utf8Decode(lsl_List.Text); //sWStrはWideString
    finally
      lsl_List.Free;
    end;
  end;

Unicodeな文字列をUTF-16のままファイルから読みんだりファイルへ書き込んだりしようとするとそこそこ大変なのですが、UTF-8で良ければDelphi 6でもあっけなくできてしまいます。

サロゲートペア

Delphi 6のUtf8Encode関数とUtf8Decode関数はサロゲートペアに対応していません。
サロゲートペアを含んだWideStringもきちんと変換させるにはWindowsのAPIに頼るのが楽で良いと思います。

function gfnsWideToUTF8(sSrc: WideString): UTF8String;
//WideStringをUTF-8にエンコードして返す。
var
  li_Len: Integer;
  lp_Buff: PAnsiChar;
begin
  li_Len  := WideCharToMultiByte(CP_UTF8, 0, PWideChar(sSrc), -1, nil, 0, nil, nil);
  lp_Buff := AllocMem(li_Len + 1);
  try
    WideCharToMultiByte(CP_UTF8, 0, PWideChar(sSrc), -1, lp_Buff, li_Len +1, nil, nil);
    Result := UTF8String(lp_Buff);
  finally
    FreeMem(lp_Buff);
  end;
end;

function gfnsUTF8ToWide(sSrc: UTF8String): WideString;
//UTF-8でエンコードされている文字列をWideStringにして返す。
var
  li_Len: Integer;
  lp_Buff: PWideChar;
begin
  li_Len := MultiByteToWideChar(CP_UTF8, 0, PAnsiChar(sSrc), -1, nil, 0);
  lp_Buff := AllocMem((li_Len + 1) * 2);
  try
    MultiByteToWideChar(CP_UTF8, 0, PAnsiChar(sSrc), -1, lp_Buff, li_Len);
    Result := WideString(lp_Buff);
  finally
    FreeMem(lp_Buff);
  end;
end;

Utf8Encodeの代わりにgfnsWideToUtf8を、Utf8Decodeの代わりにgfnsUtf8ToWideを使用します。


2010-10-30:UTF-8で保存でBOMをシングルクオートで囲んでいた間違いを修正。
2010-10-03:サロゲートペアの項を追加。
2008-06-12: