Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2021年01月29日] [更新 2021年03月14日] Delphi 一般・その他
テキストの保存とコードページ指定と文字化け
動作確認等 Windows 7 U64(SP1) + Delphi XE5(UP2) Pro VCL-32
[整理番号 - 0830]







テキスト ( 文字列 ) の保存とコードページ指定と文字化け


Delphi 2009 で Delphi がユニコード化され、10 年以上が経過しました。
Delphi 2007 以前の、いわゆる ANSI 版の Delphi では、文字 はバイト単位の処理で、1 バイト文字、2 バイト文字というのがありました。Delphi 2009 以降の Unicode の文字は、コードポイントの 2 バイトであるエレメント単位の処理になりました。そのことに関するトラブルもありますが、ここではテキストの保存の基本的な処理だけに的を絞って、その動作を確認してみました。






01_文字のバイナリデータ ( コードポイント )


文字のバイナリデータの値を調べてみます。これは、その文字のコード、コードポイントのことです。文字のバイナリ値は Ord 関数で調べることができます。
下図は文字「唖」を調べた結果です。16 進数で 5516 となっています。Delphi 2009 以降の String 型はユニコードの UTF-16 です。
この表には、Shift_JIS には存在しない文字が多くあります。


16 進数 0 1 2 3 4 5 6 7 8 9 A B C D E F
U+5510
U+5520
U+5530


以下の参考リンクのユニコードコンソーシアムのサイトでも、文字のコードポイントを調べることができます。「唖」は 16 進数で 5516 になっています。Unicode の漢字は、情報処理推進機構のサイトでも検索することができます。



最初のリンクでは、[Lookup]ボタンの右側の入力欄に文字、またはコードポイントのの値を入力して [Lookup] ボタンをクリックします。
2 番目のリンクでの検索では、「対応する UCS」が UTF-16 のコードポイントです。UTF-16 のコードポイントは U+XXX の形式で入力します。
3 番目のリンクでは、右上の入力欄に、漢字またはコードポイントの値を入力します。



図1
文字「唖」のコードポイントは 16 進数で 5516

リスト1
//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo1.Lines.Text := '唖娃';
end;

//=============================================================================
//  文字列の最初の文字のコードポイントを 16 進数表示
//  これはその文字のバイナリデータの値
//  結果 : 唖のコードポイントは $5516
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LText : string;
begin
  Memo1.Lines.Add(IntToHex(Ord(Memo1.Lines.Text[1]), 4));

  LText := '唖娃';
  Memo1.Lines.Add(IntToHex(Ord(LText[1]), 4));
end;


TMemo のテキストを TMemo.SaveToStream メソッドでメモリストリームに保存します。そのメモリのデータを String 型の変数に読み込んで、バイナリデータを調べてみます。結果は 16 進数で 88A0 となっています。文字「唖」の Unicode のコードポイントは 5516 ですから、全く違う値です。
下の表は Shift_JIS のコードポイントです。88A0 の箇所を見ると、この「唖」という文字になっています。つまり、88A0 という値は、Shift_JIS の文字コードです。


16 進数 0 1 2 3 4 5 6 7 8 9 A B C D E F
88A0
88B0
88C0


一般的に、同じ文字であっても、文字符号化方式、つまり、コードページが異なれば、コートポイントの値も異なります。



図2
メモリストリームに保存した「唖」の文字コート
  • Shift_JIS のコードポイントになっている

リスト2
//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo1.Lines.Text := '唖娃';
end;

//=============================================================================
//  TMemo の文字列をメモリストリームに書き込む
//  書き込んだ文字列を読み出してコードポイントを 16 進数で表示
//
//  SaveToFile, SaveTpStream で保存するとバイトオーダが逆順になる
//  それを Swap 関数で上位バイトと下位バイトを入れ換えてから処理
//  結果 : 唖のコードポイントは $88A0
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LStream  : TMemoryStream;
  LText    : string;
  LAnsiStr : AnsiString;
