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