Ввод и вывод кириллицы в консольном приложении Qt6
Как известно, в шестой версии фреймворка Qt однобайтовым кодировкам объявлена война, а простой способ кириллизации консоли с помощью метода setCodec
не работает, поскольку сам этот метод упразднён.
Для компилятора MSVC и только для Windows, наверное, подошло бы решение с SetConsoleCP/SetConsoleOutputCP
, но как быть тем, кто хочет работать с MinGW?
Вот несколько набросков, которые удалось получить, проверялись они в актуальной сборке Windows 10 и Qt 6.2.0 с MinGW 8.1.0 64 bit из-под оболочки Qt Creator.
Везде тип проекта - "Консольное приложение Qt", кодировка редактора Utf-8. Вообще же, проблема состоит в том, что при работе с консолью в современных версиях Windows требуется учитывать взаимодействие сразу нескольких кодировок:
- кодировка файла исходника (по умолчанию это это UTF-8);
- кодировка локали (по умолчанию это "default" для локали Си);
- кодировка в окне консоли (в русской Windows это по-прежнему "русская DOS" или OEM-866);
- внутренняя кодировка самой Windows (теперь это UTF-16).
Только приведя их все в согласие, можно добиться успеха.
Вот прекрасные соображения по теме от ув. Dr.Offset:
Во-первых, в mingw не используется актуальный msvc runtime по лицензионным причинам, а используется его legacy версия, которая из соображений совместимости всегда присутствует во всех версиях Windows. Актуальные же версии поставляются только в составе VC++ redist (ну или ставятся в составе Visual Studio).
Как следствие, setmode нормально работает только в Visual Studio.
Во-вторых использование UTF-8 в Windows - идея спорная, т.к. внутри все равно будет UTF-16 и перекодировка.
Получается, что способ с минимальными приседаниями заключается в двух ключевых моментах:
1) Использовать setmode из актуального msvc runtime (т.е. использовать, считай, только компилятор майкрософт)
2) Использовать нативную кодировку - UTF-16.
При соблюдении условий и ввод и вывод работают одинаково хорошо с минимальными "приседаниями" (листинг 3).
К сожалению одинаковых приложений сразу под все системы таким образом не видать. Нужно будет делать какие-то макроподстановки, чтобы свести все к общему знаменателю. Очень жаль, что винда не имеет нативной поддержки UTF-8, хотя сравнительно недавно все-таки начались кое-какие движения в эту сторону:
Microsoft Windows has a code page designated for UTF-8, code page 65001. Prior to Windows 10 insider build 17035 (November 2017), it was impossible to set the locale code page to 65001, leaving this code page only available for (a) explicit conversion functions such as MultiByteToWideChar and/or (b) the Win32 console command chcp 65001 to translate stdin/out between UTF-8 and UTF-16.
Microsoft said that a UTF-8 locale might break some functions (a possible example is _mbsrev) as they were written to assume multibyte encodings used no more than 2 bytes per character, thus code pages with more bytes such as GB 18030 (cp54936) and UTF-8 could not be set as the locale.
This means that "narrow" functions, in particular fopen (which opens files), cannot be called with UTF-8 strings, and in fact there is no way to open all possible files using fopen no matter what the locale is set to and/or what bytes are put in the string, as none of the available locales can produce all possible UTF-16 characters. This problem also applies to all other api that takes or returns 8 bit strings, including Windows ones such as SetWindowText.
On all (known) modern non-Windows platforms, the file-name string passed to fopen is effectively UTF-8. This produces an incompatibility between other platforms and Windows. The normal work-around is to add Windows-specific code to convert UTF-8 to UTF-16 using MultiByteToWideChar and call the "wide" function instead of fopen. Another popular work-around is to convert the name to the 8.3 filename equivalent, this is necessary if the fopen is inside a library function that takes a string filename and thus calling another function is not possible.
There were proposals to add new APIs to portable libraries such as Boost to do the necessary conversion, by adding new functions for opening and renaming files. These functions would pass filenames through unchanged on Unix, but translate them to UTF-16 on Windows. Such a library, Boost.Nowide, was accepted into Boost and will be part of the 1.73 release. This would allow code to be "portable", but required just as many code changes as calling the wide functions.
With insider build 17035 and the April 2018 update (nominal build 17134) for Windows 10, a "Beta: Use Unicode UTF-8 for worldwide language support" checkbox appeared for setting the locale code page to UTF-8. This allows for calling "narrow" functions, including fopen and SetWindowTextA, with UTF-8 strings.
(источник)
По умолчанию виндовая консоль настроена на однобайтовую кодировку DOS, которая соответствует текущему языку системы. Для русской Windows - это кодировка 866. Т.о. существуют две системные локальные кодировки: одна для приложений в псевдо-DOS режиме (866 для рус. вин.), другая для всего остального (1251 для рус. вин.).
DOS-кодировка для консоли оставлена из соображений совместимости.
По умолчанию runtime для консольного вывода пробует перекодировать то, что ему передают в 866. Эта перекодировка работает на основе настроек текущей локали. По умолчанию программа стартует в стандартной локали языка С, которая поддерживает только символы ASCII. Поэтому вы видите пустой вывод (UTF-16 кодируется используя настройки локали в "cырой" ASCII, что приводит к потере информации, дальше скорее всего процесс не идет, т.к. перекодирование завершилось с ошибкой).
Если мы добавим строку
setlocale(LC_ALL, "")
, то мы заставим сменить локаль в соответствии с текущими установками системы. Для русской Windows в Visual Studio оно выберет локаль: "Russian_Russia.1251". Это можно увидеть вот таким кодом:char const * loc = setlocale(LC_ALL, ""); std::cout << loc << '\n';В общем-то это "неожиданно" починит вывод, но за этим легким действием будет стоять очень много. Во-первых кодировка вывода самой консоли все еще DOS 866. Благодаря некой "магической"* внутренней "силе"*, runtime умеет кодировать то, что передается на вывод в одной однобайтовой кодировке, в другую однобайтовую. Поэтому под капотом этого простого действия мы получаем вот что:
- UTF-16 из программы пользователя кодируется в 1251 (используя настройки локали),
- затем "магическая сила" внутри runtime кодирует 1251 в 866,
- затем происходит корректный вывод.
А вот с вводом все еще сложнее, т.к. "магическая сила"* тут почему-то уже не работает. Вводим мы в консоли все еще 866. Мы помним, что текущая локаль установлена в 1251, преобразование в UTF-16 выполняется так, как будто бы мы ввели строку в 1251. В результате мы получаем некорректную UTF-16 строку.
Исправить это можно принудительно задав локаль DOS 866 (напоминаю, что даю названия локалей, которые есть в Visual Studio).
char const * loc = setlocale(LC_ALL, "Russian_Russia.866");Это создаст иллюзию работоспособности, которой, в принципе, достаточно большинству новичков на форуме.
Но если мы таки пишем настоящее интернациональное приложение, то этот способ только добавляет проблем. Во-первых потому что мы принудительно выставили локаль прямо в коде, а во-вторых, потому что само по себе выставление текущей локали лишает возможности вводить текст на нескольких языках одновременно. Именно поэтому явное использование в коде setlocale как c пустой строкой, так и с каким-то конкретным именем - не подходит для настоящих интернациональных приложений: например, в такой конфигурации в французской Windows будут работать только перекодирования "французская дос-кодировка<->юникод UTF-16" и никакие другие варианты.
Небольшое резюме.
Для вывода в юникоде с использованием локалей всегда будет минимум 2 перекодирования: unicode (utf-16) -> current locale encoding -> current console encoding.
Примеры:
1) текущая локаль программы 1251: UTF16->1251->866 = OK
2) текущая локаль программы 866: UTF16->866->866 = OK
Для ввода в юникод с использованием локалей всегда будет минимум 1 перекодирование: current console encoding (as current locale encoding) -> unicode (utf-16).
Примеры:
1) текущая локаль программы 1251: 866 (рассматривается как 1251)->UTF16 = FAIL
2) текущая локаль программы 866: 866 (рассматривается как 866)->UTF16 = OK
Чтобы убрать 1) FAIL, можно сменить кодировку консоли на 1251 с помощью SetConsoleCP и SetConsoleOutputCP. Это обычно советуют здесь во всех темах. Тогда получится:
1) текущая локаль программы 1251: 1251 (рассматривается как 1251)->UTF16 = OK
int main() { SetConsoleCP(1251); // ввод SetConsoleOutputCP(1251); // вывод setlocale(LC_ALL, ""); // ставит 1251 в русской винде std::wstring wstr; std::getline(std::wcin, wstr); // 1251 to UTF16 std::wcout << wstr; // UTF16 to 1251 to 1251 }В общем, единственный прямой способ - это сразу работать в юникоде (что и делает setmode). Что избавляет от постоянных подкапотных перекодировок. Причем сразу в нативном. Для Windows это всегда только UTF-16 на данный момент, увы.
Листинг 1. Вывод кириллицы с помощью qDebug в Qt6
//тип проекта - "Консольное приложение QT" #include <clocale> #include <QtCore> int main() { setlocale(LC_ALL, ""); QByteArray text = "Текст"; QByteArray compressed = qCompress(text); qDebug() << compressed; //выводим сжатый текст qDebug() << QString::fromUtf8(qUncompress(compressed)); //и разжатый qDebug() << QString("Проверка кириллицы"); QString str = "Ещё текст"; qDebug() << str; return 0; }
Листинг 2. Вывод кириллицы с перекодированием исходника в DOS
/* В свойствах cmd (окна консоли) смотрим, какая для неё выбрана кодировка (В Windows обычно ОЕМ-866, то есть, русская кодировка DOS), далее в Qt обращаемся к меню Правка, Выбрать кодировку и выбираем ту же, что и в консоли. Листинг ниже нужно перекодировать в кодировку DOS (OEM-866) и затем вставить в консольное приложение Qt. */ #include <iostream> int main() { char name[80]; std::cout << "Как Вас зовут?\n"; std::cin >> name; std::cout << "Привет, " << name << "!"; return 0; }
Листинг 3. Ввод и вывод кириллицы с помощью _setmode
/* Про _setmode: https://learn.microsoft.com/ru-ru/cpp/c-runtime-library/reference/setmode?view=msvc-170 В этом листинге работают и ввод, и вывод для консоли с кириллицей. Следует иметь в виду, что UTF-8 в Windows поддерживается плохо и рекомендуется в общем случае пользоваться UTF-16. В QString внутри тоже используется UTF-16, поэтому с точки зрения эффективности это самое лучшее решение для Windows. */ #include <iostream> #include <string> #include <io.h> #include <fcntl.h> int main() { _setmode(_fileno(stdout), _O_U16TEXT); _setmode(_fileno(stdin), _O_U16TEXT); std::wcout << L"Привет, мир!" << std::endl << L"Введите имя: "; std::wstring wstr; std::getline(std::wcin, wstr); std::wcout << wstr; return 0; }
Листинг 4. Сложное решение с шаблоном класса
#include <windows.h> #include <iostream> #include <iomanip> #include <string> #include <cstring> template <bool IsInput, size_t Chunk = 128> class Utf16StreamBufWin32 : public std::basic_streambuf<wchar_t> { public: Utf16StreamBufWin32(DWORD handleId) : m_handle(::GetStdHandle(handleId)) , m_buffer() { m_buffer.reserve(Chunk); if (IsInput) { setg(0, 0, 0); } } protected: void replace() { if(m_buffer.empty()) return; size_t start_pos = 0; while ((start_pos = m_buffer.find(L"\r\n", start_pos)) != std::wstring::npos) { m_buffer.replace(start_pos, 2, L"\n"); start_pos += 1; } } std::basic_streambuf<wchar_t>* setbuf(char_type* s, std::streamsize n) override { (void)s; (void)n; return nullptr; } int sync() override { if (IsInput) { ::FlushConsoleInputBuffer(m_handle); setg(0, 0, 0); } else { if (m_buffer.empty()) { return 0; } DWORD writtenSize; ::WriteConsoleW(m_handle, m_buffer.c_str(), m_buffer.size(), &writtenSize, NULL); } m_buffer.clear(); return 0; } int_type underflow() override { if (!IsInput) { return traits_type::eof(); } if (gptr() >= egptr()) { m_buffer.resize(Chunk + 2); DWORD readSize; wchar_t* p = m_buffer.data(); if (!::ReadConsoleW(m_handle, p, m_buffer.size(), &readSize, NULL)) { return traits_type::eof(); } replace(); setg (p, p, p + readSize); if (gptr() >= egptr()) return traits_type::eof(); } return sgetc(); } int_type overflow(int_type c = traits_type::eof()) override { if (IsInput) { return traits_type::eof(); } m_buffer += traits_type::to_char_type(c); return traits_type::not_eof(c); } private: HANDLE m_handle; std::wstring m_buffer; }; int main() { std::wcout.rdbuf(new Utf16StreamBufWin32<false>(STD_OUTPUT_HANDLE)); std::wcin.rdbuf(new Utf16StreamBufWin32<true>(STD_INPUT_HANDLE)); std::wstring name; std::wcout << L"Как Вас зовут?\n"; std::getline(std::wcin, name); std::wcout << L"Привет, " << name << L"!\n" ; return 0; }
Увы, гарантий, что последнее решение тоже заработает "из коробки", нет, хотя у меня именно так и произошло.
Листинг 5. Ещё один вариант с установкой локали и wstring.
У меня сработало и так.
#include <QCoreApplication> // консольное окно #include <QTextStream> // для раб. с текстом #include <conio.h> // kbhit, getch #include <stdio.h> // fopen, printf #include <qmath.h> // pow, sqrt #include <iostream> // cout, cin #include <string> // string #include <locale> // setlocale #include <windows.h> // функции windows using namespace std; // Объявим пространство имен std int main() { setlocale(LC_CTYPE,".866"); wcout << L"Ввод данных с клавиатуры. Вывод в консольное окно. " <<endl; wcout << L"Введите ваше имя " <<endl; wstring wstr; // объявили строковую переменную getline(wcin, wstr); // ввод строки с клавиатуры в переменную wstr // Выведем сообщение на экран wcout << wstr << L" - у вас хорошее имя!"<< endl; //вывод строки // Ждем нажатия любой клавиши while(!kbhit()); return 0; // значение, возвращаемое функцией main() }
14.08.2023, 13:42 [994 просмотра]