Создание собственных виджетов Qt. Сигналы, слоты и события.

Когда я начинал изучать библиотеку Qt, весьма полезным показался пример виджета, выводящего бегущую строку. Подобный пример описан в настоящей статье, на нем мы разберем:

  1. механизм сигналов и слотов Qt;
  2. организацию объектов Qt в древовидную структуру, обеспечивающую автоматическое освобождение памяти при разрушении родительского объекта;
  3. обработку событий в Qt.

В результате у нас получится создать свой собственный виджет и связать его со стандартными элементами управления.

castom_widget_in_Qt

Собственный виджет Qt

Класс QObject

В библиотеке Qt есть множество самых различных классов, многие из них являются потомками класса QObject, который позволяет:

  • использовать механизм сигналов и слотов Qt;
  • организовывать объекты в древовидную структуру;
  • дополняет объект информацией о типе (мета-объектной информацией);
  • перемещать объект в отдельный поток [1];
  • обрабатывать события (QEvent);
  • использовать встроенный таймер QObject;

Механизм сигналов и слотов Qt

Механизм сигналов и слотов позволяет объектам обмениваться сообщениями. Например, при изменении текста внутри поля ввода (QLineEdit) генерируется сигнал textChanged, который может обработать любой другой объект, при этом его функция-обработчик называется слотом. Для того, чтобы при возникновении некоторого сигнала управление получил некоторый слот, достаточно соединить их функцией connect.

connect(speedSpinbox, SIGNAL(valueChanged(int)), runline, SLOT(setSpeed(int)));
connect(textLine, SIGNAL(textChanged(QString)), runline, SLOT(setString(QString)));

В приведенном примере при генерацией сигнала valueChanged объекта speedSpinBox активируется слот setSpeed объекта runline.

Один сигнал может быть соединен сразу с несколькими слотами. Механизм сигналов и слотов позволяет общаться объектам, находящихся в различных потоках. Последним аргументом метода connect может быть тип соединения, позволяющий переключать асинхронную и синхронную обработку сигналов. По умолчанию используется соединение типа Qt::AutoConnection, означающее асинхронную обработку в случае если источник и обработчик сигнала находятся в одном потоке и синхронную (сигналы накапливаются в очереди потока), в противном случае. Для использования данного механизма в начале описания класса должен стоять макрос Q_OBJECT.

class RunLine : public QLabel {
  Q_OBJECT
public:
  RunLine(QWidget *parent = 0);
public slots:
  void setString(const QString string);
  void setSpeed(const int speed);
protected:
  virtual void timerEvent(QTimerEvent*);

  int m_shift, m_timerId;
  QString m_string;
};

Из листинга видно, что наша бегущая строка имеет 2 слота, для изменения текста и скорости движения строки, но не генерирует ни одного сигнала.

Автоматическая сборка мусора в Qt

При использовании библиотеки Qt объекты часто формируют древовидную структуру с отношениями «родитель-потомок». Для любого объекта можно назначить родителя вызовом метода setParent, получить список дочерних объектов методом children или, допустим, выполнять поиск среди дочерних объектов. Обычно родительский объект передается в качестве параметра конструктора дочернему объекту.

При уничтожении родительского объекта, рекурсивно удаляются все дочерние объекты. Иногда этот процесс называют автоматической сборкой мусора (т.к. память мы выделяем руками, а уничтожаются объекты сами).

В нашем примере создается главное окно (класс MainWidget), которое включает в себя объекты классов QSpinBox, QLineEdit и RunLine. Очевидно, что вложенный QLineEdit не имеет смысла «оставлять живым» после того, как главное окно будет закрыто, т.е. время жизни поля ввода не больше чем время жизни главного окна, поэтому главное окно может быть родителем для вложенных в него объектов. Не всегда ситуация так очевидна, при установлении отношений «родитель-потомок» в Qt надо руководствоваться именно временем жизни объектов.

MainWidget::MainWidget(QWidget *parent) : QWidget(parent) {
  QGridLayout *layout(new QGridLayout(this));
  QSpinBox *speedSpinbox(new QSpinBox(this));
  QLineEdit *textLine(new QLineEdit(this));
  RunLine *runline(new RunLine(this));
// ...
}

 

UML-class_diagram_Qt

Диаграмма используемых классов Qt

Методы findChild и findChildren позволяют находить среди дочерних объектов, такие, которые приводятся к требуемому типу и имеют заданное имя (имя может быть задано методом setObjectName). Информация о типе и имени объекта называется мета-объектной.

