Qt Timer & Event Loop

QTimer, QEventLoop, event processing, custom events, and the Qt event system explained with examples and labs.

7 min read
50566 chars

What Is the Qt Event Loop?

The Qt event loop is the heartbeat of every Qt application. It continuously waits for events (mouse clicks, timer ticks, socket data, signals) and dispatches them to the right objects.

app.exec()
    ↓
┌─────────────────────────────────────────┐
│           Event Queue                   │
│  [TimerEvent] [MouseEvent] [KeyEvent]   │
└──────────────────┬──────────────────────┘
                   ↓
          QCoreApplication::notify()
                   ↓
         QObject::event(QEvent*)
                   ↓
           Specific handler:
     timerEvent() / mousePressEvent() ...

QTimer — The Simplest Way to Schedule Work

Single-Shot Timer

#include <QTimer>

// Fire once after 2 seconds
QTimer::singleShot(2000, []() {
    qDebug() << "2 seconds elapsed!";
});

Repeating Timer

QTimer *timer = new QTimer(this);
timer->setInterval(1000);       // every 1 second
timer->setSingleShot(false);

connect(timer, &QTimer::timeout, this, &MyClass::onTick);
timer->start();

// ... later
timer->stop();

Precise Timer

QTimer *preciseTimer = new QTimer(this);
preciseTimer->setTimerType(Qt::PreciseTimer);   // OS high-res timer
preciseTimer->setInterval(10);                  // 10 ms
preciseTimer->start();

Timer Type Comparison:

TypeAccuracyCPUUse Case
Qt::CoarseTimer±5%LowUI updates, polling
Qt::PreciseTimer~1msHigherAudio, sampling
Qt::VeryCoarseTimer~1sLowestHeartbeat, watchdog

QElapsedTimer — High-Resolution Timing

#include <QElapsedTimer>

QElapsedTimer clock;
clock.start();

// ... some operation ...
heavyComputation();

qDebug() << "Elapsed:" << clock.elapsed() << "ms";
qDebug() << "Elapsed ns:" << clock.nsecsElapsed();

// Measure multiple sections
clock.restart();
step1();
qint64 t1 = clock.elapsed();
step2();
qint64 t2 = clock.elapsed();
qDebug() << "step1:" << t1 << "ms  step2:" << (t2-t1) << "ms";

Event Handling — QEvent & virtual handlers

Every QWidget subclass can override event handlers:

#include <QWidget>
#include <QMouseEvent>
#include <QKeyEvent>
#include <QPaintEvent>
#include <QTimerEvent>

class Canvas : public QWidget {
    Q_OBJECT
    int m_timerId = 0;
    QPoint m_cursor;

protected:
    // Called every frame
    void timerEvent(QTimerEvent *e) override {
        if (e->timerId() == m_timerId)
            update();  // schedule repaint
    }

    void mouseMoveEvent(QMouseEvent *e) override {
        m_cursor = e->pos();
        update();
    }

    void keyPressEvent(QKeyEvent *e) override {
        if (e->key() == Qt::Key_Space) {
            qDebug() << "Space pressed";
            e->accept();
        } else {
            e->ignore();  // pass to parent
        }
    }

    void paintEvent(QPaintEvent *) override {
        QPainter p(this);
        p.drawEllipse(m_cursor, 10, 10);
    }

    void showEvent(QShowEvent *) override {
        m_timerId = startTimer(16);  // ~60 fps
    }

    void hideEvent(QHideEvent *) override {
        killTimer(m_timerId);
    }
};

Event Filters — Intercept Events on Any Object

class GlobalFilter : public QObject {
    Q_OBJECT
protected:
    bool eventFilter(QObject *watched, QEvent *event) override {
        if (event->type() == QEvent::KeyPress) {
            auto *key = static_cast<QKeyEvent*>(event);
            qDebug() << watched->objectName()
                     << "key pressed:" << key->key();
            // Return true to eat the event, false to pass it on
            if (key->key() == Qt::Key_F12) return true;  // swallow
        }
        return QObject::eventFilter(watched, event);  // normal processing
    }
};

// Install on specific object
GlobalFilter *filter = new GlobalFilter(this);
lineEdit->installEventFilter(filter);

// Install on entire app
qApp->installEventFilter(filter);

Custom Events

