Writeln能否支持Unicode?

Dav*_*nan 25 windows delphi delphi-xe7

考虑这个程序:

{$APPTYPE CONSOLE}

begin
  Writeln('????????Z??????????????????????????????????????');
end.
Run Code Online (Sandbox Code Playgroud)

我的控制台上使用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.
Run Code Online (Sandbox Code Playgroud)

输出为:

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

可以Writeln说服尊重Unicode,还是它固有的残缺?

LU *_* RD 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.
Run Code Online (Sandbox Code Playgroud)

输出:

????????Z??????????????????????????????????????
????????Z??????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)

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


更新:

以上工作在Delphi-XE2及以上版本.在Delphi-XE中,您需要显式转换为UTF-8才能使其正常工作.

WriteLn(UTF8String('????????Z??????????????????????????????????????'));
Run Code Online (Sandbox Code Playgroud)

附录:

如果在调用之前在另一个代码页中完成了对控制台的输出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??????????????????????????????????????');
Run Code Online (Sandbox Code Playgroud)


Dav*_*nan 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.
Run Code Online (Sandbox Code Playgroud)

产生这个输出:

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

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

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

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


Jen*_*off 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.
Run Code Online (Sandbox Code Playgroud)

总结如下:

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

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

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

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

unicode文本文件输出在XE2和Delphi 2009之间有所不同?

这意味着盲目地使用WriteConsolebreak输出重定向.如果你使用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;
Run Code Online (Sandbox Code Playgroud)

请注意,任何编码的输出重定向都可正常工作cmd.exe.它只是将输出流写入文件不变.

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

MSDN建议使用GetConsoleMode以查明标准句柄是否为控制台句柄,还提到了BOM:

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