Qt
Qt - C++ фреймворк для работы с графическим интерфейсом (в основном).
Как устроены Qt-программы?
Так выглядит пустое окно:
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
QMainWindow w;
w.show();
return a.exec();
}
Функция exec
внутри достаёт сообщения из очереди (в очередь кладутся ивенты типа кликов мыши, нажатий на клавиатуру и т.д.) и обрабатывает их. По умолчанию exec
выходит, когда закрывается последнее окно, основное время программа проводит внутри этой функции.
Добавим кнопку:
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
QMainWindow w;
QPushButton* button = new QPushButton(&w);
button->setText("hello world");
w.show();
return a.exec();
}
В Qt все элементы интерфейса называются виджетами (Qt Widgets). Помимо способа создавать их руками (как написано выше), виджеты можно создавать в редакторе (встроен в Qt Creator), который просто генерирует .xml
файлы. Вместе с Qt поставляется UI-compiler, который транслирует эти .xml
в плюсовый код.
QObject
Если посмотреть на иерархию Qt-шных объектов, то многие из них наследуются от QObject
. Одна из вещей, которую он предоставляет - иерархия parent-child. У каждого объекта может быть какой-то parent и несколько child-объектов. Когда удаляется child, то он отписывается от своего parent'a, а parent при удалении удаляет все свои дочерние объекты. Именно поэтому в примере выше не нужен delete
, так как за удаление QPushButton button
ответственен его parent - QMainWindow w
.
Можно не использовать это и при создании объекта не указывать у него parent. Кроме того, можно явно удалять объекты, у которых указан parent. Это может быть удобно, если к долгоживущему объекту привязан короткоживущий, то без явного удаления он будет жить, пока жив родительский объект, что можно считать мемори-ликом.
Кроме функционала владения, QObject
предоставляет сигналы и слоты
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget* parent = nullptr) :
QMainWindow(parent),
ui(new Ui::MainWindow) {
ui->setupUi(this);
connect(ui->pushButton, &QPushButton::clicked, this,
&MainWindow::buttonClicked);
}
~MainWindow() {
delete ui;
}
private slots:
void buttonClicked() {
ui->plainTextEdit->setPlainText("hello, world!");
}
signals:
void mysignal;
private:
Ui::MainWindow* ui;
}
Можно заметить, что в Qt много своих макросов (Q_OBJECT
, slots
, signals
). Некоторые из них, например, slots
и signals
раскрываются в ничего/public:
. В Qt есть Meta-Object Compiler, который генерит вместо этих макросов дополнительный код. Во многом, это всё пережиток прошлого, потому что Qt писалась ещё до появления шаблонов в языке, кроме того, MOC всё равно нужен для внутренностей Qt (reflection и др.), а сигналы в таком виде компилируются быстрее (по сравнению с известными попытками переписать это на современный C++). Кстати, в QObject
встроенный механизм слотов разрывает connection
между сигналом и слотом, если один из них умирает, поэтому не нужно делать вручную disconnect
.
У QObject
есть ещё одно интересно свойство - они знают, какому потоку они принадлежат (thread affinity
). Основная операция, которая от этого зависит - операция connect
. У неё есть параметр ConnectionType
, который по умолчанию AutoConnection
. Но у неё могут быть и другие значения:
- AutoConnection - если таргет живёт в потоке, который эмитит сигнал, используется DirectConnection, иначе QueuedConnection.
- DirectConnection - при эмите сигнала вызывает все слоты. Слот выполняется в потоке сигнала.
- QueuedConnection - вместо прямого вызова функции, в очередь сообщений потока, которому принадлежит таргет, добавляется сообщение о том, что нужно вызвать слот (требуется, чтобы в том потоке крутился event loop).
- BlockingQueuedConnection - добавляет в очередь и ждёт, пока отработает.
Пример: факторизация чисел
Дальше на лекции рассматривается пример программы, которая считает факторизацию введённого в формочку числа. Лучше посмотреть запись, это небольшой пересказ происходящего:
qfactor1
В qfactor1
при вводе числа считается его факторизация в том же потоке. Факторизация тут является блокирующей операцией, поэтому UI фризится, пока она не досчитается, а все ивенты, происходящие в это время, просто кладутся в очередь.
qfactor2
В qfactor2
попробовали это пофиксить, сделав отдельный поток (в данном случае это QThread
, который позволяет создать поток и крутить в нём event (message) loop), запрос на факторизацию и результат передаются с помощью сигналов.
Стоит обратить внимание на worker_obj.moveToThread(&worker_thread)
- это привязывает объект к другому потоку, что сказывается на работе коннекта. Теперь UI перестал фризить, но есть другая проблема - если какое-то число уже начало считаться, то следующие введённые не будут считаться, пока не досчитаются предыдущие. Хоть и UI перестаёт зависать, лучше от этого не стало - особенно это проявляется в том, что при закрытии UI, рабочий поток продолжает считать факторизации. Вывод: нужно как-то отменять вычисления.
Ещё стоит обратить внимание, что невалидные запросы в коде тоже добавляются в очередь. Можно было бы сразу на них выводить ошибку и делать return
, но тогда может произойти следующее: в очередь добавилось большое число, пока оно считалось, ввели неверный запрос и получили ошибку в выводе, а затем только пришёл и вывелся ответ на большое число.
qfactor3
В qfactor3
применяется следующая идея: сделали класс factoring_worker
, который внутри себя хранит поток, конструктор его запускает, деструктор отменяет. У него есть функции set_input
и get_output
(по названиям понятно, что они делают). Понятно, что get_output
можно вызывать, только когда результат готов, для этого делаем сигнал output_changed
, который сообщает о готовности результата. Можно заметить ещё следующую оптимизацию: в каждый момент в очереди событий для UI хранится не более одного аутпута (для этого используется булевый флаг notify_output_changed
), благодаря этому очередь событий UI-потока не забивается уведомлениями о результате, что могло бы приводить к фризам.
qfactor4
Наконец, в qfactor4
применяется следующая идея. В качестве примера можно посмотреть на такую программу:
void MainWindow::buttonClicked() {
QString filename = QFileDialog::getOpenFileName(this, tr("Open File"));
if (!filename.isEmpty()) {
QMessageBox::information(this, "Demo file", "File Selected");
} else {
QMessageBox::warning(this, "Demo file" "No file selected")
}
}
Функция getOpenFileName
крутит свой встроенный event (message) loop, открывая диалог выбора файла (он называется модальным), до закрытия которого не происходит выхода из функции.
Теперь, зная такую идею, попробуем усовершенствовать программу с факторизацией чисел - проблема в qfactor1
была именно в том, что не прокручивался event loop. В Qt есть функция, которая называется processEvents
, она пампит event loop и передаёт обработчику сообщения, если они там есть. Можно воткнуть её во все циклы в qfactor1
и тогда UI перестанет фризить, но вылезет следующий баг: какие-то ответы могут начать заменяться более старыми (понятно, почему - внутри факторизации числа x
вызовется факторизация числа y
, введённого позже, выведется ответ на неё, а потом перезапишется ответом на x
) - напоминает проблему reentrancy. Можно пофиксить это, делая проверку после processEvents
и отменять текущую факторизацию, если пришёл какой-то ивент на новые входные данные.
Итог: часто используется способ, как в qfactor3
, потому что код как в qfactor4
может показаться непривычным и раздутым, хотя в некоторых случаях он несложный и успешно подходит (например, в этом примере с факторизацией).