Мета-объектная информация используется также, например при вызове функции qobject_cast, проверяющий принадлежность объекта к определенному типу.

Компилятор c++ ничего не знает об именах объектов (классы в c++ устроены проще), сигналах, слотах, событиях и т.п. В связи с этим, всякий раз, когда вы отправляете свою Qt-программу на компиляцию, сначала она подается на вход мета-объектного компилятора (MOC), который инициирует мета-объекты и генерирует код, понятный обычному компилятору.

Обработка событий в Qt

После работы с другими библиотеками, такими как WinAPI может быть трудно понять разницу между сигналом и событием, она кроется в источнике воздействия:

  • если мы нажимаем на кнопку, то кнопка обрабатывает соответствующее событие (QKeyEvent/QMouseEvent,QTouchEvent, смотря чем нажали), при этом кнопка генерирует сигнал;
  • если мы провели мышью над окном, то элементы под мышью получали об этом событие QHoverEvent, кроме того, обрабатывалось событие QPaintEvent, т.к. область окна под мышью перерисовывалась.

Из этих примеров понятно, что сигнал должен генерируется объектом при изменении его внутреннего состояния, а событие всегда приходит из внешней среды. Источником события может быть указатель, колесико и кнопки мыши, кнопки клавиатуры, тачскрин, сетевой адаптер или, например, системный таймер.

При поступлении события в приложение, оно распространяется до тех пор, пока не будет обработано (если какой-либо объект обработал событие, он должен вызвать у него метод accept чтобы оно не распространялось дальше).

Например, мы щелкнули мышью над кнопкой спинбокса, при этом событие получает сначала MainWidget, но т.к. обработать он его не может, то передает его тому дочернему элементу, над которым был произведен щелчок (по спинбоксу), спинбокс передает событие кнопке, которая его обрабатывает и вызывает метод accept, генерируя сигнал clicked.

Пример отражает суть механизма обработки событий, однако, любое из промежуточных звеньев тоже могло обработать событие (при этом как выполнив для него accept, прервав обработку, так и передать событие обработанное дальше). Для реализации такого поведения может быть использован виртуальный метод event. Кроме того, объект может генерировать события методами sendEvent и postEvent.

Лично я считаю генерацию событий не правильным подходом. Возможно это стоит использовать в каких-то очень специфических ситуациях, когда невозможно использование сигналов и слотов (не видел таких ситуаций). Обработка нескольких видов событий, а также событий, предназначенных для других объектов в методе event превращает код в кашу и это не хорошо. Пользуйтесь событиями правильно.

Обработка событий таймера

В библиотеке Qt есть 3 вида таймера — встроенный в QObject, QTimer и QBasicTimer. Все эти таймеры по умолчанию запускаются в том потоке, в котором были созданы, т.е. они не могут сработать пока выполняется какой-либо другой код в текущем потоке, а значит, их точность зависит от загруженности и зернистости (granularity, трудоемкости функций) потока. Если вам нужна точность, таймер стоит перенести в отдельный поток.

Наиболее удобным из них является QTimer, именно его рекомендует использовать официальная документация — он с заданной периодичностью генерирует сигналы. Более подробное описание и пример использования QTimer можно найти в соседней статье [2].

QBasicTimer является низкоуровневым, периодическим (генерирует события до тех пор, пока не будет остановлен) таймером. Весь интерфейс таймера составляют 4 метода — isActive возвращает true если таймер работает, timerId возвращает идентификатор таймера (идентификатор нужен в том случае, если объект-обработчик принимает события от нескольких таймеров и должен их различать). Методы stop и start останавливают и запускают таймер соответственно. Метод start в качестве аргументов принимает период в миллисекундах, с которым должны генерироваться события и указатель на объект-обработчик событий, для которого должен быть перегружен метод timerEvent.

Интерфейс таймеров, встроенных в QObject очень поход на QBasicTimer. Они тоже являются периодическими и порождают события, а не сигналы. Запуск таймера осуществляется методом startTimer, который принимает периодичность и возвращает идентификатор таймера. Таймер работает и порождает события до тех пор, пока не будет уничтожен методом killTimer (принимающий идентификатор таймера в качестве аргумента).

Наша бегущая строка должна перемещаться через равные промежутки времени, для выполнения этого требования удобнее всего использовать таймер. Мы применим встроенный таймер для того, чтобы привести пример обработки событий в Qt.

