Отлов событий в Qt с помощью фильтров событий

Исполнение компьютерных программ происходит последовательно, выполняя одну инструкцию за другой. Иногда в программе требуется дождаться некоторого события и отреагировать на него. Ждать этого события в бесконечном цикле расточительно и для обработки таких событий придуманы библиотеки и системные функции, позволяющие вызывать пользовательские функции в тот момент когда происходит событие. В Qt имеется собственная система событий. Очевидный и топорный способ реакции на возникшее событие — переопределение методов базового класса, но этот же способ является и самым громоздким, а также может приводить к нарушению принципов DRY (don’t repeat yourself) т. е. дублированию кода. В этой статье вы познакомитесь со способом вынесения пользовательских обработчиков событий в отдельный класс с возможностью повторного использования.

Содержание статьи

1. Создание главного окна
2. Фильтры событий
2.1 Определение класса фильтра событий
2.2 Реализация фильтра событий
2.3 Обработчик события нажатия кнопки
2.4 Обработчик события отпускания кнопки
2.5 Обработчик события перемещения мыши
3. Выводы
4. Исходный код

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

Алгоритм следующий

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

1. Создание главное окна

Создадим файл main.cpp следующего содержания (см. листинг 1)

#include <QApplication>
#include <QtWidgets>
#include "PaintEvent.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    auto view = new QGraphicsView();
    view->resize(800,600);

    auto scene = new QGraphicsScene(view);
    view->setScene(scene);

    auto paintEvent = new PaintEvent(scene);
    scene->installEventFilter(paintEvent);

    view->show();

    return a.exec();
}

Листинг 1. Код файла main.cpp

Рисование в Qt осуществляется в объекте QGraphicsScene, указатель на который передается для отображения в виджет QGraphicsView. Связка QGraphicsScene и QGraphicsView работает в парадигме модель-представление.

В приведенном выше исходном коде создается экземпляр класса QGraphicsView и устанавливается размер 800х600 пикселей. Создается экземпляр QGraphicsScene с указанием предка — экземпляра класса QGraphicsView. Экземпляру класса QGraphicsView устанавливаем сцену для отображения. Создаем фильтр событий — экземпляр пользовательского класса (подробно разберем далее) PaintEvent, передавая в конструктор указатель на сцену. Методом installEventFilter устанавливаем фильтр событий paintEvent для объекта scene. И делаем экземпляр QGraphicsView видимым.

При попытке запустить приложение увидим окно программы как на рисунке 1.

Рисунок 1.  Главное окно программы
Рисунок 1. Главное окно программы

Можете поводить мышью, по нажимать, но ничего не произойдёт. Необходимо привязаться к некоторым событиям и описать реакцию программы на эти действия.

Очевидный и одновременно громоздкий способ реализации алгоритма это создать объект, унаследованный от класса QGraphicsScene и в собственном классе переопределить методы:

  • virtual void mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent);
  • virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent);
  • virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent);

Это не лучший способ, к тому же он способен утяжелить и раздуть код. Аккуратно отловить события можно фильтром событий.

2. Фильтры событий

Фильтры событий это объекты унаследованные от QObject и имеющие метод bool eventFilter(QObject* object, QEvent* event); Метод принимает 2 параметра: указатель QObject на объект, событие для которого было перехвачено и указатель на QEvent – объект, описывающий произошедшее событие. Возвращает метод значение булева типа. Это свойство я называю поглощением события. Поглощение события не дает событию выйти за пределы фильтра, оно поглощается. При возвращении значения true событие перехватывается фильтром и не будет передано далее к стандартным обработчикам — фильтр поглощает событие, при возвращении false событие будет передано дальше — фильтр прозрачен.

2.1 Определение класса фильтра событий

Создадим определение класса PaintEvent в файле painEvent.cpp (см. листинг 2). Разместим в закрытой секции указатели на экземпляры следующих классов:

  • QGraphicsScene – отвечает за рисование объектов;
  • QGraphicsItem – хранит последний отрезок, созданный при перемещении курсора;
  • QPointF – хранит позицию курсора мыши в момент нажатия кнопки;
  • два экземпляра QPen для задания стиля рисуемых отрезков: один для вспомогательного и один для постоянных отрезков,

и булеву переменную pressed – хранящую состояние длящегося нажатия.

Конструктор принимает указатель на объект сцены класса QGraphicsScene. Сцена будет использоваться для рисования и будет родителем для PaintEvent. Метод bool eventFilter(QObject* object, QEvent* event) будет исполнять роль фильтра событий.

#pragma once

#include <QObject>
#include <QtWidgets>

class PaintEvent : public QObject
{
    Q_OBJECT
    QGraphicsScene* scene {nullptr};
    QPointF startPoint;
    QGraphicsItem* draftLine {nullptr};
    QPen tempPen;
    QPen pen;
    bool pressed = false;
public:
    explicit PaintEvent(QGraphicsScene* inScene);
    bool eventFilter(QObject* object, QEvent* event);
};

Листинг 2. Определение класса PaintEvent в файле paintEvent.h

2.2 Реализация фильтра событий

Приступим к реализации методов класса PaintEvent. Их два: конструктор и eventFilter.

