Важно В Qt5/Qt6 строки QString представляются в кодировке UTF-16. Строковые константы, которые в редакторе кода записываются, как правило в кодировке UTF-8 - всё равно преобразуются компилятором во внутреннее представление в UTF-16.
Самое важное - нормализация работает не везде. Иными словами, не все графемные кластеры можно нормализовать — в смысле «свести к одной и той же последовательности кодов» универсально для всех случаев. Почему не все можно нормализовать:
Стандарты Unicode Normalization
Форма | Назначение | Пример |
---|---|---|
NFC | Каноническая композиция | U+0065 + U+0301 → U+00E9 |
NFD | Каноническая декомпозиция | U+00E9 → U+0065 + U+0301 |
NFKC | Совместимая композиция | ¹ → 1 |
NFKD | Совместимая декомпозиция | ① → 1 + ◯ |
Пока мы в Qt5/Qt6 работаем с числами, латиницей, кириллицей - проблем возникать не должно. Но как только приходится использовать символы за диапазоном BMP, или графемные кластеры - "привычные" функции/методы из библиотеки начинают работать не так, как ожидалось.
Функции/методы определения длины строки (а нам важно именно количество графем, включая кластеры графем) начинают возвращать не те цифры, функции/методы получения части строк могут свободно разрезать строки не в тех местах - разрезать суррогатные пары или графемные кластеры. Ниже небольшой пример это демонстрирующий:
#include <QCoreApplication>
#include <QByteArray>
#include <QTimer>
#include <QDebug>
#include <QList>
#include <unicode/uversion.h>
#include <unicode/brkiter.h>
#include <unicode/unistr.h>
int countGraphemes(const QString& str) {
UErrorCode status = U_ZERO_ERROR;
// Конвертируем QString (UTF-16) в icu::UnicodeString
icu::UnicodeString uniStr(str.utf16(), str.length());
// Создаём BreakIterator для графем
icu::BreakIterator* graphemeIter = icu::BreakIterator::createCharacterInstance(icu::Locale(), status);
if (U_FAILURE(status)) {
qDebug() << "Failed to create BreakIterator:" << u_errorName(status);
return -1;
}
graphemeIter->setText(uniStr);
int32_t count = 0;
while (graphemeIter->next() != UBRK_DONE) {
count++;
}
delete graphemeIter;
return count;
}
int main(int argc, char* argv[]) {
QCoreApplication app(argc, argv);
QString utf16_1 = "Hello 😊";
QByteArray utf8_1 = utf16_1.toUtf8();
QList<uint> utf32_1 = utf16_1.toUcs4().toList(); //.toList() для совместимости с Qt5
qDebug() << "String: " << utf16_1;
qDebug() << "Size UTF-8 (bytes): " << utf8_1.size();
qDebug() << "Length (UTF-16 code units): " << utf16_1.length();
qDebug() << "Length (UTF-32 code points): " << utf32_1.size();
qDebug() << "Graphemes:" << countGraphemes(utf16_1);
qDebug() << "- - - - - - - - - - breakaway line - - - - - - - - - - - - ";
QString utf16_2 = "Хэлоу ворлд!";
QByteArray utf8_2 = utf16_2.toUtf8();
QList<uint> utf32_2 = utf16_2.toUcs4().toList(); //.toList() для совместимости с Qt5
qDebug() << "String: " << utf16_2;
qDebug() << "Size UTF-8 (bytes): " << utf8_2.size();
qDebug() << "Length (UTF-16 code units): " << utf16_2.length();
qDebug() << "Length (UTF-32 code points): " << utf32_2.size();
qDebug() << "Graphemes:" << countGraphemes(utf16_2);
qDebug() << "- - - - - - - - - - breakaway line - - - - - - - - - - - - ";
// в нормализованной форме (NFC) U+00E9
QString utf16_3 = "é";
QByteArray utf8_3 = utf16_3.toUtf8();
QList<uint> utf32_3 = utf16_3.toUcs4().toList(); //.toList() для совместимости с Qt5
qDebug() << "String: " << utf16_3;
qDebug() << "Size UTF-8 (bytes): " << utf8_3.size();
qDebug() << "Length (UTF-16 code units): " << utf16_3.length();
qDebug() << "Length (UTF-32 code points): " << utf32_3.size();
qDebug() << "Graphemes:" << countGraphemes(utf16_3);
qDebug() << "- - - - - - - - - - breakaway line - - - - - - - - - - - - ";
//в декомпозированной форме (NFD) — 2 кодовые точки (U+0065 + U+0301)
QString utf16_4 = QString::fromUtf8("\x65\xCC\x81");
QByteArray utf8_4 = utf16_4.toUtf8();
QList<uint> utf32_4 = utf16_4.toUcs4().toList(); //.toList() для совместимости с Qt5
qDebug() << "String: " << utf16_4;
qDebug() << "Size UTF-8 (bytes): " << utf8_4.size();
qDebug() << "Length (UTF-16 code units): " << utf16_4.length();
qDebug() << "Length (UTF-32 code points): " << utf32_4.size();
qDebug() << "Graphemes:" << countGraphemes(utf16_4);
QTimer::singleShot(1000, &app, &QCoreApplication::quit);
return app.exec();
}
А вывод будет следующий:
String: "Hello 😊" Size UTF-8 (bytes): 10 Length (UTF-16 code units): 8 Length (UTF-32 code points): 7 Graphemes: 7 - - - - - - - - - - breakaway line - - - - - - - - - - - - String: "Хэлоу ворлд!" Size UTF-8 (bytes): 22 Length (UTF-16 code units): 12 Length (UTF-32 code points): 12 Graphemes: 12 - - - - - - - - - - breakaway line - - - - - - - - - - - - String: "é" Size UTF-8 (bytes): 2 Length (UTF-16 code units): 1 Length (UTF-32 code points): 1 Graphemes: 1 - - - - - - - - - - breakaway line - - - - - - - - - - - - String: "é" Size UTF-8 (bytes): 3 Length (UTF-16 code units): 2 Length (UTF-32 code points): 2 Graphemes: 1
Способ | Ожидаемый результат | Описание |
---|---|---|
Использование UTF-32 | удовлетворительный | защищает только от порезки code points, но не от порезки графемных кластеров |
Использование QTextBoundaryFinder::Grapheme | хороший | работает с графемными кластерами, функционал может быть слегка ограничен |
Использование ICU | лучший | работает с графемными кластерами, функционал может быть выше чем у QTextBoundaryFinder, доступны расширенные настройки |