void RunLine::timerEvent(QTimerEvent *) {
  const int length = m_string.length();

  if(++m_shift >= length)
    m_shift = 0;

  setText(m_string.right(m_shift) + m_string.left(length - m_shift));
}

void RunLine::setSpeed(const int speed) {
  if (m_timerId)
    killTimer(m_timerId);
  m_timerId = 0;

  if (speed < 0)
    return;

  if (speed)
    m_timerId = startTimer(1000/speed);
}

void RunLine::setString(const QString string) {
  m_string = string;
  m_shift = 0;
  setText(m_string);
}

Мы не используем объект QTimerEvent в методе timerEvent, т.к. у нас работает лишь один таймер (нам не требуется идентифицировать источник события). Скорость движения строки у нас задается методом setSpeed, однако мы могли избавить от этого метода при использовании внешнего QBasicTimer — достаточно было бы указать объект RunLine в качестве адресата событий.

Создание пользовательского интерфейса Qt

Класс QWidget является базовым для всех виджетов (элементов управления). Этот класс содержит множество полей и методов, например, методы изменения размера или перемещения объекта. Виджеты могут вкладываться друг в друга (визуально, а не с установкой отношения «родитель-потомок», рассмотренного выше), при этом виджет-контейнер может использовать менеджер размещения (QLayout).

В листинге конструктора MainWidget представленного выше уже использовали менеджер QGridLayout, позволяющий размещать вложенные виджеты по сетке, кроме него может использоваться QVBoxLayout или QHBoxLayout (для размещения виджетов в линию по вертикали или горизонтали соответственно). Менеджер размещения управляет размерами и положением вложенных виджетов при изменении размера виджета-контейнера. Кроме того, менеджеры размещения могут вкладываться друг в друга. Размещать виджеты удобно визуально, мышью с использованием QtDesigner [2], но в этой статье мы вызываем addWidget явно. Во многих случаях явное использование addWidget оказывается единственным возможным, например если бы мы разрабатывали игру «Сапер» и размер игрового поля был бы заранее не известен.

layout->addWidget(textLine, 1, 1, 1, 1);
layout->addWidget(speedSpinbox, 1, 2, 1, 1);
layout->addWidget(runline, 2, 1, 1, 2);

Метод addWidget класса QGridLayout принимает в качестве второго и третьего аргумента координаты верхней левой ячейки таблицы, в которую требуется поместить виджет, указанный в первом аргументе. Последние два аргумента указывают количество ячеек по вертикали и горизонтали, которое займет данный виджет.

На приведенных выше листингах вы могли заметить, что класс бегущей строки наследует QLabel и вызывает его метод setText. Класс QLabel предназначен для вывода текста или изображений, является наследником класса QFrame. QLabel позволяет использовать HTML теги для оформления содержимого, однако, в текущей статье используется лишь метод setText() для задания выводимого текста.

Класс QFrame расширяет возможности QWidget в плане отображения рамки вокруг виджета и является базовым для элементов управления, нуждающихся в особой рамке. Наследниками класса QFrame являются, например, виджет панели инструментов (QToolBox), виджет видовой прокрутки (QAbstractScrollArea) или виджет вывода текста/изображений (QLabel).

Бегущая строка Qt. исходный код.

Рекомендуемая литература:

  1. Многопоточный сервер Qt. Пул потоков. Паттерн Decorator. Пример перемещения объекта в поток [Электронный ресурс] – режим доступа: https://pro-prof.com/archives/1390. Дата обращения: 06.05.2016.
  2. Собственные виджеты в Qt Designer. Описание и пример использования QTimer [Электронный ресурс] – режим доступа: https://pro-prof.com/archives/958. Дата обращения: 06.05.2016.
  3. Разработка игры «Сапер». Пример использования QGridLayout [Электронный ресурс] – режим доступа: https://pro-prof.com/archives/887. Дата обращения: 06.05.2016.
  4. Официальная документация по библиотеке Qt [Электронный ресурс] – режим доступа: http://doc.qt.io/. Дата обращения: 06.05.2016.
  5. QML. Как сделать кнопку? [Электронный ресурс] – режим доступа: http://younglinux.info/qt-qml/button. Дата обращения: 06.05.2016.
  6. Библиотека Qt. Уроки [Электронный ресурс] – режим доступа: http://www.evileg.ru/. Дата обращения: 06.05.2016.

Добавить комментарий