Журнал LinuxFormat - перейти на главную

LXF79:Qt/KDE

Материал из Linuxformat
Перейти к: навигация, поиск

Parlez-vous Fran ais? Ваше приложение в любой момент могут запустить в интернет-кафе где нибудь на Монмартре, поэтому говорить и выглядеть нужно соответствующе. Оказывается, это вовсе не так уж трудно – следуйте за Андреем Боровским.

В мире, где приложения распространяются через Интернет и становятся моментально доступны пользователям по всему земному шару, качественно выполненная интернационализация может существенно способствовать успеху продукта. Пример буквально у нас перед глазами. Как вы думаете, велико ли было бы число пользователей Linux, если бы эта система не обрела многоязычный интерфейс? Но интернационализация приложения – задача не простая и проблема тут не только в том, что надо знать иностранные языки. Когда величайшие мудрецы человечества учили свои народы грамоте, они, к сожалению, не предвидели, что в далеком будущем их потомки будут вводить текст с клавиатуры. Мало того, что в одних языках пишут слева направо, а в других – справа налево, начертание символов в некоторых языках (например, арабском) зависит от соседних символов, так что при подсчете ширины текста в пикселях приходится учитывать правописание. Мы упростим себе задачу и рассмотрим интернационализацию на примере европейских языков. Напишем небольшую программу, которая в дальнейшем подвергнется интернационализации:

 #include <qapplication.h>
 #include <qpushbutton.h>
 int main(int argc, char *argv[])
 {
  QApplication i18nApp(argc, argv);
  QPushButton button(QObject::tr("Exit"), NULL);
  QObject::connect(&button, SIGNAL(clicked()), &i18nApp, SLOT(quit()));
  i18nApp.setMainWidget(&button);
  button.show();
  return i18nApp.exec();
 }

Главным и единственным визуальным элементом является кнопка button. Ее сигнал clicked связан со слотом quit объекта i18nApp, так что щелчок по кнопке приводит к завершению работы приложения. Единственным текстом, подлежащим интернационализации, является надпись на кнопке – «Exit». По сравнению с предыдущим уроком, в программу добавился лишь один новый элемент – вызов статического метода tr() класса QObject. Метод tr() выполняет замену переданной ему строки на перевод, соответствующий локализации системы, если, конечно, эта строка содержится в ресурсах программы. Таким образом, нам нужно решить две задачи: создать файлы ресурсов (по одному файлу для каждого поддерживаемого языка), необходимые для работы метода tr() и добавить в программу код, отвечающий за загрузку этих ресурсов во время ее выполнения. На практике этот процесс состоит из нескольких шагов. Сохраните текст программы в файле app2.1.cpp, и вызовите пос ледовательность команд qmake, необходимую для генерации make-фай ла. Помимо самого make-файла, будет сгенерирован файл проекта с расширением .pro. В него-то и нужно добавить ссылки на файлы, которые будут содержать ресурсы интернационализации. Откроем файл проекта в текстовом редакторе и добавим в него следующие строки:

 TRANSLATIONS=app21_ru.ts\
  app21_fr.ts

Мы создали в файле проекта раздел TRANSLATIONS (переводы) и добавили в этот раздел ссылки на два (пока еще не существующих) исходных файла переводов – app21_ru.ts и app21_fr.ts, для русского и французского языков, соответственно. Имена этих файлов могут быть любыми, но очень удобно включать в них в качестве суффикса или префикса название соответствующей локали (ru – для русской и fr – для французской) – дальше мы увидим, почему. Если вы захотите перевести интерфейс программы и на другие языки, вам понадобятся дополнительные файлы с именами, соответствующими той же схеме. После того, как мы добавили ссылки на файлы перевода в файл проекта, мы можем сгенерировать сами файлы с помощью утилиты lupdate.

В окне консоли дайте команду:

lupdate app2.1.pro

