25

考虑这个程序:

{$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,还是它天生就残缺不全?

4

3 回答 3

27

SetConsoleOutputCP()只需通过带有 codepage 的例程设置控制台输出代码页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ЗИІКЛМНОПҀРСТȢѸФХѾЦЧШЩЪЫЬѢѤЮѦѪѨѬѠѺѮѰѲѴ');
于 2014-10-08T12:05:16.113 回答
12

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 目前没有任何其他信息。请使用讨论页面帮助我们记录这个主题!

于 2014-10-08T12:54:20.800 回答
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调用。

还要记住,当在文件而不是控制台上使用时,它也必须工作:

XE2和Delphi 2009之间的unicode文本文件输出不同?

这意味着盲目使用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)必须包含在输出的开头(否则文件将被编码!)。此外,PowerShell 将始终将输出转换为带有前导码的 UTF-16。

MSDN 建议使用GetConsoleMode找出标准句柄是否是控制台句柄,还提到了 BOM:

如果 WriteConsole 与重定向到文件的标准句柄一起使用,它会失败。如果应用程序处理了可以重定向的多语言输出,判断输出句柄是否为控制台句柄(一种方法是调用GetConsoleMode函数并检查是否成功)。如果句柄是控制台句柄,则调用 WriteConsole。如果句柄不是控制台句柄,则输出被重定向,您应该调用 WriteFile 来执行 I/O。确保在 Unicode 纯文本文件前加上字节顺序标记。有关详细信息,请参阅使用字节顺序标记。

于 2014-10-08T12:30:52.683 回答