Глава 7. Обработка событий.

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

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

7.1. Обработчики событий.

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

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

События поступают к объектам в функцию event(), унаследованную от QObject. Реализация функции event() в QWidget передает наиболее употребимые типы событий специализированным обработчикам, таким как mousePressEvent(), keyPressEvent() и paintEvent(), остальные события игнорируются.

В предыдущих главах мы уже сталкивались с обработкой событий, при создании классов MainWindow, IconEditor, Plotter, ImageEditor и Editor. Полный список типов событий вы найдете в сопроводительной документации к классу QEvent. Кроме того, за программистом сохраняется возможность создания и диспетчеризации своих собственных типов событий. Нестандартные типы событий широко применяются в многопоточных приложениях, но это тема отдельной главы. В этой главе мы рассмотрим два типа событий: события от клавиатуры и события от таймера.

События от клавиатуры обрабатываются функциями keyPressEvent() и keyReleaseEvent(). В примере с виджетом Plotter, мы перекрывали родительский обработчик keyPressEvent(). Обычно программиста интересует только keyPressEvent(), поскольку к моменту нажатия интересующей его клавиши уже нажаты клавиши-модификаторы, а к моменту отпускания нужной клавиши, клавиши-модификаторы могут быть уже отжаты. К клавишам-модификаторам относятся: Ctrl, Shift и Alt. Состояние этих клавиш может быть получено вызовом функции state(). Например, представим, что нам необходимо написать виджет CodeEditor и реализовать обработчик событий от клавиатуры, который различал бы комбинации клавиш В начало и Ctrl+Home, в этом случае мы могли бы написать следующий код:

void CodeEditor::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Key_Home: if (event->state() & ControlButton) goToBeginningOfDocument(); else goToBeginningOfLine(); break; case Key_End: ... default: QWidget::keyPressEvent(event); } } Комбинации Tab и Backtab (Shift+Tab) -- особый случай. Они обрабатываются в QWidget::event() до того, как событие попадет в keyPressEvent(). Смысл этой комбинации заключается в передаче фокуса от одного виджета к другому, в заданной последовательности. Как правило, такое поведение нас вполне устраивает, но что делать, если необходимо реализовать иную семантику для данных комбинаций, например, чтобы клавишей Tab можно было оформлять отступы в CodeEditor? Выход довольно прост, он заключается в перекрытии метода предка event(): bool CodeEditor::event(QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = (QKeyEvent *)event; if (keyEvent->key() == Key_Tab) { insertAtCurrentPosition( \t ); return true; } } return QWidget::event(event); } Если событие пришло от клавиатуры, то объект типа QEvent приводится к типу QKeyEvent и выполняется определение нажатой клавиши. Если это клавиша Tab, то выполняются некоторые действия и функция возвращает результат true, сообщая Qt о том, что событие обработано. Если функция вернет false, то Qt попробует вызвать метод event() владельца.

Использование объектов QAction дает более высокий уровень обслуживания событий. Например, если предположить, что CodeEditor имеет два публичных слота goToBeginningOfLine() и goToBeginningOfDocument() и CodeEditor назначен центральным виджетом для класса MainWindow, то можно было бы обслуживать комбинации клавиш следующим образом:

MainWindow::MainWindow(QWidget *parent, const char *name) : QMainWindow(parent, name) { editor = new CodeEditor(this); setCentralWidget(editor); goToBeginningOfLineAct = new QAction(tr("Go to Beginning of Line"), tr("Home"), this); connect(goToBeginningOfLineAct, SIGNAL(activated()), editor, SLOT(goToBeginningOfLine())); goToBeginningOfDocumentAct = new QAction(tr("Go to Beginning of Document"), tr("Ctrl+Home"), this); connect(goToBeginningOfDocumentAct, SIGNAL(activated()), editor, SLOT(goToBeginningOfDocument())); ... } Такой способ облегчает добавление пунктов в меню или кнопок на панель инструментов, но об этом мы уже говорили в Главе 3. Если в меню не появляются пункты, описанные через QAction, то необходимо заменить QAction на QAccel -- класс, который используется QAction для обработки нажатий на комбинации клавиш.

Разница между этими двумя подходами (перекрытие метода keyPressEvent() и использование QAction или QAccel) очень похожа на разницу между перекрытием метода resizeEvent() и использованием дочерних классов от QLayout. Если вы создаете свой виджет, порождая его от QWidget, то скорее всего вам подойдет первый вариант, связанный с написанием нескольких своих обработчиков, с жестко зашитым поведением. Но если вы предполагаете использовать уже готовый виджет, то более удобен высокоуровневый подход, связанный с использованием QAction.