// Define custom event type
const QEvent::Type DeviceDataEvent =
    static_cast<QEvent::Type>(QEvent::User + 1);

// Custom event class
class DeviceEvent : public QEvent {
public:
    explicit DeviceEvent(const QByteArray &data)
        : QEvent(DeviceDataEvent), m_data(data) {}

    QByteArray data() const { return m_data; }

private:
    QByteArray m_data;
};

// Post event (thread-safe!)
QCoreApplication::postEvent(targetObject, new DeviceEvent(rawBytes));

// Handle in receiver
void MyReceiver::customEvent(QEvent *event) {
    if (event->type() == DeviceDataEvent) {
        auto *de = static_cast<DeviceEvent*>(event);
        processData(de->data());
    }
}

processEvents() — Keep UI Alive During Loops

// Long operation that must stay on main thread
void MainWindow::flashFirmware(const QByteArray &firmware) {
    int total = firmware.size();
    for (int i = 0; i < total; i += 256) {
        writeChunk(firmware.mid(i, 256));
        m_progressBar->setValue(i * 100 / total);

        // Allow UI to repaint and process queued events
        QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
    }
    m_progressBar->setValue(100);
}

Prefer worker threads. Use processEvents only for short, bounded loops.


Visual: Event Loop Flow

┌──────────────────────────────────────────────────────────┐
│                     app.exec()                           │
│                                                          │
│  ┌───────────────┐   dequeue    ┌────────────────────┐  │
│  │  Event Queue  │ ──────────►  │ QCoreApplication   │  │
│  │               │              │   ::notify()       │  │
│  │  [TimerEvent] │              └──────────┬─────────┘  │
│  │  [UserEvent]  │                         │            │
│  │  [PaintEvent] │              ┌──────────▼─────────┐  │
│  │  [InputEvent] │              │  QObject::event()  │  │
│  └───────────────┘              │  (virtual dispatch)│  │
│           ▲                     └──────────┬─────────┘  │
│           │ postEvent()                    │            │
│    other threads                  specific handlers     │
└──────────────────────────────────────────────────────────┘

Lab 1 — Stopwatch with QTimer

class Stopwatch : public QWidget {
    Q_OBJECT
    QTimer       *m_timer;
    QElapsedTimer m_elapsed;
    QLabel       *m_display;
    bool          m_running = false;

public:
    Stopwatch(QWidget *p = nullptr) : QWidget(p) {
        auto *lay   = new QVBoxLayout(this);
        m_display   = new QLabel("00:00.000");
        m_display->setAlignment(Qt::AlignCenter);
        m_display->setFont(QFont("Courier", 32, QFont::Bold));

        auto *startBtn = new QPushButton("Start");
        auto *resetBtn = new QPushButton("Reset");

        lay->addWidget(m_display);
        lay->addWidget(startBtn);
        lay->addWidget(resetBtn);

        m_timer = new QTimer(this);
        m_timer->setInterval(50);  // update every 50ms
        connect(m_timer, &QTimer::timeout, this, &Stopwatch::tick);
        connect(startBtn, &QPushButton::clicked, this, &Stopwatch::toggle);
        connect(resetBtn, &QPushButton::clicked, this, &Stopwatch::reset);
    }

private slots:
    void toggle() {
        if (m_running) {
            m_timer->stop();
            m_running = false;
        } else {
            m_elapsed.start();
            m_timer->start();
            m_running = true;
        }
    }

    void reset() {
        m_timer->stop();
        m_running = false;
        m_display->setText("00:00.000");
    }

    void tick() {
        qint64 ms = m_elapsed.elapsed();
        int min  = ms / 60000;
        int sec  = (ms % 60000) / 1000;
        int msec = ms % 1000;
        m_display->setText(QString("%1:%2.%3")
            .arg(min, 2, 10, QChar('0'))
            .arg(sec, 2, 10, QChar('0'))
            .arg(msec, 3, 10, QChar('0')));
    }
};

Lab 2 — Watchdog Timer

class Watchdog : public QObject {
    Q_OBJECT
    QTimer *m_timer;
    int     m_timeoutMs;
public:
    explicit Watchdog(int ms, QObject *p = nullptr)
        : QObject(p), m_timeoutMs(ms)
    {
        m_timer = new QTimer(this);
        m_timer->setSingleShot(true);
        connect(m_timer, &QTimer::timeout, this, &Watchdog::onTimeout);
    }