begin
  LStream := TMemoryStream.Create;
  try
    Memo1.Lines.SaveToStream(LStream);

    SetLength(LText, LStream.Size);
    LStream.Position := 0;
    LStream.Read(LText[1], LStream.Size);
    Memo1.Lines.Add(IntToHex(Swap(Ord(LText[1])), 4));
  finally
    FreeAndNil(LStream);
  end;

  // AnsiString 型の変数に代入して調べてみる
  LAnsiStr := '唖娃';
  Memo1.Lines.Add(IntToHex(Ord(LAnsiStr[1]), 2) + IntToHex(Ord(LAnsiStr[2]), 2));
end;




02_テキスト保存時のデフォルトは Shift_JIS


TMemo や TStringList 等で、SaveToStream あるいは SaveToFile メソッドでテキストを保存すると、デフォルトで Shift_JIS で保存するようになっています。その確認用のサンプルです。

TMemo に表示しているテキストを SaveToFile で保存して、LoadFromFile で読み込む単純なサンプルです。Shift_JIS にはない文字、ユニコードな文字も含めてテストしています。下図の結果のように、ユニコードな文字は正常に表示できません。



図3
起動時の画面
  • TMemo にテキストを表示
  • これを保存して読み込む
図4
保存して読み込んだ結果
  • Shift_JIS で保存したテキストを Shift_JIS として読み込んで表示したことになる
  • Shift_JIS で保存されているので、ユニコードな文字でなければ正常に表示される

