Writeln能否支持Unicode?

时间:2014-10-08 10:52:46

标签: windows delphi delphi-xe7

考虑这个程序:

{$APPTYPE CONSOLE}

begin
  Writeln('АБВГДЕЖЅZЗИІКЛМНОПҀРСТȢѸФХѾЦЧШЩЪЫЬѢѤЮѦѪѨѬѠѺѮѰѲѴ');
end.

我的控制台上使用Consolas字体的输出是:

????????Z??????????????????????????????????????

Windows控制台非常能够支持Unicode,如本程序所示:

{$APPTYPE CONSOLE}

uses
  Winapi.Windows;

const
  Text = 'АБВГДЕЖЅZЗИІКЛМНОПҀРСТȢѸФХѾЦЧШЩЪЫЬѢѤЮѦѪѨѬѠѺѮѰѲѴ';

var
  NumWritten: DWORD;

begin
  WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), PChar(Text), Length(Text), NumWritten, nil);
end.

输出为:

АБВГДЕЖЅZЗИІКЛМНОПҀРСТȢѸФХѾЦЧШЩЪЫЬѢѤЮѦѪѨѬѠѺѮѰѲѴ

是否可以说服Writeln尊重Unicode,或者它本身是否残缺?

3 个答案:

答案 0 :(得分:27)

只需使用代码页SetConsoleOutputCP()通过cp_UTF8例程设置控制台输出代码页。

program Project1;

{$APPTYPE CONSOLE}

uses
  System.SysUtils,Windows;
Const
  Text =  'АБВГДЕЖЅZЗИІКЛМНОПҀРСТȢѸФХѾЦЧШЩЪЫЬѢѤЮѦѪѨѬѠѺѮѰѲѴ';
VAR
  NumWritten: DWORD;
begin
  ReadLn;  // Make sure Consolas font is selected
  try
    WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), PChar(Text), Length(Text), NumWritten, nil);    
    SetConsoleOutputCP(CP_UTF8);
    WriteLn;
    WriteLn('АБВГДЕЖЅZЗИІКЛМНОПҀРСТȢѸФХѾЦЧШЩЪЫЬѢѤЮѦѪѨѬѠѺѮѰѲѴ');
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  ReadLn;
end.

输出:

АБВГДЕЖЅZЗИІКЛМНОПҀРСТȢѸФХѾЦЧШЩЪЫЬѢѤЮѦѪѨѬѠѺѮѰѲѴ
АБВГДЕЖЅZЗИІКЛМНОПҀРСТȢѸФХѾЦЧШЩЪЫЬѢѤЮѦѪѨѬѠѺѮѰѲѴ

WriteLn()在内部将Unicode UTF16字符串转换为选定的输出代码页(cp_UTF8)。


更新

上述内容适用于Delphi-XE2及以上版本。 在Delphi-XE中,您需要显式转换为UTF-8才能使其正常工作。

WriteLn(UTF8String('АБВГДЕЖЅZЗИІКЛМНОПҀРСТȢѸФХѾЦЧШЩЪЫЬѢѤЮѦѪѨѬѠѺѮѰѲѴ'));

<强>附录:

如果在调用SetConsoleOutputCP(cp_UTF8)之前在另一个代码页中完成了对控制台的输出, 操作系统无法正确输出utf-8中的文本。 这可以通过关闭/重新打开stdout处理程序来解决。

另一种选择是为utf-8声明一个新的文本输出处理程序。

var
  toutUTF8: TextFile;
...
SetConsoleOutputCP(CP_UTF8);
AssignFile(toutUTF8,'',cp_UTF8);  // Works in XE2 and above
Rewrite(toutUTF8);
WriteLn(toutUTF8,'АБВГДЕЖЅZЗИІКЛМНОПҀРСТȢѸФХѾЦЧШЩЪЫЬѢѤЮѦѪѨѬѠѺѮѰѲѴ');

答案 1 :(得分:11)

System单元声明了一个名为AlternateWriteUnicodeStringProc的变量,允许自定义Writeln执行输出的方式。这个计划:

{$APPTYPE CONSOLE}

uses
  Winapi.Windows;

function MyAlternateWriteUnicodeStringProc(var t: TTextRec; s: UnicodeString): Pointer;
var
  NumberOfCharsWritten, NumOfBytesWritten: DWORD;
