На этом уроке мы поговорим о слотах, сигналах и событиях в Qt5.
Модель событий в программах Qt5
Механизм сигналов и слотов является расширением языка программирования С++ в Qt5, который используется для установления связи между объектами. Если происходит какое-либо определенное событие, то при этом может генерироваться сигнал. Данный сигнал попадает в связанный с ним слот. В свою очередь, слот — это обычный метод в языке C++, который присоединяется к сигналу; он вызывается тогда, когда генерируется связанный с ним сигнал. Как видите, ничего сложного здесь нет.
Все графические приложения управляются событиями: всё, что происходит в приложении является результатом обработки тех или иных событий. Они являются важной частью любой графической программы. В большинстве случаев события генерируются пользователем приложения, но они также могут быть сгенерированы и другими средствами, например, подключением к интернету, оконным менеджером или таймером. При разработке программ в Qt5, задумываться о событиях приходится довольно редко, поскольку виджеты Qt5 генерируют сигналы, когда происходит нечто значительное. Сами же события приобретают значение в том случае, когда необходимо создать, например, новый виджет или расширить функционал существующего.
В модели событий есть 3 участника:
источник события — это объект, состояние которого изменяется;
объект события — это отслеживаемый параметр источника события (например, нажатие клавиши на клавиатуре или изменение размеров виджета);
цель события — это объект, который должен быть уведомлен о произошедшем событии.
Не нужно путать сигналы с событиями. Сигналы необходимы для организации взаимодействия между виджетами, тогда как события необходимы для организации взаимодействия между виджетом и системой.
Щелчок мыши
В следующем примере мы рассмотрим простой способ обработки событий. У нас есть одна кнопка, по щелчку мышкой на которую мы завершаем работу приложения.
Заголовочный файл — click.h:
1 2 3 4 5 6 7 8 9 |
#pragma once #include <QWidget> class Click : public QWidget { public: Click(QWidget *parent = 0); }; |
Файл реализации — click.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <QPushButton> #include <QApplication> #include <QHBoxLayout> #include "click.h" Click::Click(QWidget *parent) : QWidget(parent) { QHBoxLayout *hbox = new QHBoxLayout(this); hbox->setSpacing(5); QPushButton *quitBtn = new QPushButton("Quit", this); hbox->addWidget(quitBtn, 0, Qt::AlignLeft | Qt::AlignTop); connect(quitBtn, &QPushButton::clicked, qApp, &QApplication::quit); } |
Метод connect() соединяет сигнал со слотом. Когда мы нажимаем на кнопку Quit
, генерируется сигнал щелчка кнопки мыши. qApp
— это глобальный указатель на объект нашего приложения. Он определяется в заголовочном файле QApplication. Метод quit() вызывается при появлении сигнала щелчка мышкой:
1 |
connect(quitBtn, &QPushButton::clicked, qApp, &QApplication::quit); |
Основной файл программы — main.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <QApplication> #include "click.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); Click window; window.resize(250, 150); window.setWindowTitle("Click"); window.show(); return app.exec(); } |
Результат выполнения программы:
Нажатие кнопки клавиатуры
В следующем примере мы рассмотрим способ реагирования на нажатие кнопки клавиатуры. Приложение завершит свое выполнение, если мы нажмем на клавишу Esc
.
Заголовочный файл — keypress.h:
1 2 3 4 5 6 7 8 9 10 11 12 |
#pragma once #include <QWidget> class KeyPress : public QWidget { public: KeyPress(QWidget *parent = 0); protected: void keyPressEvent(QKeyEvent * e); }; |
Файл реализации — keypress.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <QApplication> #include <QKeyEvent> #include "keypress.h" KeyPress::KeyPress(QWidget *parent) : QWidget(parent) { } void KeyPress::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { qApp->quit(); } } |
Одним из способов работы с событиями в Qt5 является переопределение обработчика событий. QKeyEvent
— это класс, который содержит информацию о произошедшем событии. В нашем случае мы используем объект данного класса для определения того, что была нажата именно клавиша Esc:
1 2 3 4 5 6 |
void KeyPress::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Escape) { qApp->quit(); } } |
Основной файл программы — main.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <QApplication> #include "keypress.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); KeyPress window; window.resize(250, 150); window.setWindowTitle("Key press"); window.show(); return app.exec(); } |
Класс QMoveEvent
Класс QMoveEvent содержит параметры событий, возникающих при перемещении виджета. В следующем примере мы реагируем на событие перемещения, затем определяем текущие координаты x
и y
верхнего левого угла клиентской области окна и устанавливаем эти значения в заголовок окна.
Заголовочный файл — move.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#pragma once #include <QMainWindow> class Move : public QWidget { Q_OBJECT public: Move(QWidget *parent = 0); protected: void moveEvent(QMoveEvent *e); }; |
Файл реализации — move.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <QMoveEvent> #include "move.h" Move::Move(QWidget *parent) : QWidget(parent) { } void Move::moveEvent(QMoveEvent *e) { int x = e->pos().x(); int y = e->pos().y(); QString text = QString::number(x) + "," + QString::number(y); setWindowTitle(text); } |
Мы используем объект класса QMoveEvent для определения значений x
и y
:
1 2 |
int x = e->pos().x(); int y = e->pos().y(); |
Затем мы конвертируем целочисленные значения в строки:
1 |
QString text = QString::number(x) + "," + QString::number(y); |
И с помощью метода setWindowTitle() устанавливаем текст в заголовок окна:
1 |
setWindowTitle(text); |
Основной файл программы — main.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <QApplication> #include "move.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); Move window; window.resize(250, 150); window.setWindowTitle("Move"); window.show(); return app.exec(); } |
Результат выполнения программы:
Отключение сигналов
Сигнал может быть отключен от слота. Следующий пример показывает, как это можно сделать.
В заголовочном файле мы объявили два слота. Следует отметить, что slot
не является ключевым словом в языке C++, а лишь расширением Qt5. Подобные расширения обрабатываются препроцессором фреймворка до выполнения компиляции кода. Когда в наших классах мы используем сигналы и слоты, то обязательно должны предоставить макрос Q_OBJECT
в начале определения класса. В противном случае препроцессор будет выдавать сообщения об ошибках.
В следующем примере у нас есть кнопка и флажок. Флажок подключает и отключает слот от сигнала нажатия кнопок.
Заголовочный файл — disconnect.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#pragma once #include <QWidget> #include <QPushButton> class Disconnect : public QWidget { Q_OBJECT public: Disconnect(QWidget *parent = 0); private slots: void onClick(); void onCheck(int); private: QPushButton *clickBtn; }; |
Файл реализации — disconnect.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
#include <QTextStream> #include <QCheckBox> #include <QHBoxLayout> #include "disconnect.h" Disconnect::Disconnect(QWidget *parent) : QWidget(parent) { QHBoxLayout *hbox = new QHBoxLayout(this); hbox->setSpacing(5); clickBtn = new QPushButton("Click", this); hbox->addWidget(clickBtn, 0, Qt::AlignLeft | Qt::AlignTop); QCheckBox *cb = new QCheckBox("Connect", this); cb->setCheckState(Qt::Checked); hbox->addWidget(cb, 0, Qt::AlignLeft | Qt::AlignTop); connect(clickBtn, &QPushButton::clicked, this, &Disconnect::onClick); connect(cb, &QCheckBox::stateChanged, this, &Disconnect::onCheck); } void Disconnect::onClick() { QTextStream out(stdout); out << "Button clicked" << endl; } void Disconnect::onCheck(int state) { if (state == Qt::Checked) { connect(clickBtn, &QPushButton::clicked, this, &Disconnect::onClick); } else { disconnect(clickBtn, &QPushButton::clicked, this, &Disconnect::onClick); } } |
Подключаем сигналы к нашим пользовательским слотам:
1 2 |
connect(clickBtn, &QPushButton::clicked, this, &Disconnect::onClick); connect(cb, &QCheckBox::stateChanged, this, &Disconnect::onCheck); |
Если мы делаем щелчок мышкой, то в окно терминала будет отправляться текст Button clicked
:
1 2 3 4 5 |
void Disconnect::onClick() { QTextStream out(stdout); out << "Button clicked" << endl; } |
Внутри слота onCheck() мы подключаем или отключаем слот onClick() от кнопки, в зависимости от полученного параметра состояния:
1 2 3 4 5 6 7 8 |
void Disconnect::onCheck(int state) { if (state == Qt::Checked) { connect(clickBtn, &QPushButton::clicked, this, &Disconnect::onClick); } else { disconnect(clickBtn, &QPushButton::clicked, this, &Disconnect::onClick); } } |
Основной файл программы — main.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <QApplication> #include "disconnect.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); Disconnect window; window.resize(250, 150); window.setWindowTitle("Disconnect"); window.show(); return app.exec(); } |
Результат выполнения программы:
Таймер
Таймер используется для реализации одиночного действия или же повторяющихся задач. Хорошим примером, где мы можем задействовать таймер, являются часы; каждую секунду мы должны обновлять нашу метку, отображающую текущее время.
В следующем примере мы попробуем отобразить в окне текущее местное время.
Заголовочный файл — timer.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#pragma once #include <QWidget> #include <QLabel> class Timer : public QWidget { public: Timer(QWidget *parent = 0); protected: void timerEvent(QTimerEvent *e); private: QLabel *label; }; |
Файл реализации — timer.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include "timer.h" #include <QHBoxLayout> #include <QTime> Timer::Timer(QWidget *parent) : QWidget(parent) { QHBoxLayout *hbox = new QHBoxLayout(this); hbox->setSpacing(5); label = new QLabel("", this); hbox->addWidget(label, 0, Qt::AlignLeft | Qt::AlignTop); QTime qtime = QTime::currentTime(); QString stime = qtime.toString(); label->setText(stime); startTimer(1000); } void Timer::timerEvent(QTimerEvent *e) { Q_UNUSED(e); QTime qtime = QTime::currentTime(); QString stime = qtime.toString(); label->setText(stime); } |
Для отображения времени мы используем виджет-метку:
1 |
label = new QLabel("", this); |
Затем мы определяем текущее местное время и устанавливаем его в виджет-метку:
1 2 3 |
QTime qtime = QTime::currentTime(); QString stime = qtime.toString(); label->setText(stime); |
Запускаем таймер (при этом каждые 1000 мс генерируется событие таймера):
1 |
startTimer(1000); |
Для работы с событиями таймера необходимо переопределить метод timerEvent():
1 2 3 4 5 6 7 8 |
void Timer::timerEvent(QTimerEvent *e) { Q_UNUSED(e); QTime qtime = QTime::currentTime(); QString stime = qtime.toString(); label->setText(stime); } |
Основной файл программы — main.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <QApplication> #include "timer.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); Timer window; window.resize(250, 150); window.setWindowTitle("Timer"); window.show(); return app.exec(); } |
Результат выполнения программы:
Заключение
На этом уроке мы узнали, что генерация события вызывается каким-либо действием со стороны пользователя (например, щелчок кнопкой мыши, изменение размеров окна и т.д.) или самой программой. При этом события являются механизмом оповещения более низкого уровня по сравнению с сигналами и слотами.
Как компьютер понимает, когда надо вызывать метод keyPressEvent()?
Просто мы его негде не вызывали
Я привык что в С++ для того чтобы метод начал работать, его имя нужно вызвать(написать) к примеру в main()-е. А тут мы просто написали метод, но нигде не вызывали его.
Как компьютер понимает когда надо вызывать метод keyPressEvent(), т.е мы нажали на кнопку esc и как он понял что нам нужен именно этот метод?
Я сам пока учусь, но если я правильно понимаю, этот метод уже находиться внутри базового класс QWidget, но он там пуст. Система его вызывает автоматически при появлении события. И остаётся его только переопределить в дочернем классе.
Точно, спасибо))
Разве не лучше было бы явно биндить функцию к некому делегату, как я понял в QT все виджеты получают уведомление о каждом нажатии клавиш, а это скорее всего делается путем итерации по всем виджетам, а их может быть 50+ в одном окне
Скажите, пожалуйста, как правильно в "Отключение сигналов" задать QTextStream out(stdout), чтобы при запуске через консоль печатало "Button clicked" в эту же консоль? Ожидалось, что будет печатать. Приложение запускается, чек-бокс работает, а при нажатии на кнопку не выводит ничего(
Честно говоря — без понятия, никогда таким вопросом не задавался 🙂
Подскажите по нескольким базовым вопросам:
1. В файле keypress.h что значит class KeyPress : public QWidget {?
2. В файле keypress.cpp что значит void KeyPress::keyPressEvent(QKeyEvent *event) {?
3. Тут же откуда взялась qApp что она означает? в выражении qApp->quit();
1. Это значит, что мы берем класс QWidget и создаем от него потомка (класс KeyPress). Такой приём носит название "Наследование". Благодаря ему класс потомок наделяется всеми свойствами, которые есть у предка. Подробнее об этом можно почитать здесь:
https://ravesli.com/urok-153-nasledovanie-vvedenie/
https://ravesli.com/urok-154-bazovoe-nasledovanie-v-c/
2. void KeyPress::keyPressEvent(QKeyEvent *event) — это функция-обработчик события нажатия кнопки.
3. Практически в самом начале статье есть ответ на ваш вопрос:
"[…] qApp — это глобальный указатель на объект нашего приложения. Он определяется в заголовочном файле QApplication[…]".
Взялось оно вот отсюда:
1. А в файле "keypress.cpp"
это просто конструктор, со списком инициализации членов?
Как раз сейчас повторяю основы ООП 120 урок, до 153 еще не дошел! =)
2. keyPressEvent — она описана в QT, в каком-нибудь QMoveEvent?
Как туда перейти, чтобы посмотреть? Раньше вроде можно было F12 нажать и перейти к определению функции? Или я что-то путаю?
1. Да, он самый 🙂
2. https://imgur.com/a/nVejnzz