В конструкторе инициализируем параметр scene указателем inScene и настроим экземпляры класса QPen описывающие стили пера для рисования отрезков: pen и tempPen. Перу pen установим цвет Qt::blue методом setColor и толщину 2 пикслея методом setWidth. Аналогично для пера tempPen установим цвет Qt::gray и толщину 1 пиксель. Pen определяет стиль нарисованных отрезков, появляющиеся после отпускания кнопки мыши. TempPen задает стиль для вспомогательных отрезков, рисуемых из позиции курсора в момент нажатия и до текущего положения мыши, пока кнопка мыши ещё нажата. (см. листинг 3)

#include "PaintEvent.h"

#include <QGraphicsSceneEvent>
#include <QPainter>
#include <QDebug>

PaintEvent::PaintEvent(QGraphicsScene* inScene) :
    QObject(inScene),
    scene(inScene)
{
    tempPen.setColor(Qt::gray);
    tempPen.setWidth(1);
    pen.setColor(Qt::blue);
    pen.setWidth(2);
}

Листинг 3. Определение конструктора класса PaintEvent

А теперь самое интересное – реализация метода eventFilter (см. листинг 4). Первым делом удостоверимся, что источник события — это объект сцецны. Получим тип события вызовом метода type() экземпляра класса QEvent и отловим одно из трёх событий:

  • «Нажатие кнопки мыши» – QEvent::GraphicsSceneMousePress;
  • «Отпускание кнопки мыши» – QEvent::GraphicsSceneMouseRelease;
  • «Перемещение указателя мыши» – QEvent::GraphicsSceneMouseMove.

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

В фильтр событий попадают все события генерируемые объектом, для которого этот фильтр установлен: нажатие клавиш, события мыши и другие. Наша цель отфильтровать лишь нужные события.

Все события мыши произошедшие с объектом класса QGraphicsScene описываются классом QGraphicsSceneMouseEvent, поэтому необходимо привести указатель на объект события QEvent к классу QGraphicsSceneMouseEvent предоставляющему доступ к информации о позиции курсора и нажатым клавишам мыши.

2.3 Обработчик события нажатия кнопки

Прежде чем приступать к обработке события, необходимо убедиться, что нажата именно левая кнопка мыши, а не другая. Для этого сравним результат вызова метода button() экземпляра класса QGraphicsSceneMouseEvent с константой Qt::LeftButton. Если условие выполняется, то сохраним текущее положение курсора (вызов метода sсenePos() экземпляра события) в параметр startPoint. Установим параметр pressed значение true. Pressed используется в обработчике события перемещения мыши как индикатор того, что нажата левая кнопка и процесс построения отрезка ещё не завершен.

2.4 Обработчик события отпускания кнопки

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

2.5 Обработчик события перемещения мыши

Отрисовку вспомогательных отрезков реализуем в третьем обработчике. Проверять нажатие кнопки в этом случае нет смысла, но удалить предыдущий вспомогательный отрезок, если таков был создан ранее, мы просто обязаны. Убедимся, что значение параметра pressed == true, тогда рисуем новый вспомогательный отрезок с использованием другого пера — tempPen.

После реализации необходимых обработчиков возвращаем из функции значение false, потому что нет причин блокировать прохождение событий далее. Реализованные обработчики, в этом конкретном случае, расширяют функционал системных обработчиков, а не заменяют их, поэтому поглощать события нет смысла.

Помните! Фильтр событий получает абсолютно все события и поглощение событий сработает для всех событий для которых метод eventFilter вернёт false!

Исходные код метода eventFilter приведен в листинге 4.

bool PaintEvent::eventFilter(QObject* object, QEvent* event) {
    if(object == scene) {
        QGraphicsSceneMouseEvent* mouseEvent = nullptr;
        switch(event->type()) {
        case QEvent::GraphicsSceneMousePress:
            mouseEvent = static_cast<QGraphicsSceneMouseEvent*>(event);
            if(mouseEvent->button() == Qt::LeftButton) {
                startPoint = mouseEvent->scenePos();
                pressed = true;
            }
            break;
        case QEvent::GraphicsSceneMouseRelease:
            mouseEvent = static_cast<QGraphicsSceneMouseEvent*>(event);
            if(mouseEvent->button() == Qt::LeftButton) {
                scene->addLine(QLineF(startPoint, mouseEvent->scenePos()), pen);
                pressed = false;
            }
            break;
        case QEvent::GraphicsSceneMouseMove:
            mouseEvent = static_cast<QGraphicsSceneMouseEvent*>(event);
            if(draftLine != nullptr) {
                scene->removeItem(draftLine);
                delete draftLine;
                draftLine = nullptr;
            }
            if(pressed)
                draftLine = scene->addLine(QLineF(startPoint, mouseEvent->scenePos()), tempPen);
            break;
        default:
            break;
        }
    }
    return false;
}

Листинг 4. Исходный код метода eventFilter

Запускаем приложение и наслаждаемся готовым результатом (см. рис. 2).

Рисунок 2. Процесс рисования отрезков в готовом приложении
Рисунок 2. Процесс рисования отрезков в готовом приложении

3. Выводы

  • Фильтры событий это объекты унаследованные от QObject и реализующие метод bool eventFilter(QObject* object, QEvent* event);
  • Фильтры событий позволяют выносить в отдельный класс реализацию обработчиков событий, расширять или переопределять функционал, избежать дублирования кода;
  • Один и тот же фильтр может быть применен сразу к нескольким объектам;
  • Несколько фильтров может быть применено к одному объекту;

4. Исходный код

Скачать исходный код можно по ссылке ниже: