Использование Qt-2.2.x в русскоязычных программах.
Автор : Сукиязов Сергей Александрович
<>Введение.>
Эта статья содержит информацию об одной из популярных библиотек для организации графического пользовательского интерфейса (GUI) - Qt Toolkit, разработаной норвежской фирмой Troll Tech.
Библиотека Qt представляет собой законченное и глубоко проработанное многоплатформенное объектноориентированное окружение для разработки GUI-приложений с использованием языка C++. Qt также хорошо интегрируется с библиотеками OpenGL/Mesa 3D.
Qt является бесплатной (free) библиотекой для разработки бесплатного программного обеспечения (freeware) в X Window System. Она включает в себя полный исходный код X-версии библиотеки и make-файлы для операционных систем Linux, Solaris, SunOS, FreeBSD и др. Эта редакция (free) Qt может модифицироваться и распространяться с соблюдением условий перечисленных в файле LICENSE.QPL
.
Qtтакже очень хорошо поддерживает операшионные системы Windows 95 и NT. Программный код разработанный для X-версии Qtможет быть перекомпилирован и использован под управлением Windows 95/NT версии библиотеки Qt.
В настоящее время Qt используется в сотнях проектов по разработке программного обеспечения по всему миру, включая популярную оболочку K Desktop Environment. Для более полной информации смотрите ссылку http://www.trolltech.com/qtprogs.html.
Qt можно загрузить по адресу http://www.trolltech.com/dl/ или через анонимный FTP с сервера ftp://ftp.trolltech.com/. На этом сервере также доступны нестабильные версии, находящиеся на стадии разработки, в виде ежедневных "снапшотов".
Qt содержит замечательную документацию: более 750 страниц в формате Postscript и HTML. Документация также доступна через WEB: http://doc.trolltech.com/.
Qt является полностью объектноориентированной библиотекой. Все "виджеты" представляют собой C++ объекты, и, используя наследование, создание новых "виджетов" получается простым и естественным.
В рамках этой статьи мы не будем подробно разбирать все детали программирования с использованием библиотеки Qt. Как говорилось выше Qt содержит подробную документацию и множество примеров. В этой статье мы остановимся на проблемах корректной локализации (интернационализации) программ, разработанных с использованием Qt, и попытаемся выработать некоторые советы по использованию классов библиотеки Qt, которые помогут избежать проблем с интернационализацией программ.
Т.к. библиотека Qt для представления текстовых данных использует UNICODE, то при некорректном преобразовании текстовых данных из однобайтовых кодировок в UNICODE и проявляются проблемы с отображением национальных символов.
Внешне эти проблемы выглядят следующим образом: вместо национальных символов (коды более 127) выводится символ '?' или вместо строки, содержащей национальные UNICODE-символы (коды более U+00FF), в результате преобразований получаются пустые однобайтовые строки.
Может сложиться впечатление, что в Qt вопрос преобразования из однобайтовых строк к UNICODE-строкам недостаточно продуман. На самом деле это не так. В Qt преобразование строк достачно хорошо продуманно и описано в документации. К единственному недостатку докуметации можно отнести тот факт, что документацию писали в большинстве своем англоязычные программисты и, соответственно, вопросам использования национальных UNICODE-символов уделено незначительное внимание.
Дело в том, что англоязычные программисты используют кодировку ISO-8859-1 (или US-ASCII), коды символов которой совпадают с UNICODE-кодами этих символов. В этом случае преобразование из однобайтовой строки в UNICODE-строку сводится к простому расширению однобайтового значения до двухбайтового (просто в сташий байт заносится значение 0). Для национальных символов преобразование не столь тривиально: русская буква 'А' имеет код в кодировке ISO-8859-5 равный 0xB0, в кодировке UNICODE - код равный U+0410. В результате простого расширения однобайтового значения русская буква 'А' получит UNICODE код U+00B0 вместо U+0410, что далеко не одно и тоже.
Далее мы проанализируем причины приводящие к возникновению этих ошибок и разберем несколько советов по их устранению.
Представление строк в Qt.
В библиотеке Qt, как говорилось выше, внутренний формат строк - UNICODE. Поэтому почти все методы и функции библиотеки Qt в качестве своих фактических параметров желают иметь именно UNICODE строки.
Для хранения кажого символа в кодировке UNICODE в Qt отводится два байта. Для представления UNICODE символа в Qt используется класс QChar
. Полное и подробное описание конструкторов, методов и операторов этого класса можно посмотреть в документации по библиотеке Qt. Преобразование по умолчанию из одобайтового символа (C тип char
) в UNICODE символ (Qt тип QChar
) выполняется простым расширением значения. Т.е. русская буква 'А' (код в ISO-8859-5 - 0xB0) получает UNICODE код U+00B0.
Для представления UNICODE строк в Qt используется класс QString
, который в общем представляет собой массив символов типа QChar
. Полное и подробное описание конструкторов, методов и операторов этого класса также можно посмотреть в документации по библиотеке Qt. Преобразование по умолчанию из одобайтовой строки (C тип char *
) в UNICODE строку (Qt тип QString
) также выполняется простым расширением значения. Т.е. русская буква 'А' (код в ISO-8859-5 - 0xB0 ) получает UNICODE код U+00B0.
Для хранения однобайтовых строк, в библиотеке Qt, используется класс QCString
, наследуемый от класса QByteArray
. В этом классе строки представлены как массивы однобайтовых символов.
Для корректного преобразования из однобайтовых строк в UNICODE-строки и обратно с учетом особенностей национальных кодировок, в Qt используется класс QTextCodec
. Полное и подробное описание конструкторов, методов и операторов этого класса также можно посмотреть в документации по библиотеке Qt. Мы только остановимся на некоторых наиболее важных методах этого класса:
static QTextCodec* QTextCodec::codecForName(const char* hint, int accuracy=0);
- Производит поиск среди всех объектов
QTextCodec
, и возвращает указатель на объект QTextCodec
, имя которого наиболее совпадает с именем, переданным через параметр hint. Если кодек (Codec) не найден возвращает NULL
. Параметр accuracy определяет точность совпадения имени кодека, значение 0 определяет точное совпадение.
static QTextCodec* QTextCodec::codecForLocale();
- Возвращает указатель объект который наиболее подходит для набора символов, используемого в текущей установке локали. Про настройки локали можно прочитать в статье "Локализация, как она есть".
virtual QString QTextCodec::toUnicode(const char* chars, int len) const;
- Преобразует len символов из chars в UNICODE.
virtual QCString QTextCodec::fromUnicode(const QString& uc, int& lenInOut) const;
- Преобразует из UNUCODE в однобайтовую строку lenInOut символов (типа
QChar
) начиная с первого символа строки uc. Возвращает объект типа QCString
, и также возвращает длинну результата в lenInOut.
Для последних двух методов существуют перегруженные методы, которые позволяют опускать второй параметр:
QCString QTextCodec::fromUnicode(const QString& uc) const;
QString QTextCodec::toUnicode(const char* chars) const;
В качестве примера использования класса QTextCodec
для преобразования строк, можно привести следующий пример (предполагается что используется локаль "ru" и она настроена):
Разберем подробнее приведенный пример.
В строке 5 используется преобразование по умолчанию (вызывается конструктор QString( const char * )
) из одобайтовой строки (C тип char *
) в UNICODE строку QString
- т.е. простое расширением значения. В этом случае русская буква 'А' (код в ISO-8859-5 - 0xB0) получает UNICODE код U+00B0.
В строке 6 производится преобразование из строки QString
в однобайтовую строку с использованикм метода QCString QString::local8Bit()
. Этот метод выполняет преобразование с учетом установок локали, и подробнее это метод будет рассмотрен ниже. Т.к. для кириллицы в UNICODE не определены символы с кодами от U+0080 до U+00FF, то в результате преобразования вместо русских букв получаются символы '?'. То же самое происходит и с другими языками, для которых UNICODE коды символов больше U+00FF.
В строке 8 производится преобразование из строки QString
в однобайтовую строку с использованикм метода const char *QString::latin1()
. Этот метод выполняет преобразование простым сжатием двухбайтового значения до однобайтового. При сжатии для значений, старший октет которых равен 0, берется значение младшего октета, а для значений, старший октет которых отличен от нуля, берется значение 0 (Октет - последовательность из 8 бит или байт. На различных платформах размер байта может отличаться.). Подробнее это метод будет рассмотрен ниже. Т.к. при отбросе старшего нулевого октета для U+00B0 получается 0xB0 (код русской буквы 'А' в ISO-8859-5) то в строке 7 русские символы выводятся правильно.
В строке 11 мы находим кодек, который будет использоваться по умолчанию для текущих установок локали. Если метод QTextCodec::codecForLocale()
вернет ненулевое значение то кодек найден и можно выполнять преобразования.
В строке 15 используется преобразование с помощью найденного кодека. В этом случае русская буква 'А' (код в ISO-8859-5 - 0xB0) получает UNICODE код U+0410. Поэтому в строке 16 будут корректно напечатаны русские буквы. А строке 19 при отбросе старшего не нулевого октета для U+0410 (старший октет 0x04) получается значение 0x00 (признак конца строки в языке C) и, соответственно, выводится пустая строка.
Для корректного преобразования из однобайтовых строк в UNICODE-строки и обратно с учетом особенностей национальных кодировок кроме класса QTextCodec
, в классе QString
помимо конструкторов введены специалные методы, которые позволяют конструировать объекты QString
с учетом особенностей определенных однбайтовых кодировок. Это следующие методы:
const char* latin1() const;
- Этот метод возвращает Latin-1 представление строки. Этот метод выполняет преобразование простым сжатием двухбайтового значения до однобайтового. При сжатии для значений, старший октет которых равен 0, берется значение младшего октета, а для значений, старший октет которых отличен от нуля, берется значение 0. (Октет - последовательность из 8 бит или байт. На различных платформах размер байта может отличаться.). Предполагается, что UNICODE строка
QString
представлена в кодировке ISO-8859-1.
static QString fromLatin1(const char*, int len=-1);
- Этот метод использует для преобразования из однобайтовой строки (C тип
char *
) в UNICODE строку QString
кодек для кодировки ISO-8859-1. Т.о. преобразование выполняется простым расширением значения. В этом случае русская буква 'А' (код в ISO-8859-5 - 0xB0) получает UNICODE код U+00B0. Если задан параметр len то преобразуются только len символов исходной строки.
QCString utf8() const;
- Этот метод возвращает UTF-8 представление строки. Этот метод выполняет преобразование с использованием кодека для кодировки UTF-8. Кодировка UTF-8 позволяет закодировать UNICODE представление строк с помощью однобайтовых строк.
static QString fromUtf8(const char*, int len=-1);
- Этот метод использует для преобразования из однобайтовой строки (C тип
char *
) в UNICODE строку QString
кодек для кодировки UTF-8. Если задан параметр len то преобразуются только len символов исходной строки.
QCString local8Bit() const;
- Этот метод возвращает представление строки зависящее от установок текущей локали. Он выполняет преобразование с использованием кодека для для текущих установок локали. Т.о. преобразование выполняется с учетом особенностей кодировки, определяемой установками текущей локали. Например если в текущей локали используется кодировка ISO-8859-5, то будет использован кодек для этой кодировки. В этом случае русская буква 'А' (код в UNICODE - U+0410) получает код 0xB0. Если в текущей локали UNICODE код символа не определен, то однобайтовое значение для такого символа будет - '?'. Предполагается, что UNICODE строка
QString
представлена в "чистом" UNICODE. Если для текущей локали не найден подходящий кодек то, используется кодек для кодировки ISO-8859-1.
static QString fromLocal8Bit(const char*, int len=-1);
- Этот метод использует для преобразования из однобайтовой строки (C тип
char *
) в UNICODE строку QString
кодек для текущих установок локали. Т.о. преобразование выполняется с учетом особенностей кодировки, определяемой установками текущей локали. Например, если в текущей локали используется кодировка ISO-8859-5, то будет использован кодек для этой кодировки. В этом случае русская буква 'А' (код в ISO-8859-5 - 0xB0) получает UNICODE код U+0410. Если задан параметр len, то преобразуются только len символов исходной строки. Если для текущей локали не найден подходящий кодек, то используется кодек для кодировки ISO-8859-1.
C использованием методов класса QString
приведенный выше пример можно изменить следующим образом:
Разработчики Qt реомендуют использовать перечисленные выше методы для конструирования UNICODE строк QString
и пребразования из QString
в однобайтовые строки, вместо преобразований по умолчанию.
Явный вызов вышеперечисленных методов, для конструирования строк типа QString
, позволит быть уверенным в том, что в программе используется "чистый" UNICODE.
Причины возникновения ошибок при обработке национальных символов. Рекомендации по их устранению.
Т.к. большинство программ написаны англоязычными программистами, которые используют в качестве основной кодировку ISO-8859-1, в таких программах мало внимания уделяется преобразованиям символов. Как уже говорилось выше, для кодировок ISO-8859-1 (LATIN1) и US-ASCII, преобразования из однобайтовой строки в UNICODE и обратно не имеют особого значения, т.к. коды символов (значения кодов) в этих кодировках совпадают. Например, латинская буква 'A' имеет код в кодировке ISO-8859-1 (US-ASCII) 0x65 и в UNICODE код латинской буквы 'A' будет равным U+0065. Т.е. <Код символа ISO-8859-1>==<Код символа UNICODE>. Соответственно, для кодировок ISO-8859-1 (LATIN1) и US-ASCII, преобразование простым расширением/сжатием значения выполняется корректно и программа работает с "чистым" UNICODE.
Более того, англоязычные программисты не могут заметить и, соответственно, исправить такие ошибки, т.к. в большинстве случаев для отладки программ они используют тексты, содержащие латинские символы, и локаль с кодировкой ISO-8859-1. Нереально представить себе ситуацию, когда каждый программист, например, из Великобритании для отладки программы устанавливает у себя русские шрифты и настраивает русскую локаль, а ведь кроме русского языка существует еще множество других языков.
Поэтому, чтобы избавить себя и пользователей программы от проблем с локализацией, нужно более четко определить ситуации когда возникают эти ошибки и сформулировать некоторые принципы написания кода программы. Все рекомендации, которые будут сформулированны ниже, в общем виде, применимы не только к программам использующим библиотеку Qt, но и ко всем другим программам которые хранят и обрабатывают строки в формате UNICODE или WideChar.
Ситуация первая.
Чаще всего ошибки с преобразованием национальных символов в библиотеке Qt возникают из-за использования неявного преобразования типов к типу QString
. Неприятность этих ошибок заключается в том, что на этапе компиляции не выдается никаких сообщений и предупреждений, т.к. с точки зрения синтаксиса и правил языка программирования ошибки действительно нет. Подобная ошибка проявляется только на этапе выполнения программы, причем к исключительной ситуации не приводит.
В каких местах программы возникают такие ошибки ?
Наиболее частое место возникновения этой ошибки - преобразование типа при передаче параметров в процедуру или функцию. Например, есть некоторая функция, которая в качестве параметра требует строку типа QString
, а при вызове этой функции ей в качестве фактического параметра передается обчная C-like строка:
В этом примере, в строке 10, выполняется неявное преобразование типов из const char *
в QString
с использованием конструктора QString::QString( const char * )
. В результате такого преобразования, как говорилось выше, происходит простое расширение однобайтового значения до двухбайтового. Т.е. строка "Привет мир!!!" в формате UNICODE будет иметь следующее значение:
"U+00BF,U+00E0,U+00D8,U+00D2,U+00D5,U+00E2,U+0020,U+00DC,
U+00D8,U+00E0,U+0021,U+0021,U+0021"
В "чистом" UNICODE эта строка должна иметь значение:
"U+041F,U+0440,U+0438,U+0432,U+0435,U+0442,U+0020,
U+043C,U+0438,U+0440,U+0021,U+0021,U+0021"
В качестве другого примера можно привести функцию, которая в качестве параметра требует C-like строку, а при вызове этой функции ей в качестве фактического параметра передается строка типа QString
:
В этом примере в строке 13 выполняется неявное преобразование типов из QString
в const char *
с использованием оператора преобразования типа QString::operator const char *() const;
, который определен в файле qstring.h
как:
Как видно из определения оператора QString::operator const char *() const
, для преобразования типов будет вызван метод const char* QString::latin1() const
. Этот метод для национальных символов с кодами большими U+00FF возвращает нулевое значение, т.о. функция void foo( const char * str )
, из примера выше, в качестве параметра получит C-like строку, первый символ которой равен '\0', что, в свою очередь, является признаком конца строки для C-like строк. Поэтому в результате выполнения строки 5 будет напечатана пустая строка.
Из-за того, что в этих примерах в той или иной степени используется "не чистый" UNICODE, и возникают проблемы с обработкой национальных символов. Впрочем, если мы установим локаль в "en_US" (export LC_ALL=en_US), то приведенные выше примеры будут корректно выводить русские символы.
Этот тип ошибок очень часто встречается когда, например, из программы использующей библиотеку Qt, обращаются к системным вызовам ядра (например fopen(..), open(..), mkdir(..), access(..), unlink(..)
и т.д.), которые требуют в качестве своих параметров C-like строку, а при вызове этой функции ей в качестве фактических параметров передаеются строки типа QString
. В случае вызова функций fopen(..), open(..), mkdir(..), access(..), unlink(..)
, из-за этой ошибки невозможно работать с файлами и директориями, содержащими национальные символы в именах.
Что нужно делать, чтобы исключить ошибки связанные с неявным преобразованием типов ?
Совет первый
Разработчики Qt рекомендуют для гарантии того, что в программе используется "чистый" UNICODE, указывать при компиляции программ ключи -DQT_NO_CAST_ASCII
и -DQT_NO_ASCII_CAST
. Применение этих ключей запретит преобразования по умолчанию из const char *
в QString
и обратно. Вообще с использованием ключа -DQT_NO_CAST_ASCII
будет запрещено любое неявное приведени типов из const char *
в QString
и для всех остальных методов класса QString
.
После того как будет запрещено неявное приведение типов, при компиляции рассмотренных выше примеров, будет выдана ошибка о невозможности преобразования типов. И необходимо будет явно использовать один из методов класса QString
: fromLatin1/fromLocal8Bit/fromUtf8
в зависимости от ситуации. Например вместо фрагмента программы:
для того, чтобы получить гарантию того, что в программе используется чистый UNICODE, нужно использовать следующий фрагмент кода:
1: QString qstr1 = QString::fromLocal8Bit("Привет мир!!!");
2: QString qstr2 = QString::fromLatin1("Hello world!!!");
3: char *buff;
4: ...
5: qstr2 = qstr1 + QString::fromLatin1(" ") + qstr2;
6: qstr1 = QString::fromLocal8Bit(buff);
7: ...
Небольшое замечание: Применение метода QString::fromLatin1(...)
для преобразования однобайтовой строки в UNICODE-строку, оправдано исключительно в случае, если однобайтовая строка содержит только символы в кодировке ISO-8859-1. В примерах строки 2 и 5, действительно строки "Hello world!!!" и " " содержат только ISO-8859-1 символы. В строке 1, строка "Привет мир!!!" содержит только русские символы, поэтому мы используем метод QString::fromLocal8Bit(...)
. В строке 6 переменная buff может содержать не только латинские символы, поэтому наиболее безопасное решение - применять метод QString::fromLocal8Bit(...)
. Это даст гарантию того, что все символы из строки buff будут корректно преобразованы в UNICODE.
В некоторых случаях бывает невозможно использовать ключи -DQT_NO_CAST_ASCII
и -DQT_NO_ASCII_CAST
при компиляции программы. Например, если вы используете другую билиотеку, базирующуюся на Qt, которая разрабатывалась без использования ключей -DQT_NO_CAST_ASCII
и -DQT_NO_ASCII_CAST
при компиляции. В этом случае, вам нужно придерживаться советов, рассмотренных ниже, они помогут избежать ошибок. Но если вы начинаете разрабатывать свою собственную программу с использованием библиотеки Qt, то обязательно используйте ключи -DQT_NO_CAST_ASCII
и -DQT_NO_ASCII_CAST
при компиляции программы.
Совет второй
При проектировании программы нужно обязательно определить в каком формате будут храниться и обрабатываться строки: в однобайтовом или UNICODE формате. И в дальнешем, при программировании, стараться внутри функций и классов не смешивать однобайтовые и UNICODE строки. Т.е. если некоторый класс содержит несколько членов данных с текстовой информацией, нужно стараться чтобы все эти члены данные были одного типа, а для доступа к этим членам данным использовать перегружаемые методы, которые, в свою очередь, гарантируют корректность преобразования, даже если при компиляции не заданы ключи -DQT_NO_CAST_ASCII
и -DQT_NO_ASCII_CAST
. Например:
Наличие перегруженных конструкторов и методов setD1/setD2
в этом классе гарантирует, что внутри этого класса всегда будет использоваться "чистый" UNICODE, независимо от того, каким образом мы инициализируем объекты класса Foo
:
Еще более безопасное решение - добавление в класс Foo
двух конструкторов:
1: ...
2: Foo( const QString & d1, const char * d2 )
3: {
4: m_d1 = d1;
5: m_d2 = QString::fromLocal8Bit(d2);
6: };
7: Foo( const char * d1, const QString & d2 )
8: {
9: m_d1 = QString::fromLocal8Bit(d1);
10: m_d2 = d2;
11: };
12: ...
13: QString s1 = QString::fromLocal8Bit("Привет");
14: Foo f(s1,"мир!!!");
15: ...
Если мы не будем перегружать конструкторы и методы setD1/setD2
, то мы получим все те ошибки, о которых говорилось выше:
В реальных программах кроме отдельных строк очень часто приходится использовать еще и списки строк. В библиотеке Qt для представления списков строк исоользуются два класса QStringList
и QStrList
. Первый представляет строки типа QString
, второй строки типа QCString
. В этой ситуации самой распространенной ошибкой, связанной с преобразованием строк, является попытка добавить или преобразовать строку типа QString
к списку типа QStrList
, и наоборот извлечь строку из списка QStrList
в переменную типа QString
. Это справедливо относительно строк типа QCString
и списков QStringList
. Поэтому использованию списков строк тоже нужно уделять особое внимание.
Конечно, как говорилось выше, использование ключей -DQT_NO_CAST_ASCII
и -DQT_NO_ASCII_CAST
при компиляции, защитит от таких ошибок, но к сожалению не всегда возможно применение этих ключей.
В заключении этой темы, нужно отметить: Т.к. библиотека Qt внутри ориентированна на использование UNICODE строк, то использование класса QString
вместо char *
и QCString
для передставления строк внутри программы, а также использование класса QStringList
вместо класса QStrList
для представления списков строк будет самым безопасным решением с точки зрения появления ошибок.
Совет третий
Если в своей программе вы используете системные вызовы для доступа к файлам или директориям (например fopen(..), open(..), mkdir(..), access(..), unlink(..)
и т.д.) более безопасным решением для преобразования имени файла/директории из QString
в const char *
использовать методы класса QFile
:
static QCString QFile::encodeName( const QString & fileName );
- Преобразует имя файла, представленное в формате UNICODE типом
QString
, в однобайтовое представление типа QCString
с использованием специфики файловой системы. По умолчанию используется преобразование QCString QString::local8Bit() const
, но может быть переопределено с помощью метода static void QFile::setEncodingFunction( EncoderFn )
.
static QString decodeName( const QCString & localFileName );
- Преобразует имя файла, представленное в однобайтовом формате типа
QCString
, в представление UNICODE типа QString
с использованием специфики файловой системы. По умолчанию используется преобразование QString QString::fromLocal8Bit(const char*, int)
, но может быть переопределено с помощью метода static void QFile::setDecodingFunction( DecoderFn )
.
Например:
1: int renameFile( const QString & old, const QString & new )
2: {
3: ::rename( QFile::encodeName(old), QFile::encodename(new) );
4: }
5: ...
6: QStringList readDir( const QString & dName )
7: {
8: DIR *dp = 0L;
9: struct dirent *ep;
10: QStringList dList;
11:
12: dp = opendir( QFile::encodeName(dName) );
13: if ( dp )
14: {
15: while ( ep=readdir( dp ) )
16: {
17: dList.append(QFile::decodeName(ep->d_name));
18: }
19: closedir( dp );
20: }
21: return dList;
22: }
23: ...
Совет последний
Если нужно проверить является строка, представленная типом QString
или QCString
, пустой или нулевой, нужно использовать методы:
-
bool QString::isEmpty() const
,
bool QCString::isEmpty() const
- Проверка на пустую строку. Под строку отведена память, но данных нет. Если для стоки выполняется условие
str.isEmpty()==TRUE
, то это не означает, что будет выполняться и условие str.isNull()==TRUE
.
-
bool QString::isNull() const
,
bool QCString::isNull() const
- Проверка на нулевую строку. Под строку не отведена память. Если для строки выполняется условие
str.isNull()==TRUE
, то обязательно будет выполняться условие str.isEmpty()==TRUE
.
Ситуация вторая.
В программах написанных с ипользованием языка C++ очень часто используют потоки (streams) для ввода/вывода текстовых данных из/в строки, из/в файла или какого-либо другого устройства ввода/вывода. Действительно, с помощью потока доступ к буферу (строке), файлу или другому устройству организуется одинаково.
В обычных тестовых файлах подавляющего большинства операционных систем, информация представлена в виде однобайтовых строк в кодировке, определяемой установками локали. В некоторых случаях в тестовых файлах информация может быть представлена представлена в виде однобайтовых строк в кодировке UTF8, реже в виде двухбайтовых строк в кодировке UNICODE.
В библиотеке Qt для этих целей используется класс QTextStream
. Но т.к. библиотека Qt внутри работает со строками в формате UNICODE, то этот класс имеет свои особенности. Эти особенности мы рассмотрим ниже.
В качестве примера использования класса QTextStream
можно привести следующий фрагмент кода:
Если файл TheFile содержит текст, представленный ниже, т.е. текстовые однобайтовые строки в кодировке, определяемой установками локали:
1: Строка 1
2: Строка 2
3: Строка 3
то соответственно в результате выполнения этого фрагмента кода содержимое файла будет напечатано без искажений.
Для демонстрации момента, в котором возникает искажение национальных символов, приведем другой пример (в этом примере я намеренно использую класс QStrList
):
В этом примере в поток вставляются однобайтовые строки. В результате выполнения этого фрагмента кода, файл TheFile, будет содержать строки:
1: ?????? 1
2: ?????? 2
3: ?????? 3
Если вместо класса QStrList
использовать класс QStringList
или вместо файла связать поток с классом QCString
(QByteArray
), то строки будут выводиться правильно. Почему так происходит ?
Разработчики библиотеки Qt реализовали класс QTextStream
следующим образом: предполагается, что в файле, связанном с потоком, текст хранится в виде однобайтовых строк в кодировке, определенной для текущей локали. Поэтому при создании потока, связанного с файлом, устанавливается флаг Encoding == QTextStream::Locale с помощью метода QTextStream::setEncoding( QTextStream::Encoding e)
c параметром e = QTextStream::Locale.
При выводе однобайтового символа в поток применяется следующее преобразование: QChar <= unsigned short <= int <= char
(Смотри реализацию методов QTextStream::ts_putc(...)
и QTextStream::writeBlock( const char*, uint)
). В результате такого преобразования русская буква 'А' (код в ISO-8859-5 - 0xB0) получает UNICODE код U+00B0.
В методе QTextStream::ts_putc(QChar)
при непосредственной записи в файл происходит преобразование к однобатовому символу с помощью метода QTextCodec::fromUnicode(...)
. Если мы вставляем в поток, связанный с файлом, строки типа QString
, то преобразование выполняется правильно и русские буквы выводятся без искажений. Если мы вставляем в поток однобайтовые строки, то преобразование выполняется некорректно (с точки зрения использования UNICODE) и русские символы искажаются.
В свою очередь, для текстового потока QTextStream
, связанного с однобайтовой строкой (QByteArray
или QCString), устанавливается флаг Encoding == QTextStream::Latin1, который выполняет преобразование из UNICODE в однобайтовую строку по следующему алгоритму:
Если мы вставляем в поток, связанный с однобайтовой строкой, строку типа QString
то выполняется метод QTextStream::operator<<( const QString & s )
, который для Encoding == QTextStream::Latin1 производит преобразование 'Русская буква' ==> '?'. Для символов в кодировках ISO-8859-1 или US-ASCII преобразования не имеют эффекта, коды этих символов не меняются: в UNICODE старший байт равен 0, младший байт равен коду символа. Для национальных символов в UNICODE старший байт всегда отличен от 0 и в общем случае младший байт не совпадает с однобайтовым кодом символа для кодировки определенной в локали, и такие строки подвергаются изменениям. Если тестировать программы, используя только строки в кодировке ISO-8859-1 или US-ASCII, то ошибки преобразования "UNICODE <==> однобайтовая строка" не видны, т.к. фактически никаках преобразоаний не происходит.
Что нужно делать, чтобы при использовании QTextStream не искажались национальные символы ?
Для того чтобы избежать искажения национальных символов при использовании класса QTextStream
, нужно придерживаться простого правила: после создания потока нужно явно установить для него тип кодирования текста с помощь метода QTextStream::setEncoding( QTextStream::Encoding e)
.
Наш пример будет выглядеть следубщим образом:
В случае если мы используем поток, связанный с однобайтовой строкой, и в этот поток вставляем строки типа QString
, то нужно указать Encoding == QTextStream::Locale:
1: ...
2: QCString buff;
3: QString s;
4: QStringList strList;
5:
6: strList.append(QString::fromLocal8Bit("Строка 1"));
7: strList.append(QString::fromLocal8Bit("Строка 2"));
8: strList.append(QString::fromLocal8Bit("Строка 3"));
9:
10: QTextStream t( & buff );
11: t.setEncoding( QTextStream::Locale );
12: //^^^Сообщаем потоку, что UNICODE строки нужно преобразовывать
13: // в соответствии с установками локали
14: for ( QStringList::Iterator it = strList.begin(); it != strList.end(); ++it )
15: {
16: t << *it << QString::fromLatin1("+");
17: }
18: ...
В результате выполнения этого фрагмента кода переменная buff получит значение "Строка 1+Строка 2+Строка 3". Если мы уберем из примера строку 11, то переменная buff получит значение "?????? 1+?????? 2+?????? 3".
Все выше сказанное можно сформулировать в виде следующих правил:
-
Если в поток типа QTextStream
с помощью оператора вставки в поток (QTextStream::operator<<(...)
) будут вставляться однобайтовые строки в кодировке, определяемой установками локали или UTF8, то перед вызовом оператора вставки в поток необходимо установить тип кодирования текстовой информации в значение QTextStream::Latin1 с помощью вызова метода QTextStream::setEncoding( QTextStream::Encoding e)
.
-
Если в поток типа QTextStream
с помощью оператора вставки в поток (QTextStream::operator<<(...)
) будут вставляться строки типа QString
, то перед вызовом оператора вставки в поток необходимо явно установить тип кодирования текстовой информации c помощью вызова метода QTextStream::setEncoding( QTextStream::Encoding e)
.
Тип кодирования текстовой информации долженен принимать следующие значения:
- QTextStream::Locale
- Если необходимо чтобы при записи UNICODE строк в обычный текстовый файл выполнялось преобразование этих строк в однобайтовые строки с учетом кодировки, определяемой установками локали.
- QTextStream::UnicodeUTF8
- Если необходимо чтобы при записи UNICODE строк в обычный текстовый файл выполнялось преобразование этих строк в однобайтовые строки в кодировке UTF8.
-
QTextStream::Unicode,
QTextStream::UnicodeNetworkOrder,
QTextStream::UnicodeReverse,
QTextStream::RawUnicode
- Если необходимо чтобы при записи UNICODE строк в файл не выполнялось преобразование этих строк в однобайтовые строки. Подробнее об особенностях этих значений можно прочитать в справочной системе библиотеки Qt.
-
Если поток используется с одним типом строк, то тип кодирования текстовой информации можно задавать только один раз - после создания потока. Если поток используется с разными типами строк, то тип кодирования текстовой информации можно задавать каждый раз перед измением типа строк. Например:
1: ...
2: QCString mbs = "Строка 1", buff;
3: QString wcs = QString::fromLocal8Bit("Строка 2");
4:
5: QTextStream t( & buff );
6:
7: t.setEncoding( QTextStream::Latin1 );
8: //^^^Сообщаем потоку, что однобайтовые строки преобразовывать
9: // не нужно
10: t << mbs;
11:
12: t.setEncoding( QTextStream::Locale );
13: //^^^Сообщаем потоку, что UNICODE строки нужно преобразовывать
14: // в соответствии с установками локали
15: t << wcs;
16:
17: ...
-
Все вышесказанное справедливо и для оператора извлечения из потока QTextStream::operator>>(...)
.
Патчи, исправляющие описанные проблемы в KDE 2.1.1, вы можете скачать здесь.
1: ...
2: QFile file(QFile::decodeName("TheFile"));
3: QString s;
4: QStrList strList;
5:
6: strList.append("Строка 1");
7: strList.append("Строка 2");
8: strList.append("Строка 3");
9:
10: if (file.open(IO_WriteOnly))
11: {
12: QTextStream t( & file);
13: t.setEncoding( QTextStream::Latin1 );
14: //^^^Сообщаем потоку, что однобайтовые строки преобразовывать
15: // не нужно
16: QStrListIterator it(strList);
17:
18: const char * tmp;
19: while ( (tmp=it.current()) )
20: {
21: ++it;
22: t << tmp << "\n";
23: }
24: file.close();
25: }
26: ...
1: QChar c;
2:
3: if( c.row() ) // Если старший октет отличен от нуля
4: {
5: dev->putch( '?' ); // Выводим символ '?'
6: }
7: else
8: {
10: dev->putch( c.cell() ); // Младший октет
11: }
1: ...
2: QFile file(QFile::decodeName("TheFile"));
3: QString s;
4: QStrList strList;
5:
6: strList.append("Строка 1");
7: strList.append("Строка 2");
8: strList.append("Строка 3");
9:
10: if (file.open(IO_WriteOnly))
11: {
12: QTextStream t( & file);
13: QStrListIterator it(strList);
14:
15: const char * tmp;
16: while ( (tmp=it.current()) )
17: {
18: ++it;
19: t << tmp << "\n";
20: }
21: file.close();
22: }
23: ...
1: ...
2: QFile file(QFile::decodeName("TheFile"));
3: QString s;
4:
5: if (file.exists())
6: {
7: if (file.open(IO_ReadOnly))
8: {
9: QTextStream t(&file);
10:
11: while ((s=t.readLine()) != QString::null)
12: {
13: qWarning((const char *)s.local8Bit());
14: }
15: file.close();
16: }
17: }
18: ...
1: class Foo
2: {
3: public:
4: Foo( const Foo & foo )
5: {
6: m_d1 = foo.m_d1;
7: m_d2 = foo.m_d2;
8: };
9: Foo( const QString & d1, const QString & d2 = QString::null )
10: {
11: m_d1 = d1;
12: m_d2 = d2;
13: };
14: vod setD1( const QString & d1 )
15: {
16: m_d1 = d1;
17: };
18: vod setD2( const QString & d2 )
19: {
20: m_d2 = d2;
21: };
22: QString do()
23: {
24: return m_d1 + QString::fromLatin1(" ") + m_d2;
25: };
26: private:
27: QString m_d1;
28: QString m_d2;
29: };
30:
31: Foo f1( "Привет","мир!!!" ); // Неявное преобразование
32: QString s1 = QString::fromLocal8Bit("Привет");
33: Foo f2;
34:
35: f2.setD1( s1 );
36: f2.setD2( "мир!!!" ); // Неявное преобразование
37: qWarning("%s", (const char *)f1.do().local8Bit());
38://^^ Напечатает строку : ?????? ???!!!
39: qWarning("%s", (const char *)f2.do().local8Bit());
40://^^ Напечатает строку : Привет ???!!!
1: Foo f1( "Привет","мир!!!" );
2: QString s1 = QString::fromLocal8Bit("Привет");
3: Foo f2;
4:
5: f2.setD1( s1 );
6: f2.setD2( "мир!!!" );
7: qWarning("%s", (const char *)f1.do().local8Bit());
8://^^ Напечатает строку : Привет мир!!!
9: qWarning("%s", (const char *)f2.do().local8Bit());
10://^^ Напечатает строку : Привет мир!!!
1: class Foo
2: {
3: public:
4: Foo( const Foo & foo )
5: {
6: m_d1 = foo.m_d1;
7: m_d2 = foo.m_d2;
8: };
9: Foo( const QString & d1, const QString & d2 = QString::null )
10: {
11: m_d1 = d1;
12: m_d2 = d2;
13: };
14: Foo( const char * d1, const char * d2 = NULL )
15: {
16: m_d1 = QString::fromLocal8Bit(d1);
17: m_d2 = d2 ? QString::fromLocal8Bit(d2) : QString::null;
18: };
19: vod setD1( const QString & d1 )
20: {
21: m_d1 = d1;
22: };
23: void setD1( const char * d1 )
24: {
25: m_d1 = QString::fromLocal8Bit(d1);
26: };
27: vod setD2( const QString & d2 )
28: {
29: m_d2 = d2;
30: };
31: void setD2( const char * d2 )
32: {
33: m_d2 = QString::fromLocal8Bit(d2);
34: };
35: QString do()
36: {
37: return m_d1 + QString::fromLatin1(" ") + m_d2;
38: };
39: private:
40: QString m_d1;
41: QString m_d2;
42: };
1: QString qstr1("Привет мир!!!"); // Ошибка !!!
2: QString qstr2 = "Hello world!!!"; // Ошибка !!!
3: char *buff;
4: ...
5: qstr2 = qstr1 + " " + qstr2; // Ошибка !!!
6: qstr1 = buff; // Ошибка !!!
7: ...
1: class Q_EXPORT QString
2: {
3: ...
4: operator const char *() const { return latin1(); }
5: ...
6: };
1: #include
2:
3: void foo( const char * str )
4: {
5: qWarning("%s",str);
6: }
7:
8: int main( int argc, char *argv[] )
9: {
10: QString qstr = QString::fromLocal8Bit( "Привет мир!!!" );
11: //^^ Инициализируем qstr в "чистый" UNICODE
12:
13: foo( qstr );
14: //^^ Будет напечатана пустая строка
15: }
1: #include
2:
3: void foo( const QString & str )
4: {
5: qWarning("%s",(const char *)str.local8Bit());
6: }
7:
8: int main( int argc, char *argv[] )
9: {
10: foo( "Привет мир!!!" );
11: //^^ Будет напечатана строка : ?????? ???!!!
12: }
1: char *str = "Привет мир!!!"
2: QTextCodec *c;
3: QString wstr;
4:
5: wstr = str;
6: qWarning("%s", (const char *)wstr.local8Bit() );
7: //^^ Будет напечатано : ?????? ???!!!
8: qWarning("%s", wstr.latin1() );
9: //^^ Будет напечатано : Привет мир!!!
10:
11: wstr = QString::fromLocal8Bit(str);
12: qWarning("%s", (const char *)c->fromUnicode(wstr) );
13: //^^ Будет напечатано : Привет мир!!!
14: // c->fromUnicode(wstr) еквивалентно wstr.local8Bit()
15: qWarning("%s", (const char *)wstr.latin1() );
16: //^^ Будет напечатана пустая строка
1: char *str = "Привет мир!!!"
2: QTextCodec *c;
3: QString wstr;
4:
5: wstr = str;
6: qWarning("%s", (const char *)wstr.local8Bit() );
7: //^^ Будет напечатано : ?????? ???!!!
8: qWarning("%s", wstr.latin1() );
9: //^^ Будет напечатано : Привет мир!!!
10:
11: c = QTextCodec::codecForLocale();
12: // или c = QTextCodec::codecForName("ISO-8859-5");
13: if ( c )
14: {
15: wstr = c->toUnicode(str);
16: qWarning("%s", (const char *)c->fromUnicode(wstr) );
17: //^^ Будет напечатано : Привет мир!!!
18: // c->fromUnicode(wstr) эквивалентно wstr.local8Bit()
19: qWarning("%s", (const char *)wstr.latin1() );
20: //^^ Будет напечатана пустая строка
21: }
22: else
23: {
24: qWarning("Кодек не найден");
25: }