Использование файла пользовательского интерфейса дизайнера в вашем приложении
Файлы пользовательского интерфейса Qt Designer представляют собой дерево виджетов формы в формате XML. Формы могут обрабатываться:
- На этапе компиляции, что означает, что формы преобразуются в код на C++, который можно скомпилировать.
- Во время выполнения, что означает, что формы обрабатываются классом QUiLoader, который динамически создает дерево виджетов во время разбора XML-файла.
Обработка формы на этапе компиляции
Вы создаете компоненты пользовательского интерфейса с помощью Qt Designer и используете интегрированные инструменты построения Qt, qmake и uic, для генерации кода для них при построении приложения. Сгенерированный код содержит объект пользовательского интерфейса формы. Это C++ структура, которая содержит:
- Указатели на виджеты формы, макеты, элементы макета, группы кнопок и действия.
- Функцию-член, называемую
setupUi(), для построения дерева виджетов на родительском виджете. - Функцию-член, называемую
retranslateUi(), которая обрабатывает перевод строковых свойств формы. Дополнительную информацию см. в разделе Реагирование на изменения языка.
Сгенерированный код можно включить в ваше приложение и использовать его напрямую. Кроме того, вы можете использовать его для расширения подклассов стандартных виджетов.
Обработанная на этапе компиляции форма может быть использована в вашем приложении с помощью одного из следующих подходов:
- Прямой подход: вы создаете виджет в качестве места для размещения компонента и настраиваете пользовательский интерфейс внутри него.
- Подход одиночного наследования: вы подклассифицируете базовый класс формы (QWidget или QDialog, например) и включаете частный экземпляр объекта пользовательского интерфейса формы.
- Подход множественного наследования: вы подклассифицируете как базовый класс формы, так и объект пользовательского интерфейса формы. Это позволяет напрямую использовать виджеты, определенные в форме, внутри области подкласса.
Для демонстрации мы создаем приложение Simple Calculator Form. Оно основано на оригинальном примере Calculator Form.
Приложение состоит из одного исходного файла, main.cpp и файла пользовательского интерфейса.
Файл calculatorform.ui, разработанный с помощью Qt Designer, показан ниже:
Мы будем использовать qmake для построения исполняемого файла, поэтому нам нужно написать файл .pro.
HEADERS = calculatorform.h
Особенностью этого файла является объявление FORMS, которое сообщает qmake, какие файлы обработать с помощью uic. В данном случае, файл calculatorform.ui используется для создания файла ui_calculatorform.h, который может использоваться любым файлом, перечисленным в объявлении SOURCES.
Примечание: Вы можете использовать Qt Creator для создания проекта Calculator Form. Он автоматически генерирует файлы main.cpp, UI и .pro, которые затем можно изменить.
Прямой подход
Для использования прямого подхода мы включаем файл ui_calculatorform.h непосредственно в main.cpp.
#include "ui_calculatorform.h"
Функция main создает виджет калькулятора, создавая стандартный виджет QWidget, который мы используем для размещения пользовательского интерфейса, описанного в файле calculatorform.ui.
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget *widget = new QWidget;
Ui::CalculatorForm ui;
ui.setupUi(widget);
widget->show();
return app.exec();
} В этом случае Ui::CalculatorForm — это объект описания интерфейса из файла ui_calculatorform.h, который настраивает все виджеты диалога и связи между его сигналами и слотами.
Прямой подход обеспечивает быстрый и простой способ использования простых, автономных компонентов в ваших приложениях. Однако компоненты, созданные с помощью Qt Designer, часто требуют тесной интеграции с остальным кодом приложения. Например, код CalculatorForm выше скомпилируется и запустится, но объекты QSpinBox не будут взаимодействовать с QLabel, так как нам нужен пользовательский слот для выполнения операции сложения и отображения результата в QLabel. Для этого нам нужно использовать подход одиночного наследования.
Подход одиночного наследования
Для использования подхода одиночного наследования мы подклассифицируем стандартный виджет Qt и включаем частный экземпляр объекта пользовательского интерфейса формы. Это может быть:
- Переменная-член
- Указатель на переменную-член
Использование переменной-члена
В этом подходе мы подклассифицируем виджет Qt и настраиваем пользовательский интерфейс внутри конструктора. Компоненты, используемые таким образом, предоставляют виджеты и макеты, используемые в форме, подклассу виджета Qt и обеспечивают стандартную систему для создания соединений между пользовательским интерфейсом и другими объектами вашего приложения. Сгенерированная структура Ui::CalculatorForm является членом класса.
Этот подход используется в примере Calculator Form.
Для того, чтобы мы могли использовать пользовательский интерфейс, нам нужно включить заголовочный файл, который uic генерирует, прежде чем ссылаться на Ui::CalculatorForm:
#include "ui_calculatorform.h"
Это означает, что файл .pro должен быть обновлен для включения calculatorform.h:
HEADERS = calculatorform.h
Подкласс определен следующим образом:
class CalculatorForm : public QWidget
{
Q_OBJECT
public:
explicit CalculatorForm(QWidget *parent = nullptr);
private slots:
void on_inputSpinBox1_valueChanged(int value);
void on_inputSpinBox2_valueChanged(int value);
private:
Ui::CalculatorForm ui;
}; Важной особенностью класса является частный объект ui, который предоставляет код для настройки и управления пользовательским интерфейсом.
Конструктор подкласса создает и настраивает все виджеты и макеты диалога, просто вызвав функцию ui объекта setupUi(). После этого можно изменять пользовательский интерфейс по мере необходимости.
CalculatorForm::CalculatorForm(QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);
} Мы можем соединять сигналы и слоты в виджетах пользовательского интерфейса стандартным способом, добавляя префикс on_<имя объекта>. Дополнительную информацию см. в разделе widgets-and-dialogs-with-auto-connect.
Преимущества этого подхода заключаются в простом использовании наследования для предоставления интерфейса на основе QWidget и инкапсуляции переменных виджетов пользовательского интерфейса в члене данных ui. Мы можем использовать этот метод для определения нескольких пользовательских интерфейсов в одном виджете, каждый из которых находится в своем пространстве имен, и наложить (или составить) их. Например, этот подход можно использовать для создания отдельных вкладок из существующих форм.
Использование указателя на переменную-член
В качестве альтернативы, структура Ui::CalculatorForm может быть сделана указателем на член класса. Заголовок тогда выглядит следующим образом:
namespace Ui {
class CalculatorForm;
}
class CalculatorForm : public QWidget
...
virtual ~CalculatorForm();
...
private:
Ui::CalculatorForm *ui;
... Соответствующий исходный файл выглядит следующим образом:
#include "ui_calculatorform.h"
CalculatorForm::CalculatorForm(QWidget *parent) :
QWidget(parent), ui(new Ui::CalculatorForm)
{
ui->setupUi(this);
}
CalculatorForm::~CalculatorForm()
{
delete ui;
} Преимущество этого подхода состоит в том, что объект пользовательского интерфейса можно объявлять вперед, что означает, что нам не нужно включать сгенерированный файл ui_calculatorform.h в заголовок. Затем форму можно изменять без перекомпиляции зависимых исходных файлов. Это особенно важно, если класс ограничен ограничениями двоичной совместимости.
Мы обычно рекомендуем этот подход для библиотек и больших приложений. Дополнительную информацию см. в разделе Создание библиотек общего использования.
Подход множественного наследования
Формы, созданные с помощью Qt Designer, могут быть подклассифицированы вместе со стандартным классом QWidget. Этот подход делает все компоненты пользовательского интерфейса, определенные в форме, напрямую доступными внутри области подкласса и позволяет устанавливать соединения между сигналами и слотами обычным способом с помощью функции connect().
Этот подход используется в примере Multiple Inheritance.
Нам нужно включить заголовочный файл, который uic генерирует из файла calculatorform.ui следующим образом:
#include "ui_calculatorform.h"
Класс определяется аналогично классу, используемому в подходе одиночного наследования, за исключением того, что на этот раз мы наследуем от обоих QWidget и Ui::CalculatorForm, следующим образом:
class CalculatorForm : public QWidget, private Ui::CalculatorForm
{
Q_OBJECT
public:
explicit CalculatorForm(QWidget *parent = nullptr);
private slots:
void on_inputSpinBox1_valueChanged(int value);
void on_inputSpinBox2_valueChanged(int value);
}; Мы наследуем Ui::CalculatorForm в частном порядке, чтобы гарантировать, что объекты пользовательского интерфейса являются частными в нашем подклассе. Мы также можем унаследовать его с помощью ключевых слов public или protected аналогичным образом, как мы могли бы сделать ui общедоступным или защищенным в предыдущем случае.
Конструктор подкласса выполняет многие те же задачи, что и конструктор, используемый в примере одиночного наследования:
CalculatorForm::CalculatorForm(QWidget *parent)
: QWidget(parent)
{
setupUi(this);
} В этом случае к виджетам, используемым в пользовательском интерфейсе, можно получить доступ так же, как к виджету, созданному вручную в коде. Мы больше не требуем префикса ui для доступа к ним.
Реагирование на изменения языка
Qt уведомляет приложения, если меняется язык пользовательского интерфейса, отправляя событие типа QEvent::LanguageChange. Для вызова функции-члена retranslateUi() объекта пользовательского интерфейса мы переопределяем QWidget::changeEvent() в классе формы следующим образом:
void CalculatorForm::changeEvent(QEvent *e)
{
QWidget::changeEvent(e);
switch (e->type()) {
case QEvent::LanguageChange:
ui->retranslateUi(this);
break;
default:
break;
}
} Обработка формы во время выполнения
В качестве альтернативы формы могут обрабатываться во время выполнения, создавая динамически сгенерированные пользовательские интерфейсы. Это можно сделать, используя модуль QtUiTools, который предоставляет класс QUiLoader для обработки форм, созданных с помощью Qt Designer.
Подход UiTools
Для обработки форм во время выполнения требуется файл ресурсов, содержащий файл пользовательского интерфейса. Кроме того, приложение должно быть настроено для использования модуля QtUiTools. Это делается путем включения следующего объявления в файл проекта qmake, гарантируя, что приложение будет скомпилировано и связано должным образом.
QT += uitools
Класс QUiLoader предоставляет объект загрузчика формы для построения пользовательского интерфейса. Этот пользовательский интерфейс может быть получен из любого QIODevice, например, объекта QFile, для получения формы, хранящейся в файле ресурсов проекта. Функция QUiLoader::load() строит виджет формы, используя описание пользовательского интерфейса, содержащегося в файле.
Классы модуля QtUiTools можно включить с помощью следующей директивы:
#include <QtUiTools>
Функция QUiLoader::load() вызывается, как показано в этом коде из примера Поиск текста:
static QWidget *loadUiFile(QWidget *parent)
{
QFile file(":/forms/textfinder.ui");
file.open(QIODevice::ReadOnly);
QUiLoader loader;
return loader.load(&file, parent);
} В классе, использующем QtUiTools для построения пользовательского интерфейса во время выполнения, мы можем находить объекты в форме с помощью QObject::findChild(). Например, в следующем коде мы находим некоторые компоненты на основе их имен объектов и типов виджетов:
ui_findButton = findChild<QPushButton*>("findButton");
ui_textEdit = findChild<QTextEdit*>("textEdit");
ui_lineEdit = findChild<QLineEdit*>("lineEdit"); Обработка форм во время выполнения даёт разработчику свободу изменения пользовательского интерфейса программы просто путём изменения файла пользовательского интерфейса. Это полезно при настройке программ под различные потребности пользователей, такие как дополнительные крупные значки или другая цветовая схема для поддержки доступности.
Автоматические соединения
Соединения сигналов и слотов, определённые для форм времени компиляции или времени выполнения, могут быть настроены вручную или автоматически, используя возможность QMetaObject устанавливать соединения между сигналами и слотами с подходящими именами.
Обычно, в QDialog, если мы хотим обработать введённую пользователем информацию перед её принятием, нам нужно соединить сигнал clicked() из кнопки ОК со своим пользовательским слотом в нашем диалоге. Мы сначала покажем пример диалога, в котором слот соединён вручную, затем сравним его с диалогом, использующим автоматическое соединение.
Диалог без автосоединения
Мы определяем диалог так же, как и раньше, но теперь включаем слот в дополнение к конструктору:
class ImageDialog : public QDialog, private Ui::ImageDialog
{
Q_OBJECT
public:
ImageDialog(QWidget *parent = 0);
private slots:
void checkValues();
}; Слот checkValues() будет использоваться для проверки введённых пользователем значений.
В конструкторе диалога мы настраиваем виджеты, как и прежде, и соединяем сигнал clicked() кнопки Отмена со слотом reject() диалога. Мы также отключаем свойство autoDefault в обеих кнопках, чтобы гарантировать, что диалог не вмешивается в то, как поле ввода обрабатывает события возврата:
ImageDialog::ImageDialog(QWidget *parent)
: QDialog(parent)
{
setupUi(this);
okButton->setAutoDefault(false);
cancelButton->setAutoDefault(false);
...
connect(okButton, SIGNAL(clicked()), this, SLOT(checkValues()));
} Мы соединяем сигнал clicked() кнопки ОК со слотом checkValues() диалога, который реализуется следующим образом:
void ImageDialog::checkValues()
{
if (nameLineEdit->text().isEmpty())
(void) QMessageBox::information(this, tr("No Image Name"),
tr("Please supply a name for the image."), QMessageBox::Cancel);
else
accept();
} Этот пользовательский слот выполняет минимум необходимых действий для обеспечения того, что введённые пользователем данные являются корректными — он принимает ввод только в том случае, если для изображения было указано имя.
Виджеты и диалоги с автосоединением
Хотя легко реализовать пользовательский слот в диалоге и соединить его в конструкторе, мы можем вместо этого использовать возможности автоматического соединения QMetaObject для соединения сигнала clicked() кнопки ОК со слотом в нашем подклассе. uic автоматически генерирует код в функции setupUi() диалога для этого, поэтому нам нужно только объявить и реализовать слот с именем, следуя стандартной конвенции:
void on_<object name>_<signal name>(<signal parameters>);
Используя эту конвенцию, мы можем определить и реализовать слот, который реагирует на щелчки мыши по кнопке ОК:
class ImageDialog : public QDialog, private Ui::ImageDialog
{
Q_OBJECT
public:
ImageDialog(QWidget *parent = 0);
private slots:
void on_okButton_clicked();
}; Ещё один пример автоматического соединения сигналов и слотов — это Поиск текста со своим слотом on_findButton_clicked().
Мы используем систему QMetaObject для включения соединений сигналов и слотов:
QMetaObject::connectSlotsByName(this);
Это позволяет нам реализовать слот, как показано ниже:
void TextFinder::on_findButton_clicked()
{
QString searchString = ui_lineEdit->text();
QTextDocument *document = ui_textEdit->document();
bool found = false;
// undo previous change (if any)
document->undo();
if (searchString.isEmpty()) {
QMessageBox::information(this, tr("Empty Search Field"),
tr("The search field is empty. "
"Please enter a word and click Find."));
} else {
QTextCursor highlightCursor(document);
QTextCursor cursor(document);
cursor.beginEditBlock();
...
cursor.endEditBlock();
if (found == false) {
QMessageBox::information(this, tr("Word Not Found"),
tr("Sorry, the word cannot be found."));
}
}
} Автоматическое соединение сигналов и слотов предоставляет как стандартную систему именования, так и явный интерфейс для работы с разработчиками интерфейсов виджетов. Предоставляя исходный код, реализующий данный интерфейс, разработчики пользовательских интерфейсов могут проверить, что их дизайн действительно работает, не написав код самостоятельно.
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/archives/qt-5.11/designer-using-a-ui-file.html