11

当我尝试下面的代码时,与 D2009 相比,XE2 中的输出似乎有所不同。

procedure TForm1.Button1Click(Sender: TObject);
var Outfile:textfile;
    myByte: Byte;

begin
  assignfile(Outfile,'test_chinese.txt');
  Rewrite(Outfile);

  for myByte in TEncoding.UTF8.GetPreamble do write(Outfile, AnsiChar(myByte));
  //This is the UTF-8 BOM

  Writeln(Outfile,utf8string('总结'));
  Writeln(Outfile,'°C');
  Closefile(Outfile);
end;

在 Windows 8 PC 上使用 XE2 编译会出现写字板

?? C

txt十六进制代码:EF BB BF 3F 3F 0D 0A B0 43 0D 0A

在 Windows XP PC 上使用 D2009 编译会出现写字板

约°C

txt十六进制代码:EF BB BF E6 80 BB E7 BB 93 0D 0A B0 43 0D 0A

我的问题是为什么它会有所不同,如何使用旧的文本文件 I/O 将中文字符保存到文本文件中?

谢谢!

4

3 回答 3

19

从 XE2 开始,AssignFile()有一个可选CodePage参数设置输出文件的代码页:

function AssignFile(var F: File; FileName: String; [CodePage: Word]): Integer; overload;

Write()并且Writeln()两者都有支持UnicodeStringWideChar输入的重载。

因此,您可以创建一个将其代码页设置为 的文件,CP_UTF8然后Write/ln()在将 Unicode 字符串写入文件时自动将其转换为 UTF-8。

缺点是您将无法再使用AnsiChar值写入 UTF-8 BOM,因为单个字节将被转换为 UTF-8,因此无法正确写入。您可以通过将 BOM 编写为单个 Unicode 字符(它实际上是 - U+FEFF)而不是单个字节来解决此问题。

这适用于 XE2:

procedure TForm1.Button1Click(Sender: TObject);
var
  Outfile: TextFile;
begin
  AssignFile(Outfile, 'test_chinese.txt', CP_UTF8);
  Rewrite(Outfile);

  //This is the UTF-8 BOM
  Write(Outfile, #$FEFF);

  Writeln(Outfile, '总结');
  Writeln(Outfile, '°C');
  CloseFile(Outfile);
end;

话虽如此,如果您想要 D2009 和 XE2 之间更兼容和更可靠的东西,请TStreamWriter改用:

procedure TForm1.Button1Click(Sender: TObject);
var
  Outfile: TStreamWriter;
begin
  Outfile := TStreamWriter.Create('test_chinese.txt', False, TEncoding.UTF8);
  try
    Outfile.WriteLine('总结');
    Outfile.WriteLine('°C');
  finally
    Outfile.Free;
  end;
end;

或者手动执行文件 I/O:

procedure TForm1.Button1Click(Sender: TObject);
var
  Outfile: TFileStream;
  BOM: TBytes;

  procedure WriteBytes(const B: TBytes);
  begin
    if B <> '' then Outfile.WriteBuffer(B[0], Length(B));
  end;

  procedure WriteStr(const S: UTF8String);
  begin
    if S <> '' then Outfile.WriteBuffer(S[1], Length(S));
  end;

  procedure WriteLine(const S: UTF8String);
  begin
    WriteStr(S);
    WriteStr(sLineBreak);
  end;

begin
  Outfile := TFileStream.Create('test_chinese.txt', fmCreate);
  try
    WriteBytes(TEncoding.UTF8.GetPreamble);
    WriteLine('总结');
    WriteLine('°C');
  finally
    Outfile.Free;
  end;
end;
于 2013-01-09T18:21:37.510 回答
6

你真的不应该再使用旧的文本 I/O。

无论如何,您可以使用 TEncoding 来获取 UTF-8 TBytes,如下所示:

procedure TForm1.Button1Click(Sender: TObject);
var Outfile:textfile;
    Bytes: TBytes;
    myByte: Byte;
begin
  assignfile(Outfile,'test_chinese.txt');
  Rewrite(Outfile);

  for myByte in TEncoding.UTF8.GetPreamble do write(Outfile, AnsiChar(myByte));
  //This is the UTF-8 BOM

  Bytes := TEncoding.UTF8.GetBytes('总结');
  for myByte in Bytes do begin
    Write(Outfile, AnsiChar(myByte));
  end;

  Writeln(Outfile,'°C');
  Closefile(Outfile);
end;

我不确定是否有更简单的方法将 TBytes 写入文本文件,也许其他人有更好的主意。

编辑:

对于纯二进制文件(File而不是TextFile类型),可以使用BlockWrite.

于 2013-01-09T10:50:07.423 回答
5

有几个迹象可以告诉您在处理 Unicode 时什么时候出错了。在您的情况下,您?在生成的输出文件中看到“”:当您尝试将某些内容从 Unicode 转换为代码页并且目标代码页不能代表请求的字符时,您会得到问号。

查看十六进制转储,很明显(计算行终止符)问号是将两个汉字保存到文件中的结果。这两个字符被转换为正好两个问号。这告诉您Writeln()决定为您提供帮助并将文本从 UTF8(Unicode 表示)转换为您的本地代码页。Delphi 团队可能决定这样做,因为旧的 I/O 例程不应该与 UNICODE 兼容。由于您正在使用旧的 I/O 例程编写 UTF8 字符串,因此他们通过将其转换为您的代码页来帮助您。您可能不欢迎伸出援助之手,但这并不意味着这样做是错误的:它是无证领域。

既然您现在知道为什么会发生这种情况,您就知道该怎么做才能阻止它。让WriteLn()您知道您正在发送不需要转换的内容。您会发现这并不是特别容易,因为 Delphi XE2 显然“帮助您”无论您做什么。例如,像这样的东西不只是改变字符串类型,它转换为 AnsiString,通过代码页转换例程得到问号:

AnsiString(UTF8String('Whatever Unicode'));

因此,如果您需要单线解决方案,您可以尝试转换例程,如下所示:

function FakeConvert(const InStr: UTF8String): AnsiString;
var N: Integer;
begin
  N := Length(InStr);
  SetLength(Result, N);
  Move(InStr[1], Result[1], N);
end;

然后,您将能够:

Writeln(Outfile,FakeConvert('总结'));

它会做你所期望的(我在发布之前确实尝试过!)

当然,这个问题的唯一正确答案是,因为您一直升级到 Delphi XE2:

停止使用已弃用的 I/O 例程,转移到基于 TStream

于 2013-01-09T12:58:13.477 回答