где app2.1.pro – файл проекта. В результате будут созданы файлы app21_ru.ts и app21_fr.ts. Теперь, когда у нас появились заготовки файлов переводов, мы можем приступить непосредственно к процессу. Для этой цели нам понадобится графическая утилита Qt Linguist.


Интерфейс этой утилиты самоочевиден. Запустив Qt Linguist, мы последовательно открываем созданные нами файлы с расширением .ts и выбираем в левом окне объект создаваемой программы, предназначенный для перевода. При этом в главном окне утилиты отображаются все связанные с объектом строковые ресурсы. В нашем случае мы имеем дело только с одним объектом-потомком QObject, у которого есть только один строковый ресурс – «Exit». В файле app21_ru.ts мы сопоставляем строке «Exit» перевод «Выход», после чего сохраняем файл. Аналогичные операции мы выполняем с файлом app21_fr.ts, в котором переводим строку «Exit» как «Quiter». Итак, у нас есть два файла, содержащие перевод, но наша работа на этом далеко не закончена. Созданные нами файлы с расширением *.ts можно рассматривать как исходные тексты файлов ресурсов перевода. Эти ресурсы еще нужно скомпилировать с помощью утилиты lrelease (те, кто выполнял интернационализацию приложений в версиях Qt 2.x, могут заметить, что порядок вызова утилит изменился). В окне терминала дайте команду:

lrelease app2.1.pro

В результате у нас появятся два новых файла: app21_ru.qm и app21_fr.qm. Это и есть двоичные файлы ресурсов интернационализации. Для завершения интернационализации нашей маленькой программы нам осталось только добавить в нее код, выполняющий загрузку этих ресурсов во время выполнения. Рассмотрим теперь текст нашей программы целиком:

#include <qapplication.h>
#include <qpushbutton.h>
#include <qtranslator.h>
#include <qtextcodec.h>
int main(int argc, char *argv[])
{
 QApplication i18nApp(argc, argv);
 QTranslator translator(0);
 QString fileName = QString("app21_") + QTextCodec::locale();
 translator.load(fileName, ".");
 i18nApp.installTranslator(&translator);
 QPushButton button(QObject::tr("Exit"), NULL);
 QObject::connect(&button, SIGNAL(clicked()), & i18nApp, SLOT(quit()));
 i18nApp.setMainWidget(&button);
 button.show();
 return i18nApp.exec();
}

Класс QTranslator, объявленный в файле qtranslator.h, загружает файлы ресурсов интернационализации во время выполнения программы и управляет переводом интерфейса на соответствующий язык. Как определить, какой файл ресурсов должен быть загружен? Класс QTextCodec обладает статическим методом locale(), который возвращает строку, содержащую имя текущей локали. Поскольку мы использовали имя локали в качестве суффикса в именах файлов ресурсов, для получения имени нужного файла мы просто добавляем имя локали к шаблону «app21_» и сохраняем результат в переменной fileName. Выбранный файл ресурса с расширением qm загружается с помощью метода load объекта translator. Метод installTranslator() класса QApplication устанавливает объект, выполняющий перевод интерфейса приложения. Теперь метод QObject::tr() может выполнять свою работу. Кстати, возьмите себе за правило использовать tr() для всех строк, которые может увидеть пользователь, даже если на текущем этапе вы не планируете интернационализацию приложения – это здорово упростит вашу задачу в дальнейшем. Конечно, если вы работаете со строкой внутри объекта-потомка QObject (а так, скорее всего, и будет), QObject::tr() можно сократить до tr().

На этом интернационализация программы закончена. Если вы все сделали правильно, то после запуска в окне терминала скомпилированной программы в русскоязычной среде Qt вы увидите текст кнопки на русском языке. Программа будет «говорить» по-русски в русскоязычной локализации Qt, по-французски – во французской, а во всех остальных локализациях будет использоваться английский текст, заданный по умолчанию. Обратите внимание, что файлы ресурсов интернационализации не становятся частью исполняемого файла приложения – их следует распространять вместе с ним. Это разумно, поскольку для проекта, переведенного на многие языки, таких файлов может быть много, а в каждой конкретной системе понадобится только несколько (как правило, – всего один) из них. Файлы интернационализации должны располагаться в рабочей директории программы, вот почему при отладке программу следует запускать из терминала, а не из графической оболочки (для которой лучше всего создать специальный ярлык).


