Подтвердить что ты не робот

Может ли Writeln поддерживать Unicode?

Рассмотрим эту программу:

{$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, или он по своей сути искалечен?

4b9b3361

Ответ 1

Просто установите кодовую страницу выхода консоли через процедуру 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) внутри.


Update:

Вышеупомянутые работы находятся в 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ЗИІКЛМНОПҀРСТȢѸФХѾЦЧШЩЪЫЬѢѤЮѦѪѨѬѠѺѮѰѲѴ');

Ответ 2

Блок 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 и как он будет взаимодействовать с классическим I/O Pascal. Однако, по-видимому, он ведет себя по желанию для вывода на консоль.

Документация AlternateWriteUnicodeStringProc в настоящее время говорит, дождитесь ее,...

В настоящее время Embarcadero Technologies не располагает никакой дополнительной информацией. Пожалуйста, помогите нам документировать эту тему, используя страницу обсуждения!

Ответ 3

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 использует вызов WriteFile.

Также имейте в виду, что при использовании в файле вместо консоли он должен работать:

Выход текстового файла unicode отличается между XE2 и 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 рекомендует с помощью GetConsoleMode, чтобы узнать, является ли стандартный дескриптор консольным дескриптором, также упоминается спецификация:

Ошибка WriteConsole, если он используется со стандартным дескриптором, который перенаправляется в файл. Если приложение обрабатывает многоязычный вывод которые могут быть перенаправлены, определить, является ли выходной дескриптор консольный дескриптор (один метод - вызвать функцию GetConsoleMode и проверьте, удастся ли это выполнить). Если дескриптор представляет собой консольный дескриптор, вызовите WriteConsole. Если дескриптор не является дескриптором консоли, выход перенаправлен, и вы должны позвонить WriteFile для выполнения ввода-вывода. Будь уверен префикс текстового файла Unicode с байтом порядка байтов. Для большего информацию см. в разделе "Использование байтов".