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

Unicode対応LoadFromFile SaveToFile

Unicodeファイル名対応LoadFromFile SaveToFileでUnicodeのファイル名に対応できたので次は中身のテキストもがんばってUnicodeに対応しましょう、というのがこのページの趣旨。

下準備

まずは下請けの関数を一つ。

//ファイルサイズを取得する関数。
function gfniFileSizeGet(const sWFile: WideString): Int64;
//sWFileのサイズをByte単位で返す。
//http://delwiki.info/?Tips%2F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%AF%20Int64%20%E3%81%A7
var
  lh_File: Cardinal;
  lr_FileInfo: TWin32FindDataW;
begin
  Result := 0;
  lh_File := FindFirstFileW(PWideChar(sWFile), lr_FileInfo);
  try
    if (lh_File <> INVALID_HANDLE_VALUE) then begin
      repeat
        if not(BOOL(lr_FileInfo.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY)) then begin
          Result := lr_FileInfo.nFileSizeHigh * (Int64(MAXDWORD) + 1) + lr_FileInfo.nFileSizeLow;
          Break;
        end;
      until not(FindNextFileW(lh_File, lr_FileInfo));
    end;
  finally
    Windows.FindClose(lh_File);
  end;
end;

次に読み書きする時の文字コードを指定するための列挙型を宣言します。
書き込む時はもとより読み込みの時もBOMありのUTF-16やUTF-8であれば自動で文字コードの判定ができますがそうでない場合自動で判定するのはややこしいのでBOMで判定できなかった時の文字コードを引数で指定するようにします。

type
  TMyCharCode = (cdAnsi, cdUnicodeLE, cdUnicodeBE, cdUTF_16LE, cdUTF_16BE, cdUTF_8, cdUTF_8N, cdUTF_7, cdAuto);
cdAnsi Shift-JIS
俗にいう普通〜のテキストファイル。
Unicode特有の文字は代替文字に変わるか ? に変わります。
cdUnicodeLE UTF-16 BOMありのリトルエンディアン
cdUnicodeBE UTF-16 BOMありのビッグエンディアン
cdUTF_16LE UTF-16LE BOMなしのリトルエンディアン
cdUTF_16BE UTF-16BE BOMなしのビッグエンディアン
cdUTF_8 UTF-8 BOMあり
cdUTF_8N UTF-8 BOMなし
cdUTF_7 UTF-7
cdAuto 書き込むテキストにウムラウトのようなUnicode文字があればUTF-8で、なければShift-JISで書き込みます。

XP付属のメモ帳(Notepad.exe)の「ファイル」→「名前をつけて保存」で「文字コード」の選択で

にそれぞれ対応します。
そして

になります。

ちなみにTStringsの文字列はUTF-7を使ってWideStringをAnsiStringに変換しているものとします。

読み込み

//Unicode対応ファイル読み込み関数。
function gfniFileRead(var pData: PAnsiChar; sWFile: WideString; const iByte: DWORD): DWORD;
//ファイルをpDataに読み込んで、読み込んだバイト数を返す関数。
//2バイト余計にメモリ確保するのは呼び出し側でWideString(pData)などとしても後ろにゴミがつかないようにするため

var
  lh_Handle: THandle;
begin
  Result := 0;
  lh_Handle := CreateFileW(
      PWideChar(sWFile),      //ファイル名
      GENERIC_READ,           //アクセスモード
      FILE_SHARE_READ,        //共有モード
      nil,                   //セキュリティ
      OPEN_EXISTING,          //作成方法
      FILE_ATTRIBUTE_NORMAL,  //ファイル属性
      0                       //テンプレート
  );

  if (lh_Handle <> 0) then begin
   try
      //呼び出し側でpDataのメモリーを開放をする必要あり
      pData := AllocMem(iByte + 2);  //2バイト余計に確保することで後の処理が楽になる
      ReadFile(lh_Handle, pData^, iByte, Result, nil);
    finally
      CloseHandle(lh_Handle);
    end;
  end;
end;
function gfnsFileReadText(sFile: WideString; var cdCode: TMyCharCode): WideString;
//Unicode対応。
//cdCodeにはBOMがない時に適用する文字コードを指定する。
var
  lp_Buff: PAnsiChar;
  ls_Swap: AnsiChar;
  li_Size, li_Start, i: DWORD;