Одна из проблем, которую мы не затронули в нашем простом примере заключается в том, что утилита lupdate обрабатывает только тот текст, который помечен для перевода (в качестве «маркера» может выступать вызов метода tr). Но как отметить статический текст, заданный вне тела функции, например, строковые константы? Для этого в Qt реализованы специальные макросы QT_TR_NOOP и QT_TRANSLATE_NOOP. Эти макросы служат маркерами для lupdate, причем более длинный вариант макроса позволяет определить так называемый контекст для константы, тогда как более короткий предполагает текущий контекст.


Управление внешним видом приложения

Интерфейс любого «настоящего» приложения содержит множество визуальных элементов, расположенных в главном и вспомогательных окнах наиболее удобным для пользователя образом. До сих пор мы сталкивались только с одним элементом управления расположением визуальных элементов – классом QVBox, который располагает дочерние элементы вертикально друг под другом. Библиотека Qt предоставляет также классы QHBox и QGrid, позволяющие расположить дочерние элементы соответственно горизонтально и в виде таблицы. Хотя, возможно, это и не очевидно, указанные классы могут служить основой самого сложного интерфейса. Наглядный пример, – редактор OpenOffice.org, в котором я пишу этот текст. В первом приближении интерфейс редактора содержит пять элементов, расположенных вертикально: строку главного меню, две панели инструментов (их может быть и больше, и меньше), окно ввода текста и строку состояния в нижней части окна. Фактически, основой интерфейса для такой сложной программы мог бы стать элемент QVBox! Конечно, в реальности все не так просто, и QVBox не обладает всей необходимой функциональностью. Однако этот пример демонстрирует основную идею – построение сложных интерфейсов начинается с простых элементов управления. Мы напишем приложение просмотра шрифтов, обладающее довольно сложным интерфейсом и для этого тоже начнем с простого элемента – главного окна, которое станет контейнером для всех графических элементов приложения. Такое окно предоставляет класс QDialog. Вопреки своему названию, этот класс создает простое окно общего назначения, а не так называемое «диалоговое окно».


Разработка приложений Qt немыслима без объектно-ориентированного программирования, к которому теперь прибегнем и мы. Весь интерфейс и логика работы нашего приложения будут содержаться в классе MainForm, потомке класса QDialog. Чтобы стало понятно, что мы делаем, лучше всего сразу взглянуть на главное окно нашей программы. В окне также отмечены границы элементов интерфейса, о которых речь пойдет ниже.

Декларацию главного класса MainForm сохраним в файле app2.2.h:

#ifndef APP2_2_H
#define APP2_2_H
#include <qdialog.h>
#include <qlayout.h>
#include <qspinbox.h>
#include <qcombobox.h>
#include <qvgroupbox.h>
#include <qcheckbox.h>
#include <qlabel.h>
class MainForm : public QDialog
{
 Q_OBJECT
public:
 MainForm( QWidget* parent = 0, const char* name = "", bool modal = FALSE, WFlags f = 0 );
 ~MainForm() {}
private:
 QVBoxLayout * MainFormLayout;
 QHBoxLayout * ChooseFontLayout;
 QHBoxLayout * ViewTextLayout;
 QComboBox * SelectFontBox;
 QSpinBox * SetSize;
 QVGroupBox * SetFontGroup;
 QCheckBox * SetBold;
 QCheckBox * SetItalic;
 QLabel * TextLabel;
private slots:
 void setLabelFont();
};
#endif // APP2_2_H

а реализацию методов – в файле app2.2.cpp:

#include <qfontdatabase.h>
#include <qfont.h>
#include "app2.2.h"
 MainForm::MainForm(QWidget* parent, const char* name, bool modal, WFlags f) : QDialog(parent, name, modal, f)
{
 setCaption(trUtf8("Просмотр шрифтов"));
 resize(300, 200);
 MainFormLayout = new QVBoxLayout(this, 5, 5);
 ChooseFontLayout = new QHBoxLayout(MainFormLayout, 4);
 SelectFontBox = new QComboBox(FALSE, this);
 ChooseFontLayout->addWidget(SelectFontBox, 0, Qt::AlignTop);
 SetSize = new QSpinBox(8, 72, 1, this);
 SetSize->setPrefix(trUtf8("Высота "));
  SetSize->setValue(12);
  ChooseFontLayout->addWidget(SetSize, 0, Qt::AlignTop);
  QSizePolicy fixedSP = QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed, FALSE);
  SetSize->setSizePolicy(fixedSP);
  ViewTextLayout = new QHBoxLayout(MainFormLayout, 4);
  SetFontGroup = new QVGroupBox(this);
  SetFontGroup->setTitle(trUtf8("Стиль"));
  SetFontGroup->setSizePolicy(fixedSP);
  SetBold = new QCheckBox(SetFontGroup);
  SetBold->setText(trUtf8("Полужирный"));
  SetItalic = new QCheckBox(SetFontGroup);
  SetItalic->setText(trUtf8("Курсив"));
  ViewTextLayout->addWidget(SetFontGroup, 0, Qt::AlignLeft | Qt::AlignTop);
  TextLabel = new QLabel(this);
  TextLabel->setAlignment(Qt::AlignLeft);
  TextLabel->setText(trUtf8("В чащах юга жил бы цитрус?\nДа, но фальшивый экземпляр."));
  ViewTextLayout->addWidget(TextLabel, 0, Qt::AlignTop);
  MainFormLayout->addStretch(1);
  connect(SelectFontBox, SIGNAL(activated(int)), this, SLOT(setLabelFont()));
  connect(SetSize, SIGNAL(valueChanged(int)), this, SLOT(setLabelFont()));
  connect(SetBold, SIGNAL(toggled(bool)), this, SLOT(setLabelFont()));
  connect(SetItalic, SIGNAL(toggled(bool)), this, SLOT(setLabelFont()));
  QFontDatabase fontDatabase;
  SelectFontBox->insertStringList(fontDatabase.families());
  setLabelFont();
 }
 void MainForm::setLabelFont()
 {
  QString family = SelectFontBox->currentText();
  int size = SetSize->value();
  int weight = QFont::Normal;
  if(SetBold->isChecked()) weight = QFont::DemiBold;
  bool italic = SetItalic->isChecked();
  QFont font(family, size, weight, italic);
  TextLabel->setFont(font);
 }


Рассмотрим конструктор класса MainForm. Он передает свои параметры конструктору базового класса. Уже в первой строке конструктора мы видим новый метод: trUtf8(). Этот метод представляет собой вариант уже знакомого нам статического метода tr(). Метод trUtf8() заменяет строку в кодировке Utf8 соответствующей ей строкой перевода в кодировке Utf16 и обычно используется для интернационализации приложений, но в данном случае мы применим его с другой целью. Хотя создаваемое нами приложение, безусловно, может претендовать на всемирную популярность, заниматься его интернационализацией мы не будем: вместо этого мы сразу вводим весь поясняющий текст интерфейса на русском языке. Однако при этом возникает одна проблема, характерная для дистрибутивов SUSE, Fedora и их производных. По умолчанию в этих дистрибутивах используется кодировка Utf8, и для того, чтобы русский текст отображался правильно, необходимо явным образом перекодировать его в кодировку Utf16, которую использует Qt. В решении этой проблемы нам на помощь приходит метод trUtf8(). Как уже упоминалось, этот метод выполняет перевод интерфейса. Но если ресурс для перевода отсутствует, trUtf8() просто перекодирует строку из Utf8 в Utf16, используя статический метод fromUtf8. Эту особенность метода trUtf8 мы и используем для перекодировки всего статического текста в нашей программе. Естественно, если бы весь текст набирался на латинице, проблема с кодировками не возникла бы. В дистрибутивах, использующих другие кодировки, явное перекодирование не потребуется.

