Автор : Сукиязов Сергей Александрович (2001г.)
Эта статья содержит информацию об одной из популярных библиотек для организации графического пользовательского интерфейса (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, как говорилось выше, внутренний формат строк - 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();
Возвращает указатель объект который наиболее подходит для набора символов, используемого в текущей установке локали. Про настройки локали можно прочитать в статье "Локализация, как она есть". (http://www.sensi.org/~alec/locale/index.html#toc)
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" и она настроена):
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: }
Разберем подробнее приведенный пример.
В строке 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
приведенный выше пример можно изменить следующим образом:
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: //^^ Будет напечатана пустая строка
Разработчики 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 строка:
1: #include <qstring.h> 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: }
В этом примере, в строке 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
:
1: #include <qstring.h> 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: }
В этом примере в строке 13 выполняется неявное преобразование типов из QString
в const char *
с использованием оператора преобразования типа QString::operator const char *() const;
, который определен в файле qstring.h
как:
1: class Q_EXPORT QString 2: { 3: ... 4: operator const char *() const { return latin1(); } 5: ... 6: };
Как видно из определения оператора 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
в зависимости от ситуации. Например вместо фрагмента программы:
1: QString qstr1("Привет мир!!!"); // Ошибка !!! 2: QString qstr2 = "Hello world!!!"; // Ошибка !!! 3: char *buff; 4: ... 5: qstr2 = qstr1 + " " + qstr2; // Ошибка !!! 6: qstr1 = buff; // Ошибка !!! 7: ...
для того, чтобы получить гарантию того, что в программе используется чистый 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
. Например:
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: };
Наличие перегруженных конструкторов и методов setD1/setD2
в этом классе гарантирует, что внутри этого класса всегда будет использоваться "чистый" UNICODE, независимо от того, каким образом мы инициализируем объекты класса Foo
:
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://^^ Напечатает строку : Привет мир!!!
Еще более безопасное решение - добавление в класс 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
, то мы получим все те ошибки, о которых говорилось выше:
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://^^ Напечатает строку : Привет ???!!!
В реальных программах кроме отдельных строк очень часто приходится использовать еще и списки строк. В библиотеке 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
можно привести следующий фрагмент кода:
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: ...
Если файл TheFile содержит текст, представленный ниже, т.е. текстовые однобайтовые строки в кодировке, определяемой установками локали:
1: Строка 1 2: Строка 2 3: Строка 3
то соответственно в результате выполнения этого фрагмента кода содержимое файла будет напечатано без искажений.
Для демонстрации момента, в котором возникает искажение национальных символов, приведем другой пример (в этом примере я намеренно использую класс QStrList
):
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: ...
В этом примере в поток вставляются однобайтовые строки. В результате выполнения этого фрагмента кода, файл 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 в однобайтовую строку по следующему алгоритму:
1: QChar c; 2: 3: if( c.row() ) // Если старший октет отличен от нуля 4: { 5: dev->putch( '?' ); // Выводим символ '?' 6: } 7: else 8: { 10: dev->putch( c.cell() ); // Младший октет 11: }
Если мы вставляем в поток, связанный с однобайтовой строкой, строку типа 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::setEncoding( QTextStream::Encoding e)
.
Наш пример будет выглядеть следубщим образом:
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: ...
В случае если мы используем поток, связанный с однобайтовой строкой, и в этот поток вставляем строки типа 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)
. 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>>(...)
.