    void kick() {
        // Reset the watchdog
        m_timer->start(m_timeoutMs);
    }

    void stop() { m_timer->stop(); }

signals:
    void timedOut();

private slots:
    void onTimeout() {
        qWarning() << "Watchdog timeout! No kick for" << m_timeoutMs << "ms";
        emit timedOut();
    }
};

// Usage
Watchdog *wd = new Watchdog(5000, this);  // 5-second watchdog
connect(wd, &Watchdog::timedOut, this, &MyApp::handleWatchdogTimeout);
wd->kick();  // start it

// In your main loop / sensor read
void onSensorData() {
    wd->kick();  // reset timeout
    processData();
}

Interview Questions

Q1: What is QEventLoop and when would you use it manually?

QEventLoop is the class that drives event processing. Normally QApplication::exec() runs it. Use it manually in a worker thread when you need signals/slots to work in that thread (e.g., QEventLoop loop; loop.exec();), or to block until an async operation completes.

Q2: What is the difference between postEvent and sendEvent?

sendEvent delivers the event synchronously — the receiver’s event() is called before returning, in the caller’s thread. postEvent enqueues the event asynchronously — it is processed in the receiver’s thread on the next event loop iteration. postEvent is thread-safe; sendEvent is not.

Q3: What happens when QTimer::timeout fires but the event loop is blocked?

The timeout event is queued but cannot be dispatched until the event loop regains control. Timer accuracy degrades. This is why you should never block the main thread with long operations.

Q4: How do you implement a timeout for an async operation?

Use a QTimer::singleShot. If the operation completes first, stop the timer. If the timer fires first, handle the timeout and cancel the operation.

Q5: What is an event filter and how does it differ from overriding event()?

An event filter is installed on a specific QObject and intercepts events before they reach the object. Overriding event() only applies to the class itself. Filters can be installed from outside the class (e.g., to intercept events on widgets you don’t own).


Application: Periodic Sensor Poller

class SensorPoller : public QObject {
    Q_OBJECT
    QTimer      *m_pollTimer;
    QTimer      *m_watchdog;
    QElapsedTimer m_uptime;
    int           m_readCount = 0;

public:
    SensorPoller(QObject *p = nullptr) : QObject(p) {
        // Poll every 500ms
        m_pollTimer = new QTimer(this);
        m_pollTimer->setInterval(500);
        connect(m_pollTimer, &QTimer::timeout, this, &SensorPoller::poll);

        // Watchdog: if no reads for 5s → alarm
        m_watchdog = new QTimer(this);
        m_watchdog->setSingleShot(true);
        m_watchdog->setInterval(5000);
        connect(m_watchdog, &QTimer::timeout, this, &SensorPoller::onWatchdog);

        m_uptime.start();
    }

    void start() {
        m_pollTimer->start();
        m_watchdog->start(5000);
    }

    void stop() {
        m_pollTimer->stop();
        m_watchdog->stop();
    }

signals:
    void dataReady(float temp, float volt);
    void watchdogAlarm();

private slots:
    void poll() {
        float temp = 22.0f + (rand() % 60) / 10.0f;
        float volt = 3.2f  + (rand() % 20) / 100.0f;
        ++m_readCount;
        m_watchdog->start(5000);  // kick
        emit dataReady(temp, volt);
        qDebug() << QString("[%1s] Read #%2: T=%3 V=%4")
            .arg(m_uptime.elapsed() / 1000)
            .arg(m_readCount)
            .arg(temp, 0, 'f', 1)
            .arg(volt, 0, 'f', 2);
    }

    void onWatchdog() {
        qWarning() << "No sensor reads for 5 seconds!";
        emit watchdogAlarm();
    }
};

References

ResourceLink
Qt Docs: QTimerhttps://doc.qt.io/qt-6/qtimer.html
Qt Docs: QElapsedTimerhttps://doc.qt.io/qt-6/qelapsedtimer.html
Qt Docs: Event Systemhttps://doc.qt.io/qt-6/eventsandfilters.html
Qt Docs: QCoreApplicationhttps://doc.qt.io/qt-6/qcoreapplication.html

Next tutorial → Qt Core Containers