Далее мы создаем объект MainFormLayout класса QVBoxLayout. Класс QVBoxLayout сам по себе не представляет визуального элемента. Этот класс относится к менеджерам компоновки (layout managers) и его задача заключается в управлении расположением дочерних визуальных компонентов на родительской форме. Название класса QVBoxLayout похоже на название класса QVBox, и это не случайно. Класс QVBoxLayout управляет дочерними элементами также как QVBox, то есть располагает их вертикально, один под другим. Класс QHBoxLayout выстраивает элементы горизонтально. Почему мы используем классы QDialog и QVBoxLayout, а не класс QVBox? Преимущество использования QVBoxLayout (как и других классов –Layout) заключается в том, что мы можем использовать одновременно несколько таких классов для одного окна. Далее мы увидим, как это делается. Конструктору объекта MainFormLayout передается ссылка на окно MainForm (this). Таким образом, мы сообщаем классу, что он будет управлять расположением элементов данного окна.

Взгляните на рис. 4 справа. На нем можно выделить две вертикально расположенные группы, в каждой из которых два элемента интерфейса (на рисунке эти группы обведены синими контурами). В верхней группе расположены раскрывающийся список и наборный счетчик (spinbox), в нижней – флажки и текстовая метка. Чтобы получить эти группы, мы создаем два объекта класса QНBoxLayout для горизонтального расположения элементов и делаем их дочерними элементами MainFormLayout, чтобы сами группы располагались вертикально (область, которой управляет MainFormLayout, обведена красным). Сразу после создания «вертикального менеджера» компоновки MainFormLayout мы создаем дочерний по отношению к нему «горизонтальный» менеджер ChooseFontLayout. В конструкторе классов QHBoxLayout и QVBoxLayout мы, помимо прочего, указываем расстояние в пикселях между элементами, находящимися под управлением данных менеджеров. Далее мы создаем объекты классов QComboBox (раскрывающийся список), QSpinBox и с помощью метода addWidget() объекта ChooseFontLayout передаем их «под управление» данного объекта. Обратите внимание на флаг Qt::AlignTop, который мы передаем методу addWidget. Этот флаг указывает на привязку визуального элемента к верхнему краю родительского окна. В результате при изменении размеров окна положение элемента относительно верхнего края не изменится. Другие флаги из этой группы включают Qt::AlignLeft, Qt::AlignRight, Qt::AlignBottom и Qt::AlignCenter.

Далее мы выполняем еще одну операцию, связанную с расположением элементов. Наши раскрывающийся список и счетчик будут выстроены горизонтально объектом класса ChooseFontLayout. Этот объект позаботится о том, чтобы у элементов, расположением которых он управляет, была одинаковая ширина, и чтобы при изменении размеров окна эта ширина увеличивалась пропорционально. Но нас такое поведение не устраивает. Ширина раскрывающегося списка может меняться вместе с шириной окна, но ширину счетчика желательно оставить такой, какой она была сразу после его создания. Как изменить поведение элементов интерфейса? В процессе создания визуальных объектов Qt автоматически выбирает для них оптимальную ширину, соответствующую так называемой «политике изменения размера» (size policy), заданной по умолчанию. Политика изменения размера представляет собой набор правил, определяющих изменение высоты и ширины элемента интерфейса при изменении размера его родительского окна. «Политика изменения размера» раскрывающегося списка, заданная по умолчанию, нас устраивает, а вот для счетчика лучше установить другую «политику», задающую минимально допустимую (с учетом ширины поля ввода и длины поясняющего текста) ширину, которая не должна меняться при изменении размеров окна. Для этого мы создаем объект fixedSP специального класса QSizePolicy, инкапсулирующего «политику изменения размера». В конструкторе объекта мы указываем, что ширина и высота, определяемые создаваемой «политикой», должны быть фиксированы. Далее, с помощью метода setSizePolicy(), мы указываем объекту SetSize новую «политику». Теперь при изменении размеров окна ширина счетчика изменяться не будет, а раскрывающийся список SelectFontBox займет все пространство слева от него.

Далее мы создаем второй объект класса QHBoxLayout и «упаковываем» в него группу флажков и текстовую метку. Группа флажков представлена объектом SetFontGroup класса QVGroupBox. Этот класс представляет собой стандартную группу кнопок (напомню, что флажки QCheckBox представляют собой разновидность кнопок – сам класс QCheckBox является потомком класса QButton), снабженную заголовком и рамкой. Буква V в имени класса указывает, что дочерние кнопки будут располагаться вертикально. Для объекта SetFontGroup мы указываем ту же «фиксированную» политик изменения размера, что и для счетчика. Добавить флажки в группу очень просто – для этого в конструкторе каждого объекта класса QCheckBox нужно указывать в качестве родительского элемента SetFontGroup. Далее мы добавляем SetFontGroup в число элементов управляемых ViewTextLayout. Из визуальных элементов нам осталось добавить только метку, что мы и делаем.

Далее следует еще один трюк работы с интерфейсом. С помощью метода addStretch() мы добавляем пустое пространство, указав ему фактор растяжения (stretch factor), равный единице. Поскольку у двух объектов QHBoxLayout этот фактор равен 0 (значение по умолчанию) при изменении высоты окна вся нижняя часть будет занята пустым пространством (на рис. 3 эта область заполнена зеленой штриховкой), а элементы управления окажутся «прижатыми» кверху (поэтом stretch иногда называют «пружиной» или «распоркой»). Чтобы лучше понять принцип действия этого механизма, закомментируйте строку addStretch(), откомпилируйте приложение и затем попробуйте изменить размер окна.


Теперь на время покинем конструктор и посмотрим на декларацию класса MainForm. В классе мы объявляем новый слот. В этом нет ничего сложного, ведь слоты, – это обыкновенные методы классов. Нужно только указать ключевое слово slots после спецификатора видимости и не забыть макрос Q_OBJECT в самом начале объявления класса [Конечно сам класс при этом должен быть прямым или непрямым наследником QObject, – прим.ред.]. Наш слот setLabelFont() будет реагировать на сигналы, поступающие от всех элементов управления программы. Когда состояние одного из элементов меняется, метод setLabelFont считывает значения всех элементов управления, конструирует объект класса QFont (шрифт) и передает новый шрифт метке. В конструкторе MainForm мы связываем слот с четырьмя сигналами, информирующими нас об изменении состояния соответствующих элементов управления. Далее мы создаем список установленных в системе шрифтов с помощью объекта класса QFontDataBase и передаем список названий шрифтов объекту SelectFontBox с помощью метода insertStringList(). Наконец мы явным образом вызываем setLabelFont() для того, чтобы установить для метки шрифт, соответствующий высоте и стилю, заданным по умолчанию (имя этого шрифта будет соответствовать первому шрифт в списке QFontDataBase). Ну вот и все... Нет, не все! Мы забыли самую малость, – функцию main для нашей программы. Создадим файл main.cpp следующего содержания:

#include <qapplication.h>
#include "app2.2.h"
int main(int argc, char *argv[])
{
 QApplication app(argc, argv);
 MainForm mf(0, "MF");
 app.setMainWidget(&mf);
 mf.show();
 return app.exec();
}

вот теперь – действительно всё.

Наш далеко не самый сложный пример демонстрирует, кроме прочего, насколько трудоемким может быть программирование интерфейса. В следующей статье мы познакомимся с Qt Designer – инструментом, упрощающим процесс построения GUI, а также с несколькими полезными классами библиотеки Qt.

Персональные инструменты
купить
подписаться
Яндекс.Метрика