begin
  Result := @t;
  if t.Handle = GetStdHandle(STD_OUTPUT_HANDLE) then
    WriteConsole(t.Handle, Pointer(s), Length(s), NumberOfCharsWritten, nil)
  else
    WriteFile(t.Handle, Pointer(s)^, Length(s)*SizeOf(WideChar), NumOfBytesWritten, nil);
end;

var
  UserFile: Text;

begin
  AlternateWriteUnicodeStringProc := MyAlternateWriteUnicodeStringProc;
  Writeln('АБВГДЕЖЅZЗИІКЛМНОПҀРСТȢѸФХѾЦЧШЩЪЫЬѢѤЮѦѪѨѬѠѺѮѰѲѴ');
  Readln;
end.

生成此输出:

АБВГДЕЖЅZЗИІКЛМНОПҀРСТȢѸФХѾЦЧШЩЪЫЬѢѤЮѦѪѨѬѠѺѮѰѲѴ

我对我如何实施MyAlternateWriteUnicodeStringProc以及它如何与经典的Pascal I / O进行交互持怀疑态度。但是,它似乎表现为输出到控制台所需的行为。

AlternateWriteUnicodeStringProc的文档目前正在说,等待它,......

  

Embarcadero Technologies目前没有任何其他信息。请使用“讨论”页面帮助我们记录此主题!

答案 2 :(得分:5)

WriteConsoleW似乎是一个非常神奇的功能。

procedure WriteLnToConsoleUsingWriteFile(CP: Cardinal; AEncoding: TEncoding; const S: string);
var
  Buffer: TBytes;
  NumWritten: Cardinal;
begin
  Buffer := AEncoding.GetBytes(S);
  // This is a side effect and should be avoided ...
  SetConsoleOutputCP(CP);
  WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), Buffer[0], Length(Buffer), NumWritten, nil);
  WriteLn;
end;

procedure WriteLnToConsoleUsingWriteConsole(const S: string);
var
  NumWritten: Cardinal;
begin
  WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), PChar(S), Length(S), NumWritten, nil);
  WriteLn;
end;

const
  Text = 'АБВГДЕЖЅZЗИІКЛМНОПҀРСТȢѸФХѾЦЧШЩЪЫЬѢѤЮѦѪѨѬѠѺѮѰѲѴ';
begin
  ReadLn; // Make sure Consolas font is selected
  // Works, but changing the console CP is neccessary
  WriteLnToConsoleUsingWriteFile(CP_UTF8, TEncoding.UTF8, Text);
  // Doesn't work
  WriteLnToConsoleUsingWriteFile(1200, TEncoding.Unicode, Text);
  // This does and doesn't need the CP anymore
  WriteLnToConsoleUsingWriteConsole(Text);
  ReadLn;
end.

总结如下:

WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), ...)支持UTF-16。

WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), ...)不支持UTF-16。

我的猜测是,为了支持不同的ANSI编码,经典的Pascal I / O使用WriteFile调用。

另请注意,在文件而非控制台上使用时,它也必须正常工作:

unicode text file output differs between XE2 and Delphi 2009?

这意味着盲目使用WriteConsole会中断输出重定向。如果您使用WriteConsole,则应该回到WriteFile,如下所示:

var
  NumWritten: Cardinal;
  Bytes: TBytes;
begin
  if not WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), PChar(S), Length(S),
    NumWritten, nil) then
  begin
    Bytes := TEncoding.UTF8.GetBytes(S);
    WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), Bytes[0], Length(Bytes),
      NumWritten, nil);
  end;
  WriteLn;
end;

请注意,使用任何编码的输出重定向在cmd.exe中正常工作。它只是将输出流写入文件中。

但是,PowerShell要求在输出开始时包含ANSI输出或正确的前导码(/ BOM)(或者文件将是malencoded!)。此外,PowerShell将始终使用前导码将输出转换为UTF-16。

MSDN recommends使用GetConsoleMode查看标准句柄是否为控制台句柄,还提到了BOM:

  

如果WriteConsole与标准句柄一起使用,则会失败   重定向到文件。如果应用程序处理多语言输出   可以重定向,确定输出句柄是否为a   控制台句柄(一种方法是调用GetConsoleMode函数和   检查是否成功)。如果句柄是控制台句柄,请调用   WriteConsole。如果句柄不是控制台句柄,则输出为   重定向,你应该调用WriteFile来执行I / O.务必   使用字节顺序标记为Unicode纯文本文件添加前缀。更多   信息,请参阅使用字节订单标记。