- Description: How Qt delivers input and system events —
QEvent hierarchy, virtual handler overrides (keyPressEvent, mousePressEvent, wheelEvent, dragEnterEvent, dropEvent, paintEvent, resizeEvent, closeEvent), accept/ignore propagation, event filters (installEventFilter, eventFilter), QTimer / QTimerEvent, and custom events
- My Notion Note ID: K2A-B3-5
- Created: 2018-03-04
- Updated: 2026-05-18
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
1. Event Loop Overview
QCoreApplication::exec() runs the event loop: pulls events out of a per-thread queue, dispatches each to its target QObject by calling target->event(QEvent *).
QObject::event is the central dispatcher. Default implementation switches on e->type() and calls the matching virtual handler (keyPressEvent, paintEvent, etc.).
- Two ways to react:
- Override the per-type virtual (
void mousePressEvent(QMouseEvent *) override) — the idiomatic way; you only see events you care about.
- Override
event(QEvent *e) itself — needed for event types Qt didn't break out into a virtual (e.g., QEvent::ToolTip, QEvent::WhatsThis, custom events).
- Events are different from signals. Events are pushed into an object's queue by the framework or other code; signals are emitted out of an object after some change. A
QPushButton's click event gets translated to a clicked signal via the button's event handler.
2. QEvent Types
- Every event has a
QEvent::Type (an enum) and a concrete subclass. Common ones:
| Type |
Class |
Triggered by |
KeyPress, KeyRelease |
QKeyEvent |
Key down/up |
MouseButtonPress, MouseButtonRelease, MouseButtonDblClick, MouseMove |
QMouseEvent |
Mouse buttons + movement |
Wheel |
QWheelEvent |
Mouse wheel / trackpad scroll |
Enter, Leave |
QEnterEvent, QEvent |
Mouse cursor enters/leaves widget |
FocusIn, FocusOut |
QFocusEvent |
Keyboard focus changes |
Paint |
QPaintEvent |
Region needs repainting |
Resize |
QResizeEvent |
Widget resized |
Show, Hide |
QShowEvent, QHideEvent |
Visibility toggled |
Close |
QCloseEvent |
User clicked the window close button |
DragEnter, DragMove, DragLeave, Drop |
QDragEnterEvent, QDragMoveEvent, QDragLeaveEvent, QDropEvent |
Drag-and-drop pipeline |
Timer |
QTimerEvent |
Posted by startTimer(ms) |
ContextMenu |
QContextMenuEvent |
Right-click / context-menu key |
Shortcut |
QShortcutEvent |
Key sequence matched a QShortcut |
User ... MaxUser |
QEvent subclass you define |
QCoreApplication::postEvent |
- Full list:
QEvent::Type enum has 150+ entries — touch, gestures, native gestures, palette/font changes, IME composition, accessibility, etc.
3. Key Events
void MyWidget::keyPressEvent(QKeyEvent *event) {
if (event->key() == Qt::Key_Escape) {
close();
return;
}
if (event->modifiers().testFlag(Qt::ControlModifier) && event->key() == Qt::Key_S) {
save();
return;
}
QWidget::keyPressEvent(event);
}
event->key() is a Qt::Key enum value — Qt::Key_Return, Qt::Key_F1, Qt::Key_Home, etc. Letter keys are uppercase: Qt::Key_M, not Qt::Key_m.
event->modifiers() is a Qt::KeyboardModifiers flag set: ShiftModifier, ControlModifier, AltModifier, MetaModifier (Cmd on macOS, Win key on Windows). Test with & or testFlag.
event->text() is the printable character string — already respects Shift / IME / dead-key composition. Use it for "what should I insert into a text buffer"; use key() for "which key, regardless of layout".
event->isAutoRepeat() — true when the OS is generating repeat events from a held-down key. Skip if you only want one action per physical press.
- For a widget to receive key events at all, it needs keyboard focus:
setFocusPolicy(Qt::StrongFocus) (or ClickFocus/TabFocus). Default Qt::NoFocus swallows the events.
4. Mouse and Wheel Events
void MyWidget::mousePressEvent(QMouseEvent *event) {
if (event->button() == Qt::LeftButton) {
startPos_ = event->position().toPoint();
}
}
void MyWidget::mouseMoveEvent(QMouseEvent *event) {
if (event->buttons() & Qt::LeftButton) {
QPoint delta = event->position().toPoint() - startPos_;
scrollBy(delta);
}
}
void MyWidget::wheelEvent(QWheelEvent *event) {
int dy = event->angleDelta().y();
zoom(dy / 120);
}
- Qt 6 coords are
QPointF via event->position() / event->scenePosition() / event->globalPosition(). The Qt 5 pos() / globalPos() returning QPoint was deprecated; mixing them across Qt 5 and 6 is a common porting gotcha. position().toPoint() rounds back to integer coords when needed.
button() is the one button that triggered this specific event (only meaningful in press/release). buttons() is the current set of all pressed buttons (useful in move events: "is left still held?").
- By default a widget only receives
mouseMoveEvent while a button is held. To track hovers, setMouseTracking(true).
setOverrideCursor(QCursor(Qt::WaitCursor)) / restoreOverrideCursor() — app-wide cursor change. They're a stack; each set must be paired with a restore. For local cursor change, prefer widget->setCursor(QCursor(Qt::ArrowCursor)).
QWidget *child = childAt(event->position().toPoint());
if (auto *label = qobject_cast<QLabel *>(child)) {
QPixmap pm = label->pixmap();
}
childAt(QPoint) ignores transparent overlays — handy for picking the "real" child under a click in a complex layout.
5. Drag and Drop
- Receiving widget opts in by calling
setAcceptDrops(true), then handles the 4-stage pipeline:
class DropZone : public QWidget {
public:
DropZone(QWidget *parent = nullptr) : QWidget(parent) {
setAcceptDrops(true);
}
protected:
void dragEnterEvent(QDragEnterEvent *event) override {
if (event->mimeData()->hasUrls()) {
event->acceptProposedAction();
}
}
void dropEvent(QDropEvent *event) override {
for (const QUrl &url : event->mimeData()->urls()) {
openFile(url.toLocalFile());
}
event->acceptProposedAction();
}
};
- Stages:
dragEnterEvent — first time a drag enters the widget. Inspect event->mimeData(); if you can handle it, accept/acceptProposedAction, else ignore.
dragMoveEvent — fires repeatedly while dragging over you. Override only if drop-validity depends on cursor position (e.g., a tree where you can drop on folders but not leaves).
dragLeaveEvent — drag left without dropping. Used to clear visual feedback.
dropEvent — drop happened on you.
- Initiating a drag (the source side):
auto *drag = new QDrag(this);
auto *mime = new QMimeData;
mime->setText(label_->text());
drag->setMimeData(mime);
drag->setPixmap(label_->grab());
Qt::DropAction result = drag->exec(Qt::CopyAction | Qt::MoveAction);
6. Lifecycle Events: paint, resize, show, hide, close
- These are events Qt synthesizes from the window system or from user code.
void MyWidget::paintEvent(QPaintEvent *event) {
QPainter p(this);
}
void MyWidget::resizeEvent(QResizeEvent *event) {
QSize newSize = event->size();
QSize oldSize = event->oldSize();
}
void MainWindow::closeEvent(QCloseEvent *event) {
if (hasUnsavedChanges()) {
auto reply = QMessageBox::question(this, tr("Unsaved"),
tr("Close anyway?"),
QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) {
event->ignore();
return;
}
}
saveSettings();
event->accept();
}
event->ignore() in closeEvent keeps the window open. event->accept() (default) lets it close. Same pattern for dragEnterEvent — ignore rejects the drop.
7. Accept / Ignore and Propagation
- For input events (key, mouse, wheel), the rule is:
- If you handle it, call
event->accept() (which most handlers do implicitly).
- If you don't handle it, call
event->ignore() (or just call BaseClass::keyPressEvent(event)) — Qt then walks up the parent chain looking for a handler.
- A common bug: overriding
keyPressEvent to handle only Escape, but forgetting to chain unhandled keys to the base class. Result: arrow keys, Tab navigation, etc. all stop working.
void MyWidget::keyPressEvent(QKeyEvent *event) {
if (event->key() == Qt::Key_Escape) {
close();
return;
}
QWidget::keyPressEvent(event);
}
8. Event Filters
- Lets one
QObject spy on (and optionally intercept) events being delivered to another. Useful when you can't subclass the target — e.g., adding a behavior to a 3rd-party widget.
class ClickLogger : public QObject {
public:
bool eventFilter(QObject *watched, QEvent *event) override {
if (event->type() == QEvent::MouseButtonPress) {
qDebug() << "click on" << watched->objectName();
}
return QObject::eventFilter(watched, event);
}
};
auto *logger = new ClickLogger(this);
someButton->installEventFilter(logger);
anotherButton->installEventFilter(logger);
eventFilter returns true to swallow the event (no further processing, no virtual handler called), false to let it continue normally.
- Multiple filters chain in last-installed-first order. Removing:
target->removeEventFilter(filter), or just delete the filter.
qApp->installEventFilter(filter) installs an application-wide filter that sees every event going to every object — powerful but expensive; only for debugging or low-level features like global key hooks.
9. Timers
- Two ways: high-level
QTimer, low-level startTimer/QTimerEvent.
auto *timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &MyWidget::onTick);
timer->start(1000);
QTimer::singleShot(500, this, &MyWidget::deferred);
class MyWidget : public QWidget {
int timerId_ = 0;
public:
void start() { timerId_ = startTimer(16); }
protected:
void timerEvent(QTimerEvent *event) override {
if (event->timerId() == timerId_) {
tick();
}
}
};
QTimer is a QObject, has signals/slots overhead, and creates one timer-id per instance. Fine for a handful per window.
startTimer is cheaper if you need many — QGraphicsItem animations historically used it. The default timer type is coarse (~5% accuracy on most OSes); for ~1 ms accuracy, opt in:
auto *timer = new QTimer(this);
timer->setTimerType(Qt::PreciseTimer);
timer->start(16);
Qt::PreciseTimer is millisecond-level, not sub-millisecond — for nanosecond-precision needs, see QChronoTimer.
10. Custom Events
- For posting your own events into the loop — useful for cross-thread "do this on the GUI thread" without writing a slot.
class FileLoadedEvent : public QEvent {
public:
static constexpr Type kType = static_cast<Type>(QEvent::User + 1);
explicit FileLoadedEvent(QByteArray data)
: QEvent(kType), data_(std::move(data)) {}
QByteArray data_;
};
QCoreApplication::postEvent(receiver, new FileLoadedEvent(bytes));
bool MyReceiver::event(QEvent *e) {
if (e->type() == FileLoadedEvent::kType) {
const auto *fe = static_cast<FileLoadedEvent *>(e);
onLoaded(fe->data_);
return true;
}
return QObject::event(e);
}
postEvent is asynchronous — the event is queued, dispatched when the receiver's thread runs its event loop. The receiver takes ownership of the event and must not delete it.
sendEvent is synchronous — handler runs immediately in the caller's thread. Use it for testing or when you want a synchronous round-trip; don't use it for cross-thread dispatch.
- For most "do this in the GUI thread" cases,
QMetaObject::invokeMethod(receiver, fn, Qt::QueuedConnection) is shorter than rolling a custom event.
11. References