Использование 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.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" и она настроена):

  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(..), из-за этой ошибки невозможно работать с файлами и директориями, содержащими национальные символы в именах.

Что нужно делать, чтобы исключить ошибки связанные с неявным преобразованием типов ?

Совет первый

Разработчики После того как будет запрещено неявное приведение типов, при компиляции рассмотренных выше примеров, будет выдана ошибка о невозможности преобразования типов. И необходимо будет явно использовать один из методов класса 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, нужно придерживаться простого правила: после создания потока нужно явно установить для него тип кодирования текста с помощь метода 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".

Все выше сказанное можно сформулировать в виде следующих правил:

  1. Если в поток типа QTextStream с помощью оператора вставки в поток (QTextStream::operator<<(...)) будут вставляться однобайтовые строки в кодировке, определяемой установками локали или UTF8, то перед вызовом оператора вставки в поток необходимо установить тип кодирования текстовой информации в значение QTextStream::Latin1 с помощью вызова метода QTextStream::setEncoding( QTextStream::Encoding e).

  2. Если в поток типа 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.

  3. Если поток используется с одним типом строк, то тип кодирования текстовой информации можно задавать только один раз - после создания потока. Если поток используется с разными типами строк, то тип кодирования текстовой информации можно задавать каждый раз перед измением типа строк. Например:

      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:  ...
    									

  4. Все вышесказанное справедливо и для оператора извлечения из потока QTextStream::operator>>(...).

Патчи, исправляющие описанные проблемы в KDE 2.1.1, вы можете скачать здесь.