|
||
![]() |
||
Приложения с графическим интерфейсом управляются событиями: все, что происходит в приложении -- есть результат обработки тех или иных событий. При разработке программ под Qt, задумываться о событиях приходится довольно редко, поскольку виджеты Qt выдают сигналы, когда происходит нечто значительное. События приобретают значение в том случае, когда необходимо создать новый виджет или когда нужно расширить функциональность существующего виджета.
В этой главе мы рассмотрим существующую модель обработки событий, расскажем о фильтрации событий и в заключение исследуем цикл обработки событий, на предмет того, как уменьшить время отклика приложения на действия пользователя, во время длительной обработки данных.
События генерируются оконной системой или 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 и реализовать обработчик событий от клавиатуры, который различал бы комбинации клавиш Home и 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() по истечении каждого интервала времени, кроме того, он предоставляет возможность создания таймеров-будильников, которые срабатывают один раз.
Одна из замечательных особенностей модели обработки событий в Qt -- возможность одного экземпляра QObject отслеживать события, предназначенные для другого экземпляра QObject до того, как последний получит их.
Предположим, что у нас имеется виджет CustomerInfoDialog, собранный из нескольких QLineEdit, и нам необходимо передавать фокус ввода, от одного к другому, нажатием на клавишу "пробел". Решение "в лоб" -- создать дочерний класс от QLineEdit и перекрыть обработчик события keyPressEvent(), в котором вызывать focusNextPrevChild(), примерно так:
void MyLineEdit::keyPressEvent(QKeyEvent *event) { if (event->key() == Key_Space) focusNextPrevChild(true); else QLineEdit::keyPressEvent(event); }Однако это решение имеет массу недостатков. Поскольку MyLineEdit -- это нестандартный виджет, то нам придется приложить некоторые усилия, чтобы интегрировать его с Qt Designer, если захотим создавать формы с помощью визуального построителя. Кроме того, если потребуется, чтобы другие типы виджетов (такие как QComboBoxe и QSpinBox) так же поддерживали эту особенность, то мы будем вынуждены создать дочерние классы и для этих виджетов.
Более гибкое решение -- позволить CustomerInfoDialog отслеживать события, отправляемые подчиненным виджетам и реализовать необходимую функциональность. Делается это с помощью фильтров событий. Установка фильтра событий производится в два этапа:
Регистрация фильтра событий, вызовом функции installEventFilter() того объекта, которому предназначены события.
Создание обработчика перехваченных событий eventFilter().
CustomerInfoDialog::CustomerInfoDialog(QWidget *parent, const char *name) : QDialog(parent, name) { ... firstNameEdit->installEventFilter(this); lastNameEdit->installEventFilter(this); cityEdit->installEventFilter(this); phoneNumberEdit->installEventFilter(this); }После регистрации фильтра, все события, которые предназначены объектам firstNameEdit, lastNameEdit, cityEdit и phoneNumberEdit, сначала попадут в обработчик CustomerInfoDialog::eventFilter().
Ниже приводится исходный код функции eventFilter():
bool CustomerInfoDialog::eventFilter(QObject *target, QEvent *event) { if (target == firstNameEdit || target == lastNameEdit || target == cityEdit || target == phoneNumberEdit) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = (QKeyEvent *)event; if (keyEvent->key() == Key_Space) { focusNextPrevChild(true); return true; } } } return QDialog::eventFilter(target, event); }Прежде всего мы убеждаемся, что событие отправлено одному из QLineEdit. Не забывайте, что базовый класс QDialog может контролировать и другие виджеты. (В Qt 3.2 это не относится к QDialog. Однако, другие классы, такие как QMainWindow, отслеживают события некоторых из подчиненных виджетов по различным причинам.)
Если событие пришло от клавиатуры, то выполняется приведение к типу QKeyEvent и проверяется -- какая клавиша нажата. Если нажата клавиша "пробел", то вызывается функция focusNextPrevChild(), которая передает фокус вводв следующему виджету и возвращается результат true, сообщая Qt о том, что событие обработано. Если вернуть false, то Qt передаст событие объекту назначения.
Если событие порождено не клавишей "пробел", то управление передается функции eventFilter() базового класса.
В Qt предусмотрены пять уровней, на которых событие может быть перехвачено и обработано:
Обработка событий в функциях-обработчиках
Перекрытие обработчиков событий, таких как: mousePressEvent(), keyPressEvent() и paintEvent(), безусловно самый распространенный способ. Мы уже видели множество примеров тому.
Перекрытие метода QObject::event().
Внутри этого обработчика мы можем перехватывать события до того, как они попадут в специализированные функции-обработчики. Этот подход чаще всего используется для того, чтобы изменить реакцию виджета на клавишу табуляции, как это было показано ранее. Он так же используется для обработки событий, которые встречаются не так часто, например: LayoutDirectionChange. Если мы перекрываем функцию event(), то необходимо предусмотреть вызов обработчика event() базового класса, чтобы обработать события, которые нас не интересуют.
Установка фильтра событий для QObject.
После того, как фильтр будет зарегистрирован функцией installEventFilter(), все события, предназначающиеся указанному объекту, сначала будут попадать в обработчик eventFilter(). Такой способ мы использовали для перехвата событий от клавиши "пробел" в примере выше.
Установка фильтра событий объекта QApplication.
После регистрации фильтра, любое событие, предназначенное для любого объекта в приложении, будет сначала попадать в обработчик eventFilter(). Такой подход чаще всего используется в целях отладки и реализации в приложении скрытых сюрпризов (так называемых "пасхальных яиц").
Создание дочернего класса от QApplication и перекрытие метода notify().
Qt вызывает QApplication::notify(), чтобы передать событие приложению. Таким способом можно перехватить любое событие до того, как оно попадет в фильтр событий. Вообще фильтры событий более удобны, поскольку допускается одновременное существование любого количества фильтров, а функция notify() может быть только одна.
Большинство типов событий, включая события от мыши и клавиатуры, могут передаваться дальше. Если событие не было обработано по пути к объекту наначения, или самим объектом, то процесс обработки события повторяется, но на этот раз объектом назначения становится виджет-владелец. Так продолжается до тех пор, пока событие не будет обработано, либо пока событие не достигнет виджет самого верхнего уровня.
Рисунок 7.2. Обработка событий в окне диалога.
С вызова функции QApplication::exec() начинается главный цикл обработки событий. Сначала Qt запускает несколько событий, чтобы отобразить и перерисовать виджеты. После этого в цикле постоянно выполняется проверка поступления новых событий и их передача виджетам приложения.
Во время обработки одного события, в очередь могут поступать другие события. Если обработка какого либо события занимает продолжительное время, то это может отрицательно сказаться на времени отклика пользовательского интерфейса. Например, ни одно событие, поступившее от оконной системы во время сохранения файла на диск, не будет обработано до тех пор, пока файл не будет сохранен полностью. В течение времени, необходимого для сохранения файла, приложение никак не реагирует на запросы оконной системы.
Как одно из возможных решений данной проблемы -- создавать многопоточные приложения, в которых один поток будет отвечать за пользовательский интерфейс, а другой -- за дисковые операции (или любые другие действия, выполняющиеся продолжительное время). В этом случае приложение будет исправно откликаться на действия пользователя даже во время выполнения длительной обработки данных. Этот подход мы будем обсуждать в Главе 17.
Более простое решение -- вызывать QApplication::processEvents() как можно чаще, во время длительных операций. Эта функция выполняет обработку событий, ожидающих в очереди, и затем возвращает управление в вызвавшую функцию. Фактически, QApplication::exec() -- это не более чем цикл while, в котором вызывается функция processEvents().
Ниже приводится пример того, как можно сократить время отклика приложения Spreadsheet, во время сохранения большого файла на диск (см. оригинальную версию):
bool Spreadsheet::writeFile(const QString &fileName) { QFile file(fileName); ... for (int row = 0; row < NumRows; ++row) { for (int col = 0; col < NumCols; ++col) { QString str = formula(row, col); if (!str.isEmpty()) out << (Q_UINT16)row << (Q_UINT16)col << str; } qApp->processEvents(); } return true; }Однако в таких случаях существует одна опасность: пользователь может закрыть приложение до того, как файл будет сохранен, или даже может повторно вызывать процедуру сохранения файла. Эта проблема решается довольно просто -- нужно заменить вызов
qApp->processEvents();на
qApp->eventLoop()->processEvents(QEventLoop::ExcludeUserInput);который заставит Qt игнорировать события от мыши и клавиатуры.
Зачастую возникает необходимость вывести окно диалога, демонстрирующего ход выполнения длительной операции. Для подобных целей предназначен QProgressDialog, который имеет индикатор хода выполнения. У него так же имеется кнопка Cancel, с помощью которой пользователь может прервать операцию. Ниже представлен измененный вариант функции, которая демонстрирует пользователю ход операции сохранения файла:
bool Spreadsheet::writeFile(const QString &fileName) { QFile file(fileName); ... QProgressDialog progress(tr("Saving file..."), tr("Cancel"), NumRows); progress.setModal(true); for (int row = 0; row < NumRows; ++row) { progress.setProgress(row); qApp->processEvents(); if (progress.wasCanceled()) { file.remove(); return false; } for (int col = 0; col < NumCols; ++col) { QString str = formula(row, col); if (!str.isEmpty()) out << (Q_UINT16)row << (Q_UINT16)col << str; } } return true; }На этот раз функция создает QProgressDialog, которому передает значение переменной NumRows, как общее число шагов. Затем, перед сохранением каждой строки, вызывается setProgress(), которая обновляет индикатор хода операции. Процент выполнения вычисляется компонентом QProgressDialog самостоятельно. Затем вызывается QApplication::processEvents(), чтобы обновить изображение на экране, а заодно и проверить -- не нажал ли пользователь на кнопку Cancel. Если кнопка Cancel была нажата, то операция сохранения прерывается и файл удаляется.
Мы не вызываем метод show() диалога, потому что он самостоятельно выполняет это действие. Если операция выполняется достаточно быстро, возможно потому что файл получился очень коротким, или потому что компьютер обладает очень высокой производительностью, QProgressDialog обнаружит это и вообще не будет выводить себя на экран.
Есть еще один способ выполнения длительных операций. Он сильно отличается от того, что был описан выше. Вместо того, чтобы в процессе длительных операций предусматривать обработку пользовательского интерфейса, можно наоборот, производить длительные операции, когда приложение простаивает. Этот способ пригоден в тех случаях, когда операция может быть безопасно прервана и затем опять продолжена.
В Qt такой вариант может быть реализован с помощью специального таймера -- таймера с нулевым интервалом. Такие таймеры генерируют события, когда очередь событий пуста. Ниже приводится пример реализации обработчика событий от такого таймера:
void Spreadsheet::timerEvent(QTimerEvent *event) { if (event->timerId() == myTimerId) { while (step < MaxStep && !qApp->hasPendingEvents()) { performStep(step); ++step; } } else { QTable::timerEvent(event); } }Если функция hasPendingEvents() возвращает true, обработка данных приостанавливается и управление передается обратно в Qt. Обработка будет продолжена, когда Qt обслужит все события в очереди.
|