Qt Event System


  • 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);    // chain to base for default behavior
}
  • 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();        // 120 per notch on most mice
    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)).
// "what widget is under the cursor inside me right now?"
QWidget *child = childAt(event->position().toPoint());
if (auto *label = qobject_cast<QLabel *>(child)) {
    QPixmap pm = label->pixmap();          // Qt 6: returns by value (used to be pointer)
    // ...
}
  • 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();         // tell OS "yes, I want this drop"
        }
    }

    void dropEvent(QDropEvent *event) override {
        for (const QUrl &url : event->mimeData()->urls()) {
            openFile(url.toLocalFile());
        }
        event->acceptProposedAction();
    }
};
  • Stages:
    1. dragEnterEvent — first time a drag enters the widget. Inspect event->mimeData(); if you can handle it, accept/acceptProposedAction, else ignore.
    2. 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).
    3. dragLeaveEvent — drag left without dropping. Used to clear visual feedback.
    4. 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());                  // visual feedback during drag
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);
    // paint stuff
}

void MyWidget::resizeEvent(QResizeEvent *event) {
    QSize newSize = event->size();
    QSize oldSize = event->oldSize();             // may be QSize(-1, -1) on first show
    // re-layout custom-drawn content
}

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();                       // veto the close
            return;
        }
    }
    saveSettings();
    event->accept();
}
  • event->ignore() in closeEvent keeps the window open. event->accept() (default) lets it close. Same pattern for dragEnterEventignore 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);    // <-- crucial
}

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);   // false = let it through
    }
};

auto *logger = new ClickLogger(this);
someButton->installEventFilter(logger);
anotherButton->installEventFilter(logger);            // same filter, multiple targets
  • 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.
// High-level — preferred
auto *timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &MyWidget::onTick);
timer->start(1000);                  // every 1000 ms

QTimer::singleShot(500, this, &MyWidget::deferred);   // one-shot, fires once after 500 ms
// Low-level — for tight loops or thousands of timers per object
class MyWidget : public QWidget {
    int timerId_ = 0;
public:
    void start() { timerId_ = startTimer(16); }       // ~60 FPS

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);     // ~1 ms accuracy; CoarseTimer is the default
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_;
};

// Sender (could be a worker thread):
QCoreApplication::postEvent(receiver, new FileLoadedEvent(bytes));

// Receiver — override event():
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