Я пытался разработать небольшое консольное приложение на C++ для Windows, которое взаимодействует с базой данных SQLite. Однако эта база данных может содержать символы UTF-8, например. Греческие буквы. Поэтому программе необходимо вводить эти символы из пользовательской консоли, использовать их в запросах и выводить.
Я хотел бы ввести эти символы, используя getline или, в идеале, getch.
Сначала даже простой ввод и вывод строки utf-8 не работал.
С использованием
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
Введенные строки имели правильную длину, но все символы были нулевыми. Например. ввод «ΑΒΓ» сохранит строку из 3 символов со значением 0.
Использование 1253 (кодовая страница для греческого языка) вместо CP_UTF8 работало для объединения, ввода и вывода. В отладчике я заметил, что значения строк недействительны и отображаются неправильно. Я также заметил, что вместо 2 байтов на символ был только 1, но, поскольку выводился нормально, я не особо задумывался об этом, поскольку потери данных быть не могло.
Однако API SQLite с этим не согласился. Построение запроса с использованием пользовательского ввода не даст результата. Выполнение запроса с использованием жестко закодированного строкового литерала с тем же символом utf-8 дало бы результат, но и жестко закодированный запрос, и результаты имели другой формат, чем пользовательский ввод, который я получал раньше. Они правильно отображались в отладчике, имели по 2 байта на символ и выводили тарабарщину под 1253 CP, но правильно под CP_UTF8.
Я не нашел никаких упоминаний об этом несоответствии в Интернете, хотя не уверен, что вообще смотрел в нужных местах. Поскольку результаты SQLite выводятся правильно под CP_UTF8, мне бы хотелось, по крайней мере, иметь возможность вводить символы в желаемом формате API SQLite (т. е. в том же формате, в котором хранятся литералы) или, по крайней мере, иметь возможность конвертировать из текущего 1253 формат к нему.
Ниже приведен минимальный воспроизводимый пример с концепциями, упомянутыми выше:
#include <iostream>
#include<string>
#include<windows.h>
using namespace std;
int main()
{
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
//Dysfunctional Input
string input1;
cout << "Enter greek letters: ";
cin >> input1;
cout << "You entered: " << input1 << endl; //input1 has correct size but all chars are null
SetConsoleOutputCP(1253);
SetConsoleCP(1253);
//Working Input and Output
string input2;
cout << "Enter greek letters: ";
cin >> input2;
cout << "You entered: " << input2 << endl; //Should output properly
//Printing a literal
string literall = u8"Γειά σας";
cout << "Literal under 1253: " << literall << endl; //Giberish
//Printing a literal under CP_UTF8
SetConsoleOutputCP(CP_UTF8);
cout << "Literal under CP_UTF8: " << literall << endl; //Correct output
return 0;
}
Приведенный выше код показывает аналогичные результаты с getline и _getch вместо cin, что многообещающе.
Заключительные замечания:
setlocale(LC_ALL, "");, похоже, также приводит к сбою рабочих примеров, приведенных выше. Ввод «ΑΒΓ» становится «ΑΑΑ», и оба литерала являются бредом.Вы уверены, что ваш SQLite настроен на использование UTF-8?
А 2 байта на символ подразумевают UTF-16 вместо UTF-8.
@MarkRansom, база данных содержала записи с данными на греческом языке, а запросы жесткого кодирования в моем консольном приложении дают правильный результат, как упоминалось выше. Я предполагаю, что это означает, что он настроен на использование UTF-8?
Литерал, определенный в приведенном выше примере, имеет длину 8 букв, но в отладчике отображается как размер 15. Означает ли это, что он хранится в UTF-16? API C-SQLite поддерживает только UTF-8, но его результаты имеют по 2 байта на греческий символ, поэтому я не уверен, как это понимать.
Вы компилируете с флагом компилятора /utf-8 ?
@Элджей, я только что добавил это, и, похоже, это не имеет значения. Оказывает ли это какое-либо влияние на наборы символов консоли?
@andydexter флаг /utf-8 вообще не влияет на консоль, а только на набор символов, который компилятор использует для анализа вашего исходного кода и для хранения узких строковых литералов в исполняемом файле. В этом случае вам лучше НЕ пытаться настроить консоль на использование UTF-8. Вместо этого используйте std::wcin и std::wstring для работы со строками UTF-16 (поскольку Windows является собственной ОС UTF-16) и преобразуйте данные в UTF-8 при передаче их в API SQLite, используя std::wstring_convert или WideCharToMultiByte(), ICONV/ICU или любой другой другой эквивалентный API/библиотека обработки Unicode по вашему выбору.
Хорошо, 15 байт, а не 16. Это не «два байта на символ». Почти так и есть, потому что, за исключением пробела, эти греческие символы в UTF-8 превращаются в два байта каждый, но это просто совпадение. Конкретно это "\xce\x93\xce\xb5\xce\xb9\xce\xac \xcf\x83\xce\xb1\xcf\x82". UTF-8 может содержать от 1 до 4 байтов на символ, в зависимости от символа.





Мне никогда не удавалось заставить консоль Windows работать напрямую с UTF-8, не запутываясь ни в самой Windows, ни в компиляторе/версии/libc/и т. д.
Однако использование консольного API с функциями расширенных символов всегда работает. Таким образом, чтобы обеспечить работу ввода-вывода UTF-8, вам необходимо соответствующим образом наполнить стандартные потоки фильтрами преобразования. Следующие настройки настраивают ситуацию:
#include <windows.h>
#include <shellapi.h>
#pragma comment(lib, "Shell32")
///////////////////////////////////////////////////////////////////////////////////////////////////
namespace duthomhas::utf8::console
///////////////////////////////////////////////////////////////////////////////////////////////////
{
//-------------------------------------------------------------------------------------------------
struct Input: public std::streambuf
//-------------------------------------------------------------------------------------------------
{
using int_type = std::streambuf::int_type;
using traits = std::streambuf::traits_type;
HANDLE handle;
char buffer[ 4 ];
wchar_t c;
DWORD n;
Input( HANDLE handle ): handle(handle) { }
Input( const Input& that ): handle(that.handle) { }
virtual int_type underflow() override
{
auto ok = ReadConsoleW( handle, &c, 1, &n, NULL );
if (!ok or !n) return traits::eof();
if (c == '\r') return underflow();
n = WideCharToMultiByte( CP_UTF8, 0, (const wchar_t*)&c, 1, (char*)buffer, sizeof( buffer ), NULL, NULL );
setg( buffer, buffer, buffer + n );
return n ? traits::to_int_type( *buffer ) : traits::eof();
}
};
//-------------------------------------------------------------------------------------------------
struct Output: public std::streambuf
//-------------------------------------------------------------------------------------------------
{
using int_type = std::streambuf::int_type;
using traits = std::streambuf::traits_type;
HANDLE handle;
std::string buffer;
Output( HANDLE handle ): handle(handle) { }
Output( const Output& that ): handle(that.handle) { }
virtual int_type sync() override
{
DWORD n;
std::wstring s( buffer.size(), 0 );
s.resize( MultiByteToWideChar( CP_UTF8, 0, (char*)buffer.c_str(), (int)buffer.size(), (wchar_t*)s.c_str(), (int)s.size() ) );
if (buffer.size() and s.empty()) return -1;
buffer.clear();
return WriteConsoleW( handle, (wchar_t*)s.c_str(), (DWORD)s.size(), &n, NULL ) ? 0 : -1;
}
virtual int_type overflow( int_type value ) override
{
buffer.push_back( traits::to_char_type( value ) );
if (traits::to_char_type( value ) == '\n') sync();
return value;
}
};
//-------------------------------------------------------------------------------------------------
void initialize()
//-------------------------------------------------------------------------------------------------
{
// Update the standard I/O streams, maybe
DWORD mode; HANDLE
handle = GetStdHandle( STD_INPUT_HANDLE ); if (GetConsoleMode( handle, &mode )) std::cin .rdbuf( new Input ( handle ) );
handle = GetStdHandle( STD_OUTPUT_HANDLE ); if (GetConsoleMode( handle, &mode )) std::cout.rdbuf( new Output( handle ) );
handle = GetStdHandle( STD_ERROR_HANDLE ); if (GetConsoleMode( handle, &mode )) std::cerr.rdbuf( new Output( handle ) );
}
} // namespace duthomhas::utf8::console
Теперь в вашем main() обязательно инициализируйте:
int main(...)
{
duthomhas::utf8::console::initialize();
// Ask the user to "Enter some Greek"
std::cout << "Βάλε λίγα ελληνικά: ";
std::string s;
getline( std::cin, s );
std::cout << "Good job! You entered: " << s << "!\n";
Опять же, это всегда работает — потому что он обходит обычную обработку «символ — это байт» и использует обработку Windows UTF-16 непосредственно под капотом — но только если вы действительно подключены к консоли!
⟶ Do remember, though, that the Windows console cannot handle anything outside the BMP. Redirected file I/O still works with the full Unicode set.
Полный код Юникода имеет длину 21 бит и обычно хранится в 32-битном целочисленном объекте (например, char32_t).
Если вы хотите обрабатывать UTF-8, вы больше не можете рассматривать «символы» как байтовые значения. Один символ может иметь длину от одного до четырех байтов, а каждый последующий символ может иметь разное количество байтов.
Кроме того, один «символьный глиф» может состоять из более чем одной кодовой точки!
tl;dr: все представляет собой строку.
Почти все, что вы захотите сделать с помощью UTF-8, можно обработать как подстроки, и вам следует структурировать свой код соответствующим образом.
Если вы планируете что-либо делать с данными UTF-8, вам следует взглянуть на ICU.
Вот еще один ответ, который я написал конкретно об использовании отделения интенсивной терапии.
⟶ ICU also includes functions for interacting with the console, but they are not out-of-the-box-supported on Windows — again due to compiler/version/etc.
Большое спасибо за ответ, все отлично работает из коробки. К счастью, мне не нужно ничего делать с этими строками, кроме отображения. Можно ли примерно объяснить, что переопределяет ваше решение?
Также есть ли способ использовать аналогичную реализацию для анализа символов с использованием _getch()? Я понимаю, что он возвращает только байт ASCII символа, но после некоторого тестирования я заметил, что при вводе греческих символов 2 байта буферизуются, поэтому вы можете использовать 2 _getch(), чтобы получить один символ, хотя я думаю, что нет никакого способа узнать, как это сделать. длинный каждый символ.
Буферы потоков ввода и вывода переопределяются для использования функций ввода-вывода всей консоли Win32 вместо методов ввода-вывода по умолчанию (какими бы они ни были). Такие вещи, как _getch(), предназначены для нажатия одной клавиши, что совершенно отличается от «символа».
Хорошей новостью является то, что ввод с клавиатуры имеет тенденцию осуществляться частями, поэтому, если пользователь вводит символ, вы, безусловно, можете проверить, доступен ли какой-либо ввод, и прочитать весь ввод сразу, и он будет представлять собой один «символ». Но опять же, это работает только тогда, когда вы подключены к клавиатуре. Возможно, стоит задать новый вопрос именно по этому поводу. (Однако не знаю, смогу ли я дать вам полезный ответ сейчас.)
Понятно, но я предполагаю, что эти куски не разделены ничем, кроме времени, верно? Поскольку _getch() и все другие известные мне методы ввода блокируются, я не знаю, как проверить, доступен ли ввод. Если понадобится, я рассмотрю возможность задать новый вопрос позже. В любом случае спасибо!
Время соответствует скорости, с которой человек может изменить положение пальца, чтобы нажать другую клавишу. Компьютер работает намного быстрее.
Весь смысл кодирования заключается в том, чтобы одни и те же байты представляли совершенно разные символы. Попытка вывести UTF-8 с использованием кодировки 1253 почти по определению приведет к тарабарщине.