リスト3
var
  FFilePath : string;

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo1.Lines.Clear;

  //文字列をセット
  Memo1.Lines.Add(#$4F60#$597D + ' !!');
  Memo1.Lines.Add(#$D842#$DFB7 + ' (サロゲートペアな文字)');
  Memo1.Lines.Add('テキストを保存');

  FFilePath := ExpandFileName('.\000.txt');
end;

//=============================================================================
//  TMemo のテキストを SaveToFile メソッドでファイルに保存
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Lines.SaveToFile(FFilePath);
end;

//=============================================================================
//  テキストを保存したファイルを読み込んで TMemo で表示
//  ユニコードな文字は正常に表示できない
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  if not FileExists(FFilePath) then Exit;

  Memo1.Lines.LoadFromFile(FFilePath);
end;


以下は TStringList の場合です。結果は上の TMemo の場合と全く同じです。
この仕様により、ユニコードな文字を含まないテキストであれば、Delphi 2009 以降で作成したプログラムで保存したテキストファイルを、Delphi 2007 以前で作成したプログラムでも読み込んで正常に表示できます。逆に、Delphi 2007 以前のプログラムで保存したテキストファイルを、Delphi 2009 以降で作成したプログラムで読み込んで表示することが可能です。

Windows は、Windows Vista で全面的に Unicode 化されましたが、メモ帳のデフォルトの文字コードは ANSI (日本語版の Windows では Shift_JIS) です。メモ帳でテキストを入力して保存する際、文字コードを指定しなければ ANSI での保存となります。



図5
TStringList の場合
  • Shift_JIS で保存したテキストを Shift_JIS として読み込んで表示したことになる
  • ユニコードな文字は保存前と同じ文字にならない

リスト4
var
  FFilePath : string;

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  FFilePath := ExpandFileName('.\000.txt');
end;

//=============================================================================
//  TStringList に文字列をセットしてファイルに保存
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LStrList : TStringList;
begin
  LStrList := TStringList.Create;
  try
    LStrList.Add(#$4F60#$597D + ' !!');
    LStrList.Add(#$D842#$DFB7 + ' (サロゲートペアな文字)');
    LStrList.Add('テキストを保存');

    LStrList.SaveToFile(FFilePath);
  finally
    FreeAndNil(LStrList);
  end;
end;

//=============================================================================
//  保存したテキストを TStringList に読み込んで TMemo に表示
//  ユニコードな文字は正常に表示できない
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LStrList : TStringList;
begin
  if not FileExists(FFilePath) then Exit;

  LStrList := TStringList.Create;
  try
    LStrList.LoadFromFile(FFilePath);
    Memo1.Lines.Text := LStrList.Text;
  finally
    FreeAndNil(LStrList);
  end;
end;


上のサンプルでは、ユニコードな文字をコードポイントで設定しています。Delphi 2009 以降の IDE のコードエディタの [ファイルフォーマット] は UTF-8 にできます。そうすれば、下図のような、ユニコードな文字の編集結果も保存できます。
[ファイルフォーマット] の設定は、コードエディタのポップアップメニューにあります。



図6
IDE のコードエディタ
  • ユニコードな文字も扱える

[備考]
Windows 10 のメモ帳は、2019年5月の Windows 10 の Update で、UTF-8 が既定のコードページに変更されています。




03_テキストの保存と読み込み時のコードページ指定と BOM


Unicode (UTF-16) な文字も保存するには、SaveToStrem メソッドや SaveToFile メソッドの第 2 引数でコードページを指定します。コードページの指定には TEncoding が使用できます。コードページの指定を省略すると、上のサンプルで確認したように、Shift_JIS となります。
ファイルからテキストを LoadFromFile メソッドで読み込む時は、そのファイル内のテキストのコードページを指定して読み込みます。この時も TEncoding が使用できます。

Delphi 2009 以降では、以下の 2 番目のリンクの記事のように、文字列のコードページ変換も簡単に処理できるようになっています。



  TEncoding.Unicode           // Unicode (UTF-16)
  TEncoding.UTF8              // UTF-8
  TEncoding.ANSI              // ANSI (日本語環境では Shift_JIS)
  TEncoding.GetEncoding(932)  // Shift_JIS
  TEncoding.Default           // 日本語環境ではコードページ 932 の Shift_JIS


以下は TMemo のテキストの保存と読み込みの例です。
UTF-8 または Unicode を指定してテキストを保存すると、先頭に BOM と呼ばれる識別用のコードが追加されます。この BOM があると、テキストの読出しの際、コードページを指定しなくても、自動的にコードページの判定が行われます。そのテストもしています。

BOM 付きのテキストファイルを、TMemo.LoadFromSteam, TMemo.LoadFromFile で読み込んでも、BOM 自身のデータは TMemo のデータとしては読み込まれません。BOM は、UTF-8 または Unicode の判定に使用されるだけです。



図7
実行開始時の画面
  • ユニコードな文字を含む、このテキストを保存して読み込む
図8
保存したテキストを読み込んだ結果
  • Unicode で保存したので、ユニコードな文字も正常に表示されている
  • Button2 クリックのコードでも結果は同じ

リスト5
var
  FFilePath : string;

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo1.Lines.Clear;

  //文字列をセット
  Memo1.Lines.Add(#$4F60#$597D + ' !!');
  Memo1.Lines.Add(#$D842#$DFB7 + ' (サロゲートペアな文字)');
  Memo1.Lines.Add('テキストを保存');

  FFilePath := ExpandFileName('.\000.txt');
end;

//=============================================================================
//  TMemo のテキストをファイルに保存
//  保存時に TEncoding を使用してエンコード指定
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Lines.SaveToFile(FFilePath, TEncoding.Unicode);
end;

//=============================================================================
//  保存したテキストファイルを TMemo に読み込む
//  読み込み時に TEncoding を使用してエンコード指定
//
//  保存しているテキストのコートページを明らかにする
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  if not FileExists(FFilePath) then Exit;

  Memo1.Lines.LoadFromFile(FFilePath, TEncoding.Unicode);
end;

//=============================================================================
//  保存したテキストファイルを TMemo に読み込む
//  読み込み時に TEncoding を使用しない場合
//
//  UTF-8 または Unicode (UTF-16) を指定して保存すると先頭に識別子 (BOM) が付く
//  BOM があると、読み込むテキストのコートページを自動判定してくれる
//  一般的にはエンコードを指定した方が良い
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
begin
  if not FileExists(FFilePath) then Exit;

  Memo1.Lines.LoadFromFile(FFilePath);
end;


意図的に BOM なしで保存することもできます。TMemo.Lines や TStringList の TStrings の派生クラスには WriteBOM プロパティがあります。そのプロパティの値を False にします。



procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Lines.WriteBOM := False;
  Memo1.Lines.SaveToFile(FFilePath, TEncoding.Unicode);
end;


BOM なしのテキストを読み込む例です。
文字列のデータを TMemoryStream に書き込みます。TMemoryStream.SaveToFile でそのメモリストリームのデータをファイルに保存します。TMemoryStream の SaveToFile メソッドは、バイトデータをそのまま保存しますから、識別子としての BOM は追加されません。
BOM がない場合、Unicode で保存していても、コードページを指定して読み込まないと、保存されているデータを Shift_JIS のコードポイントのデータとして読み込みます。その結果、保存前と同じ文字にはなりません。



図9
テキスト読み込み時にコードページを指定しない場合
図10
テキスト読み込み時にコードページを指定した場合

リスト6
var
  FFilePath : string;

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo1.Lines.Clear;

  //文字列をセット
  Memo1.Lines.Add(#$4F60#$597D + ' !!');
  Memo1.Lines.Add(#$D842#$DFB7 + ' (サロゲートペアな文字)');
  Memo1.Lines.Add('テキストを保存');

  FFilePath := ExpandFileName('.\000.txt');
end;

//=============================================================================
//  TMemo のテキストを String 型の変数に代入
//  そのテキストのバイナリデータをメモリストリームに書き込んでファイルに保存
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LText   : string;
  LStream : TMemoryStream;
begin
  LText := Memo1.Lines.Text;

  LStream := TMemoryStream.Create;
  try
    LStream.Write(LText[1], Length(LText) * SizeOf(Char));
    LStream.SaveToFile(FFilePath);
  finally
    FreeAndNil(LStream);
  end;
end;

//=============================================================================
//  保存したテキストファイルを TMemo に読み込む
//  読み込み時のエンコード指定なし
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  Memo1.Lines.LoadFromFile(FFilePath);
end;

//=============================================================================
//  保存したテキストファイルを TMemo に読み込む
//  読み込み時のエンコード指定あり
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
begin
  Memo1.Lines.LoadFromFile(FFilePath, TEncoding.Unicode);
end;


次のように、TMemoryStream を保存する時に、意図的に BOM を追加することもできます。


procedure TForm1.Button1Click(Sender: TObject);
var
  LText   : string;
  LStream : TMemoryStream;
  LBOM    : TBytes;
begin
  LText := Memo1.Lines.Text;

  LStream := TMemoryStream.Create;
  try
    LBOM := TEncoding.Unicode.GetPreamble;
    LStream.Write(LBOM[0], Length(LBOM));

    LStream.Write(LText[1], Length(LText) * SizeOf(Char));
    LStream.SaveToFile(FFilePath);
  finally
    FreeAndNil(LStream);
  end;
end;


BOM のバイナリ値は、下図のようになっています。



図11
BOM のバイナリ値
  • 最初の値は Unicode (UTF-16) の BOM
  • 次の値は UtF-8 の BOM

リスト7
procedure TForm1.Button1Click(Sender: TObject);
var
  LBOM  : TBytes;
  LText : string;
  LIdx  : Integer;
begin
  // Unicode (UTF-16) の BOM データ
  LBOM := TEncoding.Unicode.GetPreamble;
  LText := '';
  for LIdx := 0 to Length(LBOM) -1 do begin
    LText := LText + IntToHex(LBOM[LIdx], 2);
  end;
  Memo1.Lines.Add('$' + LText);

  // UTF-8 の BOM データ
  LBOM := TEncoding.UTF8.GetPreamble;
  LText := '';
  for LIdx := 0 to Length(LBOM) -1 do begin
    LText := LText + IntToHex(LBOM[LIdx], 2);
  end;
  Memo1.Lines.Add('$' + LText);
end;


[備考 1]
BOM (byte order mark - バイトオーダマーク) は、正確にはバイトオーダの識別用です。

[備考 2]
Windows のメモ帳で BOM なしの Unicode のテキストを読み込む時は、Unicode を指定して読み込まないと文字化けします。BOM 付きの Unicode のテキストであれば、自動的に判定して正常に表示してくれます。




04_コードページの違いが原因で発生する文字化け


コードページの指定が適切でないと文字化けが発生することを再確認してみます。
下図は、Shift_JIS で保存したテキストを TMemo に読み込んで表示する際、Unicode を指定しために文字化けした結果です。
このように、意図しない文字が表示される状態を一般的に「文字化け」と言っています。

カタカナの「ユ」 は、Shift_JIS では $8386 ですが、Unicode としてメモリストリームやファイルから読み込む時は、バイトオーダが逆順に読み込まれます。その結果、コードポイント $8683 の文字として表示されます。



図12
テストに使用した文字列
図13
一般的に言われる「文字化け」
  • Shift_JIS のテキストを Unicode として読み込んで表示した結果
  • 意図しない文字が表示される

下図は、UTF-8 で保存したテキストを TMemo に読み込んで表示する際、Unicode を指定しために文字化けした結果です。
このサンプルは、文字化けを発生させる極端な例ですが、テキストとして文字列を扱う場合、コードページに注意する必要があります。データベースの文字列項目のデータを処理する場合もコートページの確認が必要なことがあります。文字化けは、文字列の処理過程でも発生する可能性があります。



図14
一般的に言われる「文字化け」
  • UTF-8 のテキストを Unicode として読み込んで表示した結果
  • 意図しない文字が表示される

リスト8
var
  FFilePath : string;

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo1.Lines.Clear;

  // 文字列をセット
  Memo1.Lines.Add('ユニコードな文字なし');
  Memo1.Lines.Add('テキストを保存');

  FFilePath := ExpandFileName('.\000.txt');
end;

//=============================================================================
//  TMemo のテキストを Shift_JIS で保存
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
begin
  Memo1.Lines.SaveToFile(FFilePath);
end;

//=============================================================================
//  保存したテキストファイルを TMemo に読み込む
//  読み込み時に Unicode を指定
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
begin
  Memo1.Lines.LoadFromFile(FFilePath, TEncoding.Unicode);
end;


上のサンプルは、Unicode への変換で文字化けする例でした。以下のサンプルは、UTF-8 への変換で文字化けする例です。
Delphi ではこのサンプルの最後のコードのように、コードページの文字列型の変数に代入するだけでコードページの変換ができてしまいます。そのため、UTF-8 への変換で文字化けさせるのは難しいのですが、強引に文字化けするようにしてみました。
このサンプルの文字列は、UTF-8 では 1 文字に 3 バイト使用するので合計 9 バイト使用します。

Unicode (UTF-16) では最低でも 1 文字で 2 バイト使用します。その結果、Unicode として文字列のデータを読み込む時は、文字化けとして多くの場合、漢字が表示されます。
Shift_JIS や UTF-8 には 1 バイトの文字があります。それらの多くは ASCII 文字としてのバイナリデータです。その結果、文字化けとして半角の英数字が多く表示されます。読み出すバイト値が 0 の場合 は Null 文字ですから、それ以降のバイナリデータは文字として表示しないことになります。前項のサンプルにその例があります。


  // Null 文字以降の文字は表示しない
  ShowMessage('ABC' + #$0 + '日本語');


図15
UTF-8 への変換で文字化けする例

リスト9
//=============================================================================
//  UTF-8 への変換で無理やり文字化けを発生させる
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LStream  : TMemoryStream;
  LUniStr  : string;
  LUtf8Str : UTF8String;
begin
  LUniStr := '日本語';

  LStream := TMemoryStream.Create;
  try
    // メモリストリームに String 型の文字列のデータを書き込む
    // 書き込むバイト数は 6 バイト
    LStream.Write(LUniStr[1], Length(LUniStr) * SizeOf(Char));

    SetLength(LUtf8Str, 9);
    // メモリストリームのデータを UTF-8 型の文字列に読み込む
    LStream.Position := 0;
    LStream.Read(LUtf8Str[1], LStream.Size);
    Memo1.Lines.Add(LUtf8Str);
  finally
    FreeAndNil(LStream);
  end;

  SetLength(LUtf8Str, 0);
  // 通常は以下で十分
  LUtf8Str := LUniStr;
  Memo1.Lines.Add(LUtf8Str);
end;


[備考]
文字化けは、表示したり印刷しないと確認できません。文字化けしてしまう、という質問には、どの様な処理でどこに表示したという記述がない質問があります。その場合、当然ながら、原因の特定は困難になります。