Другой распространенный тип событий -- события от таймера. В то время, как большинство событий связаны с действиями пользователя, события от таймера генерируются системой и позволяют организовать обработку данных через определенные интервалы времени. Этот тип событий может использоваться, например, для создания мигающего курсора или просто для обновления изображения на экране.

С целью демонстрации обслуживания событий от таймера, создадим виджет Ticker. Он будет выводить строку текста и прокручивать ее справа-налево на один пиксель каждые 30 миллисекунд. Если ширина виджета больше ширины текста, то заданный текст будет нарисован столько раз, сколько уместится на виджете.

Рисунок 7.1. Внешний вид виджета Ticker.


Начнем с файла заголовка: #ifndef TICKER_H #define TICKER_H #include <qwidget.h> class Ticker : public QWidget { Q_OBJECT Q_PROPERTY(QString text READ text WRITE setText) public: Ticker(QWidget *parent = 0, const char *name = 0); void setText(const QString &newText); QString text() const { return myText; } QSize sizeHint() const; protected: void paintEvent(QPaintEvent *event); void timerEvent(QTimerEvent *event); void showEvent(QShowEvent *event); void hideEvent(QHideEvent *event); private: QString myText; int offset; int myTimerId; }; #endif Мы реализуем четыре обработчика событий, при чем с тремя из них (timerEvent(), showEvent() и hideEvent()) мы встречаемся впервые.

Перейдем к файлу с реализацией:

#include <qpainter.h> #include "ticker.h" Ticker::Ticker(QWidget *parent, const char *name) : QWidget(parent, name) { offset = 0; myTimerId = 0; } Конструктор инициализирует переменную offset значением 0. Координата x, с которой будет выводится текст, получается из переменной offset. void Ticker::setText(const QString &newText) { myText = newText; update(); updateGeometry(); } Функция setText() запоминает текст, который должен выводиться на экран. Она вызывает update(), чтобы перерисовать виджет, а функцию updateGeometry() -- чтобы известить менеджер размещения об изменении "идеального" размера виджета. QSize Ticker::sizeHint() const { return fontMetrics().size(0, text()); } Функция sizeHint() возвращает "идеальные" размеры области, которые необходимы для вывода текста. Функция QWidget::fontMetrics() возвращает экземпляр класса QFontMetrics, с помощью которого можно получить информацию об используемом шрифте. В данном случае он возвращает размеры области, в которую уместился бы заданный текст. void Ticker::paintEvent(QPaintEvent *) { QPainter painter(this); int textWidth = fontMetrics().width(text()); if (textWidth < 1) return; int x = -offset; while (x < width()) { painter.drawText(x, 0, textWidth, height(), AlignLeft | AlignVCenter, text()); x += textWidth; } } Функция paintEvent() выводит текст, с помощью вызова QPainter::drawText(). С помощью fontMetrics() она определяет ширину текста и затем рисует его столько раз, сколько потребуется, чтобы заполнить виджет на всю ширину, учитывая значение переменной offset. void Ticker::showEvent(QShowEvent *) { myTimerId = startTimer(30); } Функция showEvent() запускает таймер. Функция QObject::startTimer() возвращает целое число, которое может быть использовано для идентификации таймера. Класс QObject может поддерживать несколько независимых таймеров, каждый со своим собственным временным интервалом. После вызова startTimer(), Qt будет автоматически генерировать события от таймера через интервалы времени, приблизительно равные 30-ти миллисекундам. Точность таймера зависит от операционной системы.

В принципе, startTimer() можно было бы вызвать и в конструкторе, но мы не сделали этого с целью экономии ресурсов системы, поскольку нет большого смысла в событиях от таймера, когда виджет невидим.

void Ticker::timerEvent(QTimerEvent *event) { if (event->timerId() == myTimerId) { ++offset; if (offset >= fontMetrics().width(text())) offset = 0; scroll(-1, 0); } else { QWidget::timerEvent(event); } } Функция timerEvent() -- это обработчик событий от таймера и вызывается системой через заданные интервалы времени. Она увеличивает величину смещения на 1, чтобы создать эффект перемещения, до тех пор, пока смещение не сравняется с шириной текста. Затем прокручивает содержимое виджета на 1 пиксель влево, вызовом функции QWidget::scroll(). Теоретически, вместо scroll() можно было бы вызвать update(), но функция scroll() более эффективна и к тому же предотвращает эффект мерцания, потому что она просто перемещает существующее на экране изображение и генерирует событие "paint" для очень узкой области, в данном случае область перерисовки имеет ширину в 1 пиксель.

Если событие поступило не от того таймера, который нас интересует, то оно просто передается базовому классу.

void Ticker::hideEvent(QHideEvent *) { killTimer(myTimerId); } Функция hideEvent() вызывает QObject::killTimer(), которая останавливает таймер.

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