Оптимизация GUI на Qt
Как правило, при создании desktop-приложений на платформе Qt не возникает проблем, связанных с медленностью работы GUI. Qt – платформа достаточно надежная, неплохо вылизанная по всем параметрам, в том числе и по скорости работы. Однако всё же иногда бывают ситуации, когда из-за обилия виджетов графический интерфейс немного притормаживает, и это печально). В этой статье я приведу один частный пример простого графического интерфейса и покажу, как за два шага можно сначала ускорить его в 11 раз, а потом и в целых 34 раза. Вдобавок к этому, я постараюсь немного осветить механизм принятия решения для таких оптимизационных задач, постараюсь показать направление мыслей для правильного решения. Поехали!Сразу оговорюсь, я не буду пытаться показать самый оптимальный код и самое быстрое решение. Я буду показывать лишь то решение, которое оказывается достаточным в плане скорости, и которое требует довольно небольшой переделки кода.
Итак, задачу в студию!
Нам надо нарисовать в две колонки список параметров: имя и значение. Параметров много. Они могут быть сгруппированы в группы с общим заголовком. Таким образом, у нас будет виджет с небольшой шириной, но с большой высотой. Его, конечно, стоит поместить в QScrollArea. Собственно, вот этот виджет:

Код виджета был написан когда-то давно, быстро и просто, и он содержал в себе намек на будущее расширение функциональности, то есть, говоря по-честному, некоторую функциональную избыточность. Вот этот код.
Код:
#pragma once
#include <QScrollArea>
#include <QFormLayout>
class FormLayoutWgt : public QScrollArea
{
Q_OBJECT
public:
FormLayoutWgt(QWidget* parent = 0);
virtual ~FormLayoutWgt();
typedef QList< QPair<QString, QWidget*> > WidgetList;
void setContents(const WidgetList& widgetList);
QSize sizeHint() const;
public slots:
void clear();
private:
QFormLayout* _pLayout;
};
Код:
#include "FormLayoutWgt.h"
FormLayoutWgt::FormLayoutWgt(QWidget* parent)
: QScrollArea(parent)
{
QWidget* pWidget = new QWidget;
setWidget(pWidget);
setWidgetResizable(true);
_pLayout = new QFormLayout;
_pLayout->setLabelAlignment(Qt::AlignLeft);
_pLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
pWidget->setLayout(_pLayout);
}
FormLayoutWgt::~FormLayoutWgt()
{
clear();
}
void FormLayoutWgt::clear()
{
if (_pLayout != 0)
{
QLayoutItem* item;
for (int i = 0; i < _pLayout->rowCount(); i++)
{
item = _pLayout->itemAt(i, QFormLayout::LabelRole);
if (item != 0) delete item->widget();
item = _pLayout->itemAt(i, QFormLayout::FieldRole);
if (item != 0) delete item->widget();
}
int count = _pLayout->rowCount();
for (int i = 0; i < count; i++)
_pLayout->removeRow(0);
}
}
void FormLayoutWgt::setContents(const WidgetList& widgetList)
{
for (int i = 0; i < widgetList.size(); i++)
{
if (widgetList.at(i).first.isEmpty())
_pLayout->addRow(widgetList.at(i).second);
else
_pLayout->addRow(widgetList.at(i).first, widgetList.at(i).second);
}
}
QSize FormLayoutWgt::sizeHint() const
{
return QSize(270, 200);
А вот примерный сценарий использования такого виджета.
Код:
#pragma once
#include "FormLayoutWgt.h"
class MainWindow : public QWidget
{
Q_OBJECT
public:
MainWindow(QWidget *parent = Q_NULLPTR);
public slots:
void fill();
private:
FormLayoutWgt::WidgetList generateContents() const;
FormLayoutWgt* _flw;
};
Код:
#pragma once
#include "FormLayoutWgt.h"
class MainWindow : public QWidget
{
Q_OBJECT
public:
MainWindow(QWidget *parent = Q_NULLPTR);
public slots:
void fill();
private:
FormLayoutWgt::WidgetList generateContents() const;
FormLayoutWgt* _flw;
};
Файл MainWindow.cpp
#include "MainWindow.h"
#include <QVBoxLayout>
#include <QPushButton>
MainWindow::MainWindow(QWidget *parent)
: QWidget(parent)
{
QVBoxLayout* layout = new QVBoxLayout();
setLayout(layout);
_flw = new FormLayoutWgt();
QPushButton* fill = new QPushButton("Fill");
QPushButton* clear = new QPushButton("Clear");
layout->addWidget(fill);
layout->addWidget(clear);
layout->addWidget(_flw);
connect(clear, SIGNAL(released()), _flw, SLOT(clear()));
connect(fill, SIGNAL(released()), SLOT(fill()));
}
FormLayoutWgt::WidgetList MainWindow::generateContents() const
{
FormLayoutWgt::WidgetList widgetList;
for (int i = 0; i < 300; i++)
{
widgetList << qMakePair(QString(), new QLabel(QString("<H3>Group%1</H3>").arg(i + 1)));
widgetList << qMakePair(QString("Field1"), new QLabel("Value1"));
widgetList << qMakePair(QString("Field2"), new QLabel("Value2 long long long long"));
widgetList << qMakePair(QString("Field3\n"), new QLabel("Value3 \n two rows"));
widgetList << qMakePair(QString(), new QLabel("==========================="));
}
return widgetList;
}
void MainWindow::fill()
{
auto content = generateContents();
_flw->setContents(content);
}
Получили вот такое тестовое мини-приложение, которое и хотим ускорить:

Код виджета был написан когда-то давно, когда число параметров было небольшое – до нескольких десятков. И он исправно и быстро работал, но до тех пор, пока число параметров не возросло до нескольких сотен (или даже тысяч). GUI стало визуально притормаживать. Общее впечатление пользователя от мгновенно работающего приложения стало немного смазываться. И, в принципе, не стоит винить в этом старый код, ведь он писался для априори более простой задачи.
Итак, перед нами замаячила задача оптимизации. Как ее решать? Для начала, конечно, найти слабое место. И вот тут первая проблема: профилировщик нам тут особо не поможет. Казалось бы, что создание и добавление виджетов – и есть то слабое место. То есть вот эта часть кода:
Код:
_flw->setContents(content);
В чем же здесь дело? Дело в самом Qt. А именно, в сложных алгоритмах ядра работы с виджетами. Дело в том, что, вызывая какую-либо команду в Qt, связанную с виджетами, Вы не можете никогда надеяться на то, что эта команда будет исполнена прямо сейчас. Часто она просто ставится в очередь задач. А исполнение задач из этой очереди может происходить как в отдельном потоке, так и в цикле обработки событий в основном потоке. Это, на самом деле, очень хорошая особенность Qt, благодаря которой мы получаем в целом очень быстрый GUI. Так, вызывая функцию QLabel::setText тысячу раз подряд с одним и тем же параметром, мы не получаем тысячекратного замедления, а получаем лишь однократную перерисовку QLabel с последним поданным значением параметра.
Конкретно в нашей задаче эта особенность Qt повлияла лишь на то, что стало сложнее понимать, как замерить наши тормоза. Что ж, придется немного поиграть с бубном. Для этого добавим таймер, запустим его после добавления всех данных на лэйаут, а остановим при первом вызове события resizeEvent.
Код:
void FormLayoutWgt::setContents(const WidgetList& widgetList)
{
for (int i = 0; i < widgetList.size(); i++)
{
if (widgetList.at(i).first.isEmpty())
_pLayout->addRow(widgetList.at(i).second);
else
_pLayout->addRow(widgetList.at(i).first, widgetList.at(i).second);
}
_timer.start();
}
void FormLayoutWgt::resizeEvent(QResizeEvent* event)
{
if (_timer.isValid())
{
qDebug() << "gui time = " << _timer.elapsed();
_timer.invalidate();
}
QScrollArea::resizeEvent(event);
}
Получили 703 мс, и это как раз то замедление, которое видят наши глаза, и которое послужило причиной оптимизации.
Что дальше? Как ускорить-то, если вся работа фактически происходит в затаенных глубинах Qt, в которые у нас доступа нет? (Да, если кто-то думает, что можно взять исходники Qt и немного подрихтовать, то эта плохая идея, но зато лучший способ убить пару месяцев.)
На самом деле все просто. Просто понять, почему такой долгий расчет идет в глубине кода Qt. Мы пытаемся разместить виджеты на QFormLayout. То есть, фактически, на табличном лэйауте. А ему для отрисовки каждого виджета нужно знать координаты, на которых рисовать. И также ширину и высоту. А значит, нужно опросить каждый виджет, в каких размерах ему будет комфортно отрисоваться. То есть как минимум, обратиться к функции sizeHint() для каждого виджета. А ведь есть еще функция QWidget::heightForWidth(int). А еще у виджетов могут быть разные политики QSizePolicy… И это все надо лэйауту учесть до отрисовки, потом провести расчет координат всех строк и столбцов, и только потом можно рисоваться. Потому неудивительно, что такой расчет для нескольких сотен виджетов может быть не такой мгновенный, к которому мы привыкли.
Хорошо, это мы поняли, а что с этим делать? Можно попытаться по сути сделать то же самое, что делает QFormLayout, но сэкономить при этом на обёртках.