如何在Windows控制台中输出Unicode字符串

14

已经有一些问题与此问题相关。我认为我的问题略有不同,因为我没有实际的问题,我只是出于学术兴趣而问。我知道 Windows 的 UTF-16 实现有时与 Unicode 标准相矛盾(例如排序),或者更接近旧的 UCS-2 而非 UTF-16,但出于简单起见,我将在这里保留“UTF-16”术语。

背景:在 Windows 中,一切都是 UTF-16。无论您正在处理内核、图形子系统、文件系统还是其他任何内容,您都需要传递 UTF-16 字符串。没有 Unix 意义上的区域设置或字符集。为了与 Windows 的中古版本兼容,有一个叫做“代码页”的东西,它已经过时,但仍然得到支持。据我所知,只有一种正确且不过时的函数可以将字符串写入控制台,即 WriteConsoleW,它接受一个 UTF-16 字符串作为参数。对于输入流也有类似的讨论,但我将忽略它。

然而,我认为这代表了 Windows API 的设计缺陷:有一个通用函数可以用于写入所有流对象(文件、管道、控制台等),名为 WriteFile,但这个函数是面向字节的,并不接受 UTF-16 字符串。文档建议将 WriteConsoleW 用于控制台输出,这是面向文本的,而将 WriteFile 用于其他所有内容,这是面向字节的。由于控制台流和文件对象都由内核对象句柄表示,并且控制台流可以被重定向,因此您必须针对每个写入标准输出流的操作调用一个函数来检查句柄代表控制台流还是文件,破坏了多态性。另一方面,我确实认为 Windows 在文本字符串和原始字节之间的分离(这反映在许多其他系统中,例如 Java 或 Python)在概念上优于 Unix 的 char* 方法,后者忽略编码并不区分字符串和字节数组。

所以我的问题是:在这种情况下该怎么办?为什么即使在微软自己的库中也没有解决这个问题?.NET Framework和C、C++库似乎都遵循过时的代码页模型。你将如何设计Windows API或应用程序框架来规避这个问题?
我认为普遍存在的问题(这不容易解决)是所有库都假定所有流都是面向字节的,并在此基础上实现面向文本的流。然而,我们看到Windows在操作系统级别上确实有特殊的面向文本的流,但库无法处理这个问题。因此,在任何情况下,我们必须对所有标准库进行重大更改。一种快速而简单的方法是将控制台视为只接受一种编码的特殊面向字节的流。这仍然需要绕过C和C++标准库,因为它们没有实现WriteFile/WriteConsoleW切换。这正确吗?

3
抱歉,这个“问题”听起来像是一个伪装成博客文章的帖子;-) - Philipp
这可能与我的问题有关:http://superuser.com/questions/157225/even-on-windows-7-can-you-do-a-dir-and-be-able-to-see-filenames-that-has-unico - nonopolarity
4个回答

5
我/我们在大多数(跨平台)应用程序/项目中使用的一般策略是:我们只使用UTF-8(我的意思是真正的标准),并将其应用于所有内容。我们使用std::string作为容器,将所有内容解释为UTF8。我们也以这种方式处理所有文件IO,即我们期望UTF8并保存UTF8。在某些情况下,当我们从某处获取字符串并且知道它不是UTF8时,我们会将其转换为UTF8。
我们经常遇到WinUTF16的最常见情况是文件名。因此,对于每个文件名处理,我们都将UTF8字符串转换为WinUTF16。如果我们在目录中搜索文件,则也反过来。
控制台在我们的Windows版本中并没有真正被使用(在Windows版本中,所有控制台输出都被包装成一个文件)。由于我们在任何地方都使用UTF8,因此我们的控制台输出也是UTF8,这对于大多数现代系统来说是可以接受的。而且Windows控制台日志文件的内容是UTF8,大多数Windows上的文本编辑器都可以轻松读取。
如果我们更多地使用WinConsole,并且我们非常关心所有特殊字符的正确显示,我们可能会编写一些自动管道处理程序,将其安装在 fileno = 0 和实际 stdout 之间,这将使用您建议的 WriteConsoleW (如果没有更简单的方法)。
如果您想知道如何实现这样的自动管道处理程序:我们已经为所有类似POSIX的系统实现了这样的功能。该代码在Windows上可能无法正常工作,但我认为可以进行移植。我们当前的管道处理程序类似于 tee 所做的事情。即,如果您执行cout << "Hello" << endl,它将同时打印在 stdout 和某个日志文件中。如果您对如何完成此操作感兴趣,请查看代码

4

几个要点:

  1. Windows的 "WriteConsoleW" 和 printf 之间一个重要的区别在于 WriteConsoleW 把控制台视为图形界面而不是文本流。例如,如果你使用它并使用管道,则无法捕获输出。
  2. 我从未说过代码页已经过时。也许Windows开发人员希望它们过时,但它们永远不会过时。除了Windows API外,全世界都使用面向字节的流来表示数据:XML、HTML、HTTP、Unix等等都使用编码方式,而最流行、最强大的编码方式是UTF-8。因此,您可以在内部使用宽字符串,但在外部世界中,您需要其他东西。

    即使您打印 wcout << L"Hello World" << endl ,它也会在幕后转换为面向字节的流,在大多数系统上(但不包括Windows),转换为UTF-8。

  3. 我个人认为,微软公司在每个地方将其API更改为宽字符集,而不是在所有地方支持UTF-8时犯了错误。当然,你可能会对此进行争论。但实际上你必须分离文本和面向字节的流,并在它们之间进行转换。


  1. 微软建议在使用WriteConsole之前检查标准输出流是否连接到控制台或其他地方。这很繁琐,但似乎是唯一可能的可移植选项。
  2. Codepages和编码不同。我的意思是Windows控制台代码页。由于Windows控制台是文本导向的并使用UTF-16,因此代码页已经过时——使用代码页的每个字符串都会立即转换为UTF-16。wostream问题非常令人遗憾,但这是C ++标准规定的。
  3. 我认为使用UTF-16的决定并不不幸...
- Philipp
虽然Windows API提供了一些Unicode支持,但API的设计很差。例如,你可以考虑像GetStdHandle(STD_UTF16LE_OUTPUT_HANDLE)这样的东西,它将返回一个面向字节的流句柄,该句柄期望UTF-16-LE编码的字符串。然后你可以在任何地方使用WriteFile。另一方面,我认为C和C++没有真正的文本流这个问题更为重要。 - Philipp
我认为“除了Windows API,所有世界都使用面向字节的流来表示数据”有点夸张。Java、C#和JavaScript也将它们的字符和字符串处理作为面向字的流UTF-16来完成。 - hippietrail
@aipettrail (a) 最终当你将数据写入文件时,几乎从不使用UTF-16。(b) 我所说的是操作系统级别的API - 除了Windows之外的任何地方都是面向字节的 - 并且操作系统提供您的控制台抽象。(c) 在Java、JavaScript、C#中,最终提供了一些字符编码来将文本转换为输出流。 - Artyom
1
当微软将他们的API分成代码页(A)和宽字符(W)变体时,UTF-8还没有被发明。很难责怪他们没有在根本不存在的东西上标准化。不过我认为自那时以来他们本应该做更多的工作来使UTF-8正常工作。 - Mark Ransom

3
回答你的第一个问题,您可以使用_setmode将Unicode字符串输出到Windows控制台。关于此的具体细节可以在Michael Kaplan's blog中找到。默认情况下,控制台不是Unicode(UCS-2 / UTF-16)。它以Ansi(locale/code page)方式工作,必须特别配置才能使用Unicode。
此外,您需要更改控制台字体,因为默认字体仅支持Ansi字符。这里有一些小例外,例如零扩展ASCII字符,但打印实际的Unicode字符需要使用_setmode。
在Windows中,一切都是UTF-16。无论您处理内核、图形子系统、文件系统还是其他任何东西,您都会传递UTF-16字符串。没有Unix意义上的语言环境或字符集。
这并不完全正确。虽然Windows的底层核心确实使用Unicode,但有大量互操作性,让Windows与各种软件进行交互。
考虑记事本(是的,记事本远非核心组件,但它可以传达我的观点)。记事本能够读取包含 Ansi(当前代码页)、Unicode 或 UTF-8 的文件。你可能认为记事本是一个 Unicode 应用程序,但这并不完全准确。
更好的例子是驱动程序。驱动程序 可以用 Unicode 或 Ansi 编写,这实际上取决于接口的性质。为了进一步说明这一点,Microsoft 提供了 StrSafe 库,该库专门针对 内核模式驱动程序 编写,并包括 Unicode 和 Ansi 两个版本。虽然驱动程序是 Ansi 或 Unicode,但 Windows 内核必须正确地与它们交互,无论其采取何种形式。
离开Windows核心越远,互操作性就越重要。这包括代码页和区域设置。您必须记住,并非所有软件都考虑了Unicode。Visual C++ 2010仍具有使用Ansi、多字节或Unicode构建的能力。这包括使用代码页区域设置,它们是C/C++标准的一部分。

然而,我认为这代表了Windows API中的设计缺陷

以下两篇文章对此进行了很好的讨论。

那么我的问题是:在这种情况下该怎么办?为什么即使在微软自己的库中也没有解决这个问题?.NET框架和C、C++库似乎都遵循过时的代码页模型。您会如何设计Windows API或应用程序框架来规避此问题?

在这一点上,我认为你是在事后诸葛亮地看待Windows。Unicode并不是第一个出现的,ASCII才是。在ASCII之后,出现了代码页。在代码页之后,出现了双字节字符集。在双字节字符集之后,出现了多字节字符集(最终发展成UTF-8)。在UTF-8之后,出现了Unicode(UTF-16/UCS-2)。

每一项技术都在这些年被纳入到Windows操作系统中,并且每一个都是在上一个基础上构建,但又不会相互矛盾。每一项软件都是针对它们之一编写的。尽管有时看起来可能并非如此,但Microsoft会投入大量精力来避免破坏它没有编写的软件。即使现在,您也可以编写新的软件来利用任何这些技术,并且它们将可以正常工作。
这里的真正答案是“兼容性”。Microsoft仍在使用这些技术,许多其他公司也是如此。有无数数量的程序、组件和库没有更新(或永远不会更新)以使用Unicode。即使出现了新的技术-例如.NET-旧的技术也必须继续存在。至少要保持互操作性。
例如,假设您有一个DLL需要从.NET与之交互,但是这个DLL使用了Ansi(单字节代码页本地化)编写。更糟糕的是,您没有DLL的源代码。这里唯一的答案是使用那些过时的功能。

0

我的正确工作方式如下:

  • 内部使用UTF-16和wchar_t,这与文件名和Windows API非常兼容。
  • 将代码页设置为65001,即UTF-8。这可以确保在读取纯文本文件时,Windows会检查它们是否为UTF-16和BOM(“Windows标准”),如果没有BOM,则将文本视为UTF-8(“世界标准”)并将其转换为UTF-16供您使用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接