begin
  li_Size := gfniFileRead(lp_Buff, sFile, gfniFileSizeGet(sFile));
  if (li_Size <= 0) then begin
    Result := '';
  end else begin
    try
      li_Start := 0;
      if  (lp_Buff[0] = #$EF)
      and (lp_Buff[1] = #$BB)
      and (lp_Buff[2] = #$BF)
      then begin
        //UTF-8 BOMあり
        cdCode := cdUTF_8;
        Inc(li_Start, 3);
      end else
      if  (lp_Buff[0] = #$FF)
      and (lp_Buff[1] = #$FE)
      then begin
        //UTF-16 BOMありのリトルエンディアン
        cdCode := cdUnicodeLE;
        Inc(li_Start, 2);
        Dec(li_Size, 2);
      end else
      if  (lp_Buff[0] = #$FE)
      and (lp_Buff[1] = #$FF)
      then begin
        //UTF-16 BOMありのビッグエンディアン
        cdCode := cdUnicodeBE;
        Inc(li_Start, 2);
        Dec(li_Size, 2);
      end;

      if (cdCode = cdUTF_8) or (cdCode = cdUTF_8N) then begin
        //UTF-8
        //Utf8DecodeはBOMも変換する
        Result := Utf8Decode(Utf8String(PAnsiChar(@lp_Buff[li_Start])));
      end else
      if (cdCode = cdUnicodeLE)
      or (cdCode = cdUnicodeBE)
      or (cdCode = cdUTF_16LE)
      or (cdCode = cdUTF_16BE)
      then begin
        //UTF-16
        if (cdCode = cdUTF_16BE) or (cdCode = cdUnicodeBE) then begin
          for i := li_Start to li_Start + li_Size - 1 do begin
            if ((i - li_Start) mod 2 = 1) then begin
              //入れ替え
              ls_Swap := lp_Buff[i];
              lp_Buff[i] := lp_Buff[i -1];
              lp_Buff[i -1] := ls_Swap;
              Application.ProcessMessages;
            end;
          end;
        end;
        //gfniFileReadでlp_Buffを2バイト余計にAllocMemでメモリ確保しているのでWideStringでキャストOK
        Result := WideString(PWideChar(@lp_Buff[li_Start]));
      end else begin
        //Shift-JISあるいはUTF-7あるいはcdAuto
        //gfniFileReadでlp_Buffを2バイト余計にAllocMemでメモリ確保しているのでAnsiStringでキャストOK
        Result := AnsiString(lp_Buff);
        if (cdCode = cdUTF_7) then begin
          //UTF-7
          Result := gfnsUtf7ToWide(Result);
        end;
      end;
    finally
      FreeMem(lp_Buff);
    end;
  end;
end;

procedure gpcLoadFromFile(slList: TStrings; sFile: WideString; cdCode: TMyCharCode);
//Unicode対応のLoadFromFile
//cdCodeにはBOMがない時に適用する文字コードを指定する

begin
  slList.Clear;
  ls_Text := ;
  //WideStringをUTF-7に変換してTStringsに持つ
  slList.Text := gfnsWideToUtf7(gfnsFileReadText(sFile, cdCode));
end;

TStringsにはWideStringをUTF-7に変換してAnsiStringで持つようにします。
ListBoxなどで表示するときはOnDrawItemイベントを使います。

sFileがBOMつきのUTF-16、あるいはBOMつきのUTF-8テキストであれば文字コードが自動で判定されるのでcdCodeは無視されます。
cdCodeはBOMのついていないファイルの場合に使用されるだけで強制的に指定した文字コードで読み込むわけではありません。
またcdAutoは名称からすると自動で文字コードを判定しそうですが、これは書き込みの時に指定するもので、読み込みのときに指定した場合はcdAnsiと同じ動作になります。

書き込み

//Unicode対応のファイル書き込み関数。
function gfnhFileWriteOpen(sFile: WideString): THandle;
begin
  Result := CreateFileW(
      PWideChar(sFile),       //ファイル名
      GENERIC_WRITE,          //アクセスモード
      0,                      //共有モード
      nil,                    //セキュリティ
      CREATE_ALWAYS,          //作成方法
      FILE_ATTRIBUTE_NORMAL,  //ファイル属性
      0                       //テンプレート
  );
end;

function gfniFileWrite(hHandle: THandle; pData: PAnsiChar; iWrite: DWORD): DWORD; overload;
begin
  if (hHandle <> 0) then begin
    WriteFile(hHandle, pData^, iWrite, Result, nil);
  end;
end;

function gfniFileWrite(sWFile: WideString; pData: PAnsiChar; iWrite: DWORD): DWORD; overload;
//pDataの内容をsWFileにiWriteバイト書き込み書き込んだバイト数を返す。
var
  lh_Handle: THandle;
begin
  Result := 0;
  lh_Handle := gfnhFileWriteOpen(sWFile);

  if (lh_Handle <> 0) then begin
    try
      WriteFile(lh_Handle, pData^, iWrite, Result, nil);
    finally
      CloseHandle(lh_Handle);
    end;
  end;
end;
procedure gpcFileWriteText(sWFile, sWText: WideString; cdCode: TMyCharCode);
//sWTextを文字コードcdCodeでsWFileに書き込む
//Unicode対応

var
  lp_Buff: PAnsiChar;
  li_Len: Integer;
  i: DWORD;
  ls_Str: AnsiString;
  lh_Handle: THandle;
begin
  if (cdCode = cdUTF_8)
  or (cdCode = cdUTF_8N)
  or ((cdCode = cdAuto) and gfnbIsUnicode(sWText))
  then begin
    //UTF-8
    ls_Str := Utf8Encode(sWText);
    if (cdCode = cdUTF_8N) then begin
      //BOMなし
      gfniFileWrite(sWFile, PAnsiChar(ls_Str), Length(ls_Str) - 1);
    end else begin
      //BOMあり
      lh_Handle := gfnhFileWriteOpen(sWFile);
      try
        gfniFileWrite(lh_Handle, #$EF#$BB#$BF, 3);                        //BOM書き込み
        gfniFileWrite(lh_Handle, PAnsiChar(ls_Str), Length(ls_Str) - 1);  //本文書き込み
      finally
        CloseHandle(lh_Handle);
      end;
    end;
  end else if (cdCode = cdUTF_7) then begin
    //UTF-7
    ls_Str := gfnsWideToUtf7(sWText);
    gfniFileWrite(sWFile, PAnsiChar(ls_Str), Length(ls_Str));
  end else
  if (cdCode = cdUnicodeLE)
  or (cdCode = cdUnicodeBE)
  or (cdCode = cdUTF_16LE)
  or (cdCode = cdUTF_16BE)
  then begin
    //UTF-16
    li_Len    := Length(sWText) * 2;
    lh_Handle := gfnhFileWriteOpen(sWFile);
    try
      if (cdCode = cdUnicodeBE) or (cdCode = cdUTF_16BE) then begin
        //ビッグエンディアン
        lp_Buff := AllocMem(li_Len);
        try
          if (cdCode = cdUnicodeBE) then begin
            //UTF-16 BOMありのビッグエンディアン
            gfniFileWrite(lh_Handle, #$FE#$FF, 2);  //BOM書き込み
          end;
          for i := 0 to li_Len - 1 do begin
            //入れ替え
            if (i mod 2 = 0) then begin
              lp_Buff[i + 1] := PAnsiChar(PWideChar(sWText))[i];
            end else begin
              lp_Buff[i - 1] := PAnsiChar(PWideChar(sWText))[i];
              Application.ProcessMessages;
            end;
          end;
          gfniFileWrite(lh_Handle, lp_Buff, DWORD(li_Len));  //本文書き込み
        finally
          FreeMem(lp_Buff);
        end;
      end else if (cdCode = cdUnicodeLE) or (cdCode = cdUTF_16LE) then begin
        //リトルエンディアン
        if (cdCode = cdUnicodeLE) then begin
          //UTF-16 BOMありのリトルエンディアン
          gfniFileWrite(lh_Handle, #$FF#$FE, 2);  //BOM書き込み
        end;
        gfniFileWrite(lh_Handle, PAnsiChar(PWideChar(sWText)), li_Len);  //本文書き込み
      end;
    finally
      CloseHandle(lh_Handle);
    end;
  end else begin
    //Shift-JIS
    //AnsiString(sWText)のようにキャストするだけだとUnicodeの合成文字が正しく変換されない場合あり。
    ls_Str := gfnsWideToAnsi(sWText);
    gfniFileWrite(sWFile, PAnsiChar(ls_Str), Length(ls_Str));
  end;
end;

procedure gpcSaveToFile(slList: TStrings; sWFile: WideString; cdCode: TMyCharCode);
{
Unicode対応SaveToFile
文字コードcdCodeの指定で
Shift-JIS
UTF-16のBOMありのリトルエンディアン
UTF-16のBOMありのビッグエンディアン
UTF-16LE
UTF-16BE
UTF-8  (BOMあり)
UTF-8N (BOMなし)
UTF-7
の書き込みに対応
}
begin
  //WideStringをUTF-7に変換してTStringsに持っていると仮定。
  gpcFileWriteText(sWFile, gfnsUtf7ToWide(slList.Text), cdCode);
end;

gpcLoadFromFileでWideStringをUTF-7にしてTStringsに読み込んだり、追加するときにUTF-7に変換させたりしたものをWideStringに戻してからファイルに書き込みます。
この場合重要なのは、普通のAnsiStringの文字列であってもTStringsに追加するときはUTF-7に変換しなければならないということです。
そうでないとノーマルのテキストであってもgfnsUtf7ToWideで変換してしまうので文字化けしてしまいます。


2009-02-12:
  gfnsFileReadTextを若干書き換え。
  自作関数にリンクを張って分かりやすく。
2008-09-17:
  gpcFileWriteTextの下請け関数gfniFileWriteを載せた。
2008-05-30: