slava68: (Default)
[personal profile] slava68
Оригинал взят у [livejournal.com profile] softcreator в Консоль и русский язык в Windows
В России три главных вопроса - "Что делать?", "Кто виноват?" и "Почему у меня в консоли крякозябры вылезают?".  Ответу на третий вопрос и посвящен этот пост. Итак, почему же? И что делать, чтобы нормально писать русскоязычные консольные программы из Visual Studio?

Сначала немного теории. Думаю, ни для кого не секрет, что 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 кодировку. Как бы мы не старались, с этим шрифтом будут одни закорючки. Поэтому у нас есть два выбора:
  1. Использовать SetConsoleOutputCP(1251) и шрифт Lucida Console для окна консоли.

  2. Перекодировать выводимые символы в 866 кодировку. Для этого есть CharToOem. Что-то типа:
    cout << ru("Строка") << endl или printf(ru("Строка\n")), где функция ru вызывает CharToOem.
Первый способ очень простой и понятный, а вот второй способ надо бы сделать более изящным - чтобы вызовы функций конвертации не мешались под ногами. Изящество затруднено тем, что вывести символы можно через printf, cout/cerr, WriteConsole и WriteFile. С помощью исходников CRT, отладчика и какой-то матери удалось понять, что printf и cout/cerr внутри вызывают WriteFile. Поэтому задача 1251-изации консоли сводится к тому, чтобы перехватить вызовы WriteXXX и выполнить CharToOemBuff. Открываем книжку доброго дяди Рихтера и спустя час отладки (ибо нельзя забывать про сокращенные логические вычисления) получаем наш "Привет, мир!" всеми перечисленными функциями. Ура, товарищи!

Но наша радость была бы не полной, если бы не было юникодовских функций вывода. 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. Немного посыплю голову пеплом - не осилил низкоуровневые функции, такие, как WriteConsoleOutputWriteConsoleOutputCharacter, ReadConsoleOutputCharacter и прочие. Тут империалисты написали что-то не совсем понятное, а с отладчиком сидеть лень, после того, как cout/wcout заработали.

This account has disabled anonymous posting.
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

Profile

slava68: (Default)
slava68

May 2025

S M T W T F S
    123
45678910
11121314151617
18192021222324
25262728293031

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Jun. 17th, 2025 09:41 pm
Powered by Dreamwidth Studios