Консоль и русский язык в Windows
May. 22nd, 2014 07:16 pm![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Оригинал взят у
softcreator в Консоль и русский язык в Windows
![[livejournal.com profile]](https://www.dreamwidth.org/img/external/lj-userinfo.gif)
В России три главных вопроса - "Что делать?", "Кто виноват?" и "Почему у меня в консоли крякозябры вылезают?". Ответу на третий вопрос и посвящен этот пост. Итак, почему же? И что делать, чтобы нормально писать русскоязычные консольные программы из Visual Studio?
Сначала немного теории. Думаю, ни для кого не секрет, что Windows NT - система, основанная на Unicode, а для программ, основанных на обычных однобайтовых строках, используется кодировка CP1251. А консоль в целях совместимости поддерживает 8-битовый char и DOS-овскую устаревшую кодировку (CP866). Начнем с юникода, сделаем так:
Получим вполне логичную надпись "Привет, мир!" на экране. На русском, без крякозябров. А все потому, что добрая функция WriteConsoleW перекодирует символы Unicode в 866 кодировку. Теперь попробуем сделать то же самое, но с любимыми всеми однобайтовыми char'ами. Итак:
Вот он, любимый всеми русскими программистами "?ЁштхЄ, ьшЁ!". Чтобы понять, почему такое получилось, проследим путь нашей бедной строки на экран терминала. Начинается этот путь в редакторе Visual Studio, где кодировка однобайтовых символов CP1251. В этой же кодировке строка и будет записана в сегменте данных нашей программы. Строка при этом остается однобайтовой. И после вывода этой 1251-строки в окно с 866-й кодировкой получаем то, что получаем.
Казалось бы, всего-то надо - вызвать SetConsoleOutputCP(1251) и дело с концом. Пробуем - и опять то же самое! Почему? На этот вопрос дает ответ MSDN: "However, if the current font is a raster font, SetConsoleOutputCP does not affect how extended characters are displayed". Или, в переводе на русский, если в консоли у вас растровые шрифты (не TTF), то ничего и не изменится. Не изменится, потому что дефолтный шрифт консоли сделан в расчете на 866 кодировку. Как бы мы не старались, с этим шрифтом будут одни закорючки. Поэтому у нас есть два выбора:
Но наша радость была бы не полной, если бы не было юникодовских функций вывода. WriteConsoleW работает нормально. А вот wprintf и wcout/wcerr вообще ничего не выводят. Опять обращаемся к помощи отладчика и такой-то матери. Видим, что пока не назначена локаль стандартной библиотеки, юникод не принимается. Совсем. Как только wchar_t принимает значение больше 255, нас посылают на EILSEQ (неправильная последовательность multibyte). В общем-то, это логично, ибо для преобразования в multibyte надо бы сначала кодировку указать. И когда она указана, идет вызов WideCharToMultiByte. Указываем с помощью std::locale::global(std::locale(".1251")). Получаем "_аЁў_в, ?Ёа!" - это что-то новенькое. Причем, если даже мы укажем ".866" вместо ".1251", получим то же самое. Вернемся к отладчику. Там нашим глазам открывается следующее: после того, как строка преобразована в однобайтовую, идет ее посимвольное преобразование сначала обратно в юникод, с использованием локали стандартной библиотеки, а потом, с помощью WideCharToMultiByte(GetConsoleCP(), ...) - опять в char. Теперь понятно, что какую бы мы локаль не указали, будет кодировка консоли. Ну что же, добавляем замену кодовой страницы консоли SetConsoleCP(1251). И вот он, родной язык всех трудящихся на консоли, сделанной империалистами из Microsoft. Мы им еще покажем кузькину мать! Кстати, поскольку мы указали SetConsoleCP(1251), нет надобности в преобразовании входных символов - будет именно 1251.
Все это дело я решил оформить в небольшую библиотеку и поделиться с окружающим миром. Пользоваться этой библиотекой проще простого. Нужно кинуть файл ruconsole.h в каталог проекта Visual Studio, а в файле с функцией main написать:
и дальше все как обычно. Справится даже студент-первокурсник. Вместо russian_console можно использовать RussianConsole, russianConsole, Russian_Console - какое именование больше нравится. Все остальное будет работать автоматически. На Windows 9x использовать это не стоит - не откомпилируется, а вот начиная с Windows 2000 - запросто. Я проверял только на Visual C++ Express, но думаю, что на любой студии будет работать одинаково. В принципе, должно работать и на других компиляторах, но тут могут быть сложности из-за #pragma comment(lib, "xxxx"). Пришлось подключить psapi.dll и dbghelp.dll, которые, впрочем, идут в комплекте с Windows 2000 и выше. Работает и в C, и в C++.
P.S. Немного посыплю голову пеплом - не осилил низкоуровневые функции, такие, как WriteConsoleOutput, WriteConsoleOutputCharacter, ReadConsoleOutputCharacter и прочие. Тут империалисты написали что-то не совсем понятное, а с отладчиком сидеть лень, после того, как cout/wcout заработали.
Сначала немного теории. Думаю, ни для кого не секрет, что Windows NT - система, основанная на Unicode, а для программ, основанных на обычных однобайтовых строках, используется кодировка CP1251. А консоль в целях совместимости поддерживает 8-битовый char и DOS-овскую устаревшую кодировку (CP866). Начнем с юникода, сделаем так:
int _tmain(int argc, _TCHAR* argv[])
{
HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD dwCharsWritten;
std::wstring helloWorld(L"Привет, мир!\r\n");
WriteConsoleW(hStdOut, helloWorld.c_str(), helloWorld.length(), &dwCharsWritten, NULL);
return 0;
}
Получим вполне логичную надпись "Привет, мир!" на экране. На русском, без крякозябров. А все потому, что добрая функция WriteConsoleW перекодирует символы Unicode в 866 кодировку. Теперь попробуем сделать то же самое, но с любимыми всеми однобайтовыми char'ами. Итак:
int _tmain(int argc, _TCHAR* argv[])
{
HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD dwCharsWritten;
std::string helloWorld("Привет, мир!\r\n");
WriteConsoleA(hStdOut, helloWorld.c_str(), helloWorld.length(), &dwCharsWritten, NULL);
return 0;
}
Вот он, любимый всеми русскими программистами "?ЁштхЄ, ьшЁ!". Чтобы понять, почему такое получилось, проследим путь нашей бедной строки на экран терминала. Начинается этот путь в редакторе Visual Studio, где кодировка однобайтовых символов CP1251. В этой же кодировке строка и будет записана в сегменте данных нашей программы. Строка при этом остается однобайтовой. И после вывода этой 1251-строки в окно с 866-й кодировкой получаем то, что получаем.
Казалось бы, всего-то надо - вызвать SetConsoleOutputCP(1251) и дело с концом. Пробуем - и опять то же самое! Почему? На этот вопрос дает ответ MSDN: "However, if the current font is a raster font, SetConsoleOutputCP does not affect how extended characters are displayed". Или, в переводе на русский, если в консоли у вас растровые шрифты (не TTF), то ничего и не изменится. Не изменится, потому что дефолтный шрифт консоли сделан в расчете на 866 кодировку. Как бы мы не старались, с этим шрифтом будут одни закорючки. Поэтому у нас есть два выбора:
- Использовать SetConsoleOutputCP(1251) и шрифт Lucida Console для окна консоли.
- Перекодировать выводимые символы в 866 кодировку. Для этого есть CharToOem. Что-то типа:
cout << ru("Строка") << endl или printf(ru("Строка\n")), где функция ru вызывает CharToOem.
Но наша радость была бы не полной, если бы не было юникодовских функций вывода. WriteConsoleW работает нормально. А вот wprintf и wcout/wcerr вообще ничего не выводят. Опять обращаемся к помощи отладчика и такой-то матери. Видим, что пока не назначена локаль стандартной библиотеки, юникод не принимается. Совсем. Как только wchar_t принимает значение больше 255, нас посылают на EILSEQ (неправильная последовательность multibyte). В общем-то, это логично, ибо для преобразования в multibyte надо бы сначала кодировку указать. И когда она указана, идет вызов WideCharToMultiByte. Указываем с помощью std::locale::global(std::locale(".1251")). Получаем "_аЁў_в, ?Ёа!" - это что-то новенькое. Причем, если даже мы укажем ".866" вместо ".1251", получим то же самое. Вернемся к отладчику. Там нашим глазам открывается следующее: после того, как строка преобразована в однобайтовую, идет ее посимвольное преобразование сначала обратно в юникод, с использованием локали стандартной библиотеки, а потом, с помощью WideCharToMultiByte(GetConsoleCP(), ...) - опять в char. Теперь понятно, что какую бы мы локаль не указали, будет кодировка консоли. Ну что же, добавляем замену кодовой страницы консоли SetConsoleCP(1251). И вот он, родной язык всех трудящихся на консоли, сделанной империалистами из Microsoft. Мы им еще покажем кузькину мать! Кстати, поскольку мы указали SetConsoleCP(1251), нет надобности в преобразовании входных символов - будет именно 1251.
Все это дело я решил оформить в небольшую библиотеку и поделиться с окружающим миром. Пользоваться этой библиотекой проще простого. Нужно кинуть файл ruconsole.h в каталог проекта Visual Studio, а в файле с функцией main написать:
#include <stdio.h>
... тут идут другие #include
#include "ruconsole.h"
...
int main(int argc, char** argv)
{
russian_console();
и дальше все как обычно. Справится даже студент-первокурсник. Вместо russian_console можно использовать RussianConsole, russianConsole, Russian_Console - какое именование больше нравится. Все остальное будет работать автоматически. На Windows 9x использовать это не стоит - не откомпилируется, а вот начиная с Windows 2000 - запросто. Я проверял только на Visual C++ Express, но думаю, что на любой студии будет работать одинаково. В принципе, должно работать и на других компиляторах, но тут могут быть сложности из-за #pragma comment(lib, "xxxx"). Пришлось подключить psapi.dll и dbghelp.dll, которые, впрочем, идут в комплекте с Windows 2000 и выше. Работает и в C, и в C++.
P.S. Немного посыплю голову пеплом - не осилил низкоуровневые функции, такие, как WriteConsoleOutput, WriteConsoleOutputCharacter, ReadConsoleOutputCharacter и прочие. Тут империалисты написали что-то не совсем понятное, а с отладчиком сидеть лень, после того, как cout/wcout заработали.