Reimplementing Event Handlers
Events are generated by the window system or by Qt in response to various occurrences. When the user presses or releases a key or mouse button, a key or mouse event is generated. When a window is moved to reveal a window that was underneath, a paint event is generated to tell the newly visible window that it needs to repaint itself. An event is also generated whenever a widget gains or loses keyboard focus. Most events are generated in response to user actions, but some, like timer events, are generated independently by the system.
Events should not be confused with signals. Signals are useful when using a widget, whereas events are useful when implementing a widget. For example, when we are using QPushButton, we are more interested in its clicked() signal than in the low-level mouse or key events that caused the signal to be emitted. But if we are implementing a class like QPushButton, we need to write code to handle mouse and key events and emit the clicked() signal when necessary.
Events are notified to objects through their event() function, inherited from QObject. The event() implementation in QWidget forwards the most common types of events to specific event handlers, such as mousePressEvent(), keyPressEvent(), and paintEvent(), and ignores other kinds of events.
We have already seen many event handlers when implementing MainWindow, IconEditor, Plotter, ImageEditor, and Editor in the previous chapters. There are many other types of events, listed in the QEvent reference documentation, and it is also possible to create custom event types and dispatch custom events ourselves. Custom events are particularly useful in multithreaded applications, so they are discussed in Chapter 17 (Multithreading). Here, we will review two event types that deserve more explanation: key events and timer events.
Key events are handled by reimplementing keyPressEvent() and keyReleaseEvent(). The Plotter widget reimplements keyPressEvent(). Normally, we only need to reimplement keyPressEvent() since the only keys for which release is important are the modifier keys Ctrl, Shift, and Alt, and these can be checked for in a keyPressEvent() using state(). For example, if we were implementing a CodeEditor widget, its stripped-down keyPressEvent() that distinguishes between Home and Ctrl+Home would look like this:
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); } }
The Tab and Backtab (Shift+Tab) keys are special cases. They are handled by QWidget::event() before it calls keyPressEvent(), with the semantic of passing the focus to the next or previous widget in the focus chain. This behavior is usually what we want, but in a CodeEditor widget, we might prefer to make Tab indent a line. The event() reimplementation would then look like this:
bool CodeEditor::event(QEvent * event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = (QKeyEvent *) event; if (keyEvent->key() == Key_Tab) { insertAtCurrentPosition(' '); return true; } } return QWidget::event(event); }
If the event is a key press, we cast the QEvent object to a QKeyEvent and check which key was pressed. If the key is Tab, we do some processing and return true to tell Qt that we have handled the event. If we returned false, Qt would propagate the event to the parent widget.
A higher-level approach for implementing key bindings is to use a QAction. For example, if goToBeginningOfLine() and goToBeginningOfDocument() are public slots in the CodeEditor widget, and the CodeEditor is used as the central widget in a MainWindow class, we could add the key bindings with the following code:
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())); ... }
This makes it easy to add the commands to a menu or a toolbar, as we saw in Chapter 3. If the commands don't appear in the user interface, the QAction objects could be replaced with a QAccel object, the class used by QAction internally to support key bindings.
The choice between reimplementing keyPressEvent() and using QAction (or QAccel) is similar to that between reimplementing resizeEvent() and using a QLayout subclass. If we are implementing a custom widget by subclassing QWidget, it's straightforward to reimplement a few more event handlers and hard-code the behavior there. But if we are merely using a widget, the higherlevel interfaces provided by QAction and QLayout are more convenient.
Another common type of event is the timer event. While most types of events occur as a result of a user action, timer events allow applications to perform processing at regular time intervals. Timer events can be used to implement blinking cursors and other animations, or simply to refresh the display.
To demonstrate timer events, we will implement a Ticker widget. This widget shows a text banner that scrolls left by one pixel every 30 milliseconds. If the widget is wider than the text, the text is repeated as often as necessary to fill the entire width of the widget.
Figure 7.1. The Ticker widget
Here's the header file:
#ifndef TICKER_H #define TICKER_H #include 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
We reimplement four event handlers in Ticker, three of which we have not seen before: timerEvent(), showEvent(), and hideEvent().
Now let's review the implementation:
#include #include "ticker.h" Ticker::Ticker(QWidget *parent, const char *name) : QWidget(parent, name) { offset = 0; myTimerId = 0; }
The constructor initializes the offset variable to 0. The x coordinate at which the text is drawn is derived from the offset value.
void Ticker::setText(const QString &newText) { myText = newText; update(); updateGeometry(); }
The setText() function sets the text to display. It calls update() to force a repaint and updateGeometry() to notify any layout manager responsible for the Ticker widget about a size hint change.
QSize Ticker::sizeHint() const { return fontMetrics().size(0, text()); }
The sizeHint() function returns the space needed by the text as the widget's ideal size. The QWidget::fontMetrics() function returns a QFontMetrics object that can be queried to obtain information relating to the widget's font. In this case, we ask for the size required by the given text.
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; } }
The paintEvent() function draws the text using QPainter::drawText(). It uses fontMetrics() to ascertain how much horizontal space the text requires, and then draws the text as many times as necessary to fill the entire width of the widget, taking offset into account.
void Ticker::showEvent(QShowEvent *) { myTimerId = startTimer(30); }
The showEvent() function starts a timer. The call to QObject::startTimer() returns an ID number, which we can use later to identify the timer. QObject supports multiple independent timers, each with its own time interval. After the call to startTimer(), Qt will generate a timer event approximately every 30 milliseconds; the accuracy depends on the underlying operating system.
We could have called startTimer() in the Ticker constructor, but we save some resources by having Qt generate timer events only when the widget is actually visible.
void Ticker::timerEvent(QTimerEvent *event) { if (event->timerId() == myTimerId) { ++offset; if (offset >= fontMetrics().width(text())) offset = 0; scroll(-1, 0); } else { QWidget::timerEvent(event); } }
The timerEvent() function is called at intervals by the system. It increments offset by 1 to simulate movement, wrapping at the width of the text. Then it scrolls the contents of the widget one pixel to the left using QWidget::scroll(). It would have been sufficient to call update() instead of scroll(), but scroll() is more efficient and prevents flicker, because it simply moves the existing pixels on screen and only generates a paint event for the widget's newly revealed area (a 1-pixel-wide strip in this case).
If the timer event isn't for the timer we are interested in, we pass it on to our base class.
void Ticker::hideEvent(QHideEvent *) { killTimer(myTimerId); }
The hideEvent() function calls QObject::killTimer() to stop the timer.
Timer events are low-level, and if we need multiple timers, it can become cumbersome to keep track of all the timer IDs. In such situations, it is usually easier to create a QTimer object for each timer. QTimer emits the timeout() signal at each time interval. QTimer also provides a convenient interface for single-shot timers (timers that time out just once).