走进Qt--事件处理解析

617 阅读25分钟

前言

在Qt框架中,事件(Event)是比信号槽更底层的交互机制。当用户点击鼠标(QMouseEvent)、按下键盘(QKeyEvent),甚至当系统要求重绘界面(QPaintEvent)时,Qt都会将这些动作封装为事件对象,通过精密的事件循环(Event Loop)分发给目标控件。理解事件机制,意味着您将获得:

  • 绝对控制权:拦截并修改任何GUI交互行为
  • 性能优化空间:精准控制事件处理流程,避免无效重绘
  • 扩展能力:实现Qt原生未提供的交互范式(如复杂手势识别)

为什么信号槽不够用?

虽然信号槽是Qt的标志性特性,但在以下场景中必须使用事件处理:

场景信号槽的局限事件方案的优势
阻止按钮点击事件只能接收点击,无法拦截可在mousePressEvent中拦截
实时鼠标轨迹处理信号会有延迟和遗漏直接获取原始输入事件
自定义控件绘图无对应信号通过paintEvent完全控制

本文目的是从 Qt 的事件基础讲起,介绍常见事件类型、处理方式、事件过滤器、自定义事件等机制,并通过大量典型案例帮助读者理解事件在实际开发中的应用。


1 Qt 事件机制基础

Qt 是一个典型的事件驱动框架,所有交互行为的背后都离不开事件的触发、传递与响应机制。本章将从理论和机制两个层面系统介绍 Qt 的事件处理模型。

1.1 什么是事件(Event)?

事件(Event)是一种用来描述外部或内部变化的消息对象。例如鼠标点击、键盘按下、窗口最小化、定时器到时等,这些操作会被系统封装成特定的 QEvent 对象并传入对应控件中。

在 Qt 中,信号槽机制事件机制经常被混合使用,但它们有本质区别:

特性信号槽(Signals & Slots)事件机制(Events)
触发方式手动调用 emit由 Qt 自动生成和派发
控制粒度高层交互(如按钮点击)底层响应(如鼠标进入)
处理机制明确连接响应函数重写虚函数处理,或安装事件过滤器
是否可拦截是,事件可以被过滤或阻止传播
开发复杂度简单明了更底层,需理解分发流程
应用场景通用逻辑触发精细化交互处理与控件自定义

结论:信号槽更适合处理业务逻辑,事件机制更适合底层交互控制。

1.2 常见事件分类

Qt 提供了丰富的事件类型,不同事件通过继承 QEvent 类型进行分类。以下是开发中最常遇到的几类:

输入事件

  • 鼠标事件:QMouseEvent,处理鼠标按下、释放、移动、双击等
  • 键盘事件:QKeyEvent,处理键盘按下/释放
  • 滚轮事件:QWheelEvent

定时器事件

  • QTimer 启动后触发的 timerEvent

绘图事件

  • paintEvent,控件重绘时调用,绘图必须在此执行

自定义事件

  • 用户可继承 QEvent 自定义事件类型,用于跨线程通信、任务调度等场景

系统事件与窗口事件

  • closeEvent(窗口关闭)、resizeEvent(窗口调整)、dragEnterEvent(拖入)等

1.3 事件的接收与处理方式

Qt 提供三种处理事件的方式,按复杂度递增如下:

1. 重写虚函数

如重写 mousePressEvent() 实现鼠标点击响应:

void MyWidget::mousePressEvent(QMouseEvent* event) {
    qDebug() << "Mouse clicked at:" << event->pos();
}

优点:直接、简单;

缺点:必须继承控件类,不能复用逻辑。

2. 安装事件过滤器

使用 QObject::installEventFilter() 可以在其它对象中监控任意控件的事件:

bool MyFilter::eventFilter(QObject* obj, QEvent* event) {
    if (event->type() == QEvent::MouseMove) {
        qDebug() << "Mouse moved!";
        return true; // 拦截事件,不再向下传递
    }
    return QObject::eventFilter(obj, event);
}

3. 自定义事件 + 自定义事件循环

通过继承 QEvent 创建新事件类型,配合 postEvent()sendEvent() 使用,实现线程间或系统级通信。

class MyEvent : public QEvent {
public:
    static Type MyType;
    MyEvent() : QEvent(MyType) {}
};
QEvent::Type MyEvent::MyType = static_cast<QEvent::Type>(QEvent::User + 1);

1.4 事件传递机制详解

Qt 的事件处理机制是基于「事件循环」的。应用启动后会进入一个主事件循环(event loop),不断从事件队列中取出事件并进行处理:

事件派发时序图

sequenceDiagram
    participant App as QApplication
    participant Receiver as QObject
    participant Parent as QWidget
    
    App->>Receiver: notify(event)
    alt 已安装事件过滤器
        Receiver->>Filter: eventFilter(event)
        Filter-->>Receiver: 返回bool结果
    end
    Receiver->>Receiver: event()
    Receiver->>Receiver: specificEvent()
    alt 事件未被接受且存在父对象
        Receiver->>Parent: 传递事件
    end

事件传播的两种形式

1.向上传递(子→父)
// 示例:子控件忽略事件后父控件处理
void ChildWidget::mousePressEvent(QMouseEvent *e) {
    if (e->button() != Qt::LeftButton) {
        e->ignore();  // 传递给父控件
    } else {
        // 处理左键点击...
    }
}

void ParentWidget::mousePressEvent(QMouseEvent *e) {
    qDebug() << "父控件处理事件:" << e->pos();
}
2.手动转发(任意对象)
// 将事件转发给另一个对象
QApplication::sendEvent(otherObject, event);

sendEvent() VS postEvent()对比

特性sendEvent()postEvent()
执行时机立即同步执行异步,加入事件队列
内存管理需手动delete eventQt自动删除
线程安全性必须在目标对象所在线程调用支持跨线程(自动排队)
事件循环依赖不依赖必须运行事件循环
典型应用场景必须立即处理的紧急事件耗时操作完成后的通知

底层实现差异:

// sendEvent伪实现
bool sendEvent(QObject *receiver, QEvent *event) {
    return receiver->event(event);  // 直接调用
}

// postEvent伪实现
void postEvent(QObject *receiver, QEvent *event) {
    QCoreApplication::postEventList.append({receiver, event});  // 加入队列
}

sendEvent/postEvent时序图:

sequenceDiagram
    participant Sender
    participant Receiver
    participant EventLoop
    
    Sender->>Receiver: sendEvent (同步)
    Receiver-->>Sender: 立即返回结果
    
    Sender->>EventLoop: postEvent (异步)
    EventLoop->>Receiver: 下次事件循环处理

事件接受控制策略

accept()与ignore()的作用

在 Qt 中,事件对象(如 QEvent)会被从上到下传递(父 → 子),每个接收到事件的对象都可以选择:

  • 接受事件:event->accept();
  • 忽略事件:event->ignore();

这两个函数并不“处理”事件本身,而是告知 Qt:我是否已经处理这个事件,是否允许它继续传播

accept()与ignore()的实质

// QEvent基类中的标志位
class QEvent {
public:
    void accept() { m_accept = true; }
    void ignore() { m_accept = false; }
    bool isAccepted() const { return m_accept; }
private:
    bool m_accept = true;  // 默认接受
};

accept():告诉事件分发系统“这个事件我处理了”,不会再传给其他对象(如父控件、上层过滤器)

ignore():告诉系统“我没处理”,允许事件继续向上传递或被其他对象处理

实际开发的黄金准则

使用场景建议操作说明
自定义控件中处理了鼠标/键盘行为,不希望外部干预event->accept();阻断事件传递,避免父控件响应
子控件未响应某些事件,希望交由父控件处理event->ignore();比如一个未响应滚轮事件的控件
仅监视事件(不想影响传递)不调用任何方法默认即为 ignore,事件将继续传播

示例:鼠标事件阻断传播

void MyWidget::mousePressEvent(QMouseEvent *event) {
    qDebug() << "MyWidget clicked";
    event->accept(); // 阻断传递,父控件不会再收到这个事件
}

1.5 事件过滤器机制:installEventFiltereventFilter

Qt 提供了一种灵活的事件拦截机制 —— 事件过滤器,适用于需要监视或干预其他对象事件的场景。例如,在一个控件还未响应前拦截鼠标或键盘事件。

基本原理

  • 所有 QObject 派生类都可以调用 installEventFilter(QObject *filterObj) 安装事件过滤器。
  • 被安装后,filterObj 会收到安装对象的所有事件通知,前提是你重写了 bool eventFilter(QObject *watched, QEvent *event)
  • eventFilter 中返回 true 表示事件被拦截处理,不会再传递给原控件;返回 false 表示放行。

使用步骤

  1. 让你的类继承自 QObject 或已有控件;
  2. 重写 eventFilter()
  3. 调用 installEventFilter(this);

示例:在父窗口中拦截子控件的键盘事件

bool MainWindow::eventFilter(QObject* watched, QEvent* event) {
    if (watched == ui->lineEdit && event->type() == QEvent::KeyPress) {
        QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
        if (keyEvent->key() == Qt::Key_Return) {
            qDebug() << "回车键被拦截!";
            return true; // 事件不再传递给 lineEdit
        }
    }
    return false; // 继续传递
}

void MainWindow::setupFilter() {
    ui->lineEdit->installEventFilter(this);
}

适用场景

  • 控制第三方或封装控件行为(不方便修改源码)
  • 拦截特殊键盘/鼠标事件
  • 局部功能控制(例如禁止某个控件输入)

注意事项

  • 滥用事件过滤器会导致逻辑耦合混乱,应仅在信号槽不适用或逻辑需提前处理时使用;
  • Qt 提供的事件类型非常丰富,请搭配 QEvent::Type 判断具体事件种类。

2 典型事件处理案例

2.1 鼠标事件(mousePress/Release/Move/DoubleClick/Enter/Leave)

在 Qt 中,鼠标事件是最常见的用户交互形式之一,适用于窗口拖动、控件响应、绘图处理等场景。Qt 为我们提供了一整套鼠标事件处理接口,通过重写 QWidget 的相关虚函数,即可实现细粒度的交互控制。

常用鼠标事件函数

函数名说明
mousePressEvent(QMouseEvent*)鼠标按下时触发
mouseReleaseEvent(QMouseEvent*)鼠标释放时触发
mouseMoveEvent(QMouseEvent*)鼠标移动时触发(需要先按下)
mouseDoubleClickEvent(QMouseEvent*)双击触发事件
enterEvent(QEnterEvent*)鼠标进入控件区域时触发
leaveEvent(QEvent*)鼠标离开控件区域时触发

示例

案例目标

展示如何处理 mousePressEventmouseReleaseEventmouseMoveEvententerEventleaveEventmouseDoubleClickEvent

使用一个主窗口显示反馈信息。

使用一个 QTextEdit 控件实时记录事件顺序。

示例代码

MouseEventDemo.h

#pragma once

#include <QtWidgets/QWidget>
#include<qlabel.h>
#include<qtextedit.h>   


class MouseEventDemo : public QWidget
{
    Q_OBJECT

public:
    MouseEventDemo(QWidget *parent = nullptr);
    ~MouseEventDemo();

protected:
    //鼠标按下
    void mousePressEvent(QMouseEvent* event)override;
    //鼠标释放
    void mouseReleaseEvent(QMouseEvent* event)override;
    //鼠标移动
    void mouseMoveEvent(QMouseEvent* event)override;
    //鼠标进入
    void enterEvent(QEnterEvent* event)override;
    //鼠标离开
    void leaveEvent(QEvent* event)override;
    //鼠标双击
    void mouseDoubleClickEvent(QMouseEvent* event) override;

private:
    QLabel* statusLabel;
    QTextEdit* logEdit;

    //鼠标动作记录
    void logEvent(const QString& text);
};

MouseEventDemo.cpp

#include "MouseEventDemo.h"
#include<QVBoxLayout>
#include<QMouseEvent>
#include<qdatetime.h>

MouseEventDemo::MouseEventDemo(QWidget *parent)
    : QWidget(parent)
{
    resize(400, 300);
    setWindowTitle("鼠标事件演示");

    QVBoxLayout* mainLayout = new QVBoxLayout(this);
    statusLabel = new QLabel("等待鼠标事件...", this);
    statusLabel->setStyleSheet("font-weight:bold;font-size:16px");

    logEdit = new QTextEdit(this);
    logEdit->setReadOnly(true);

    mainLayout->addWidget(statusLabel);
    mainLayout->addWidget(logEdit);
    setMouseTracking(true); //启动鼠标移动事件
    
}

MouseEventDemo::~MouseEventDemo()
{}

void MouseEventDemo::mousePressEvent(QMouseEvent * event)
{
    QString info = QString("鼠标按下:按钮 = %1,位置=(%2,%3)")
        .arg(event->button() == Qt::LeftButton ? "左键" : "其他")
        .arg(event->position().x())
        .arg(event->position().y());
    statusLabel->setText(info);
    logEvent(info);
}

void MouseEventDemo::mouseReleaseEvent(QMouseEvent* event)
{
    QString info = QString("鼠标释放:按钮 = %1,位置=(%2,%3)")
        .arg(event->button() == Qt::LeftButton ? "左键" : "其他")
        .arg(event->position().x())
        .arg(event->position().y());
    statusLabel->setText(info);
    logEvent(info);
}

void MouseEventDemo::mouseMoveEvent(QMouseEvent* event)
{
    QString info = QString("鼠标移动位置=(%1,%2)")
        .arg(event->position().x())
        .arg(event->position().y());
    statusLabel->setText(info);
    logEvent(info);
}

void MouseEventDemo::enterEvent(QEnterEvent* event)
{
    Q_UNUSED(event);
    QString info = "鼠标进入窗口区域";
    statusLabel->setText(info);
    logEvent(info);
}

void MouseEventDemo::leaveEvent(QEvent* event)
{
    Q_UNUSED(event);
    QString info = "鼠标离开窗口区域";
    statusLabel->setText(info);
    logEvent(info);
}

void MouseEventDemo::mouseDoubleClickEvent(QMouseEvent* event)
{
    QString info = QString("鼠标双击:按钮 = %1,位置=(%2,%3)")
        .arg(event->button() == Qt::LeftButton ? "左键" : "其他")
        .arg(event->position().x())
        .arg(event->position().y());
    statusLabel->setText(info);
    logEvent(info);
}

void MouseEventDemo::logEvent(const QString& text)
{
    QString  time = QDateTime::currentDateTime().toString("hh:mm:ss");
    logEdit->append(QString("[%1] %2").arg(time).arg(text));
}
效果展示

image.png

2.2 键盘事件(keyPressEvent/keyReleaseEvent)

Qt 中的键盘事件主要通过两个虚函数处理:

  • void keyPressEvent(QKeyEvent* event):按键按下时触发;
  • void keyReleaseEvent(QKeyEvent* event):按键释放时触发。

这两个函数都属于 QWidget 类的虚函数,因此要使用键盘事件,需要在子类中进行重写。

Qt 使用 QKeyEvent 类封装键盘事件,它提供了一系列有用的方法来获取键值、字符、修饰键等信息。

常用函数与属性

函数 / 属性说明
int key()返回按键的 Qt 键码(如 Qt::Key_A
QString text()返回用户输入的实际字符(受修饰键影响)
Qt::KeyboardModifiers modifiers()获取修饰键(Ctrl、Shift、Alt 等)
bool isAutoRepeat()判断是否为自动重复按键
int count()连续敲击次数(较少使用)

常用Qt键值(部分)

键值说明
AQt::Key_A字母 A
EnterQt::Key_Return回车
EscQt::Key_Escape退出
CtrlQt::ControlModifier修饰键(用于组合)
ShiftQt::ShiftModifier修饰键
F1Qt::Key_F1功能键
左方向键Qt::Key_Left光标左移

键盘事件处理步骤概述

  1. 设置焦点策略 默认情况下某些控件(如 QWidget)不会主动接收键盘焦点,需要设置:

    setFocusPolicy(Qt::StrongFocus);
    
  2. 重写事件函数 在继承的 QWidget/QMainWindow 中重写:

    void keyPressEvent(QKeyEvent* event) override;
    void keyReleaseEvent(QKeyEvent* event) override;
    
  3. 获取按键信息并响应逻辑

示例

在该示例中,我们实现:

  • 切换“常规模式”与“移动模式”;
  • 在移动模式下,通过键盘控制 QLabel 移动;
  • 在常规模式下,记录用户键盘操作;
  • 所有操作事件都实时记录在 QTextEdit 中。
示例代码

KeyboardEventDemo.h

#pragma once

#include <QtWidgets/QWidget>
#include<qpushbutton.h>
#include<qtextedit.h>
#include<qlabel.h>
#include<QkeyEvent>
#include<QVBoxLayout>
#include<QHBoxLayout>
#include<qframe.h>


class KeyboardEventDemo : public QWidget
{
    Q_OBJECT

public:
    KeyboardEventDemo(QWidget *parent = nullptr);
    ~KeyboardEventDemo();

protected:
    void keyPressEvent(QKeyEvent* event) override;
    void keyReleaseEvent(QKeyEvent* event) override;

private:
    QPushButton* startBtn;
    QPushButton* stopBtn;
    QTextEdit* eventLog;
    QLabel* movableLabel;
    QFrame* moveArea;
    bool moveMode;

};

KeyboardEventDemo.cpp

#include "KeyboardEventDemo.h"

KeyboardEventDemo::KeyboardEventDemo(QWidget *parent)
    : QWidget(parent)
{
    setWindowTitle("键盘事件演示");
    setFixedSize(400, 400);

    startBtn = new QPushButton("开始移动");
    stopBtn = new QPushButton("停止移动");
    eventLog = new QTextEdit();
    eventLog->setReadOnly(true);

    movableLabel = new QLabel();
    movableLabel->setFixedSize(40, 40);
    movableLabel->setStyleSheet("background-color:lightblue;text-align:center;");
    movableLabel->move(10, 10);

    moveArea = new QFrame(this);
    moveArea->setFrameShape(QFrame::Box);
    moveArea->setFixedSize(360, 200);
    moveArea->setStyleSheet("background-color:#f0f0f0;");

    movableLabel->setParent(moveArea);

    QVBoxLayout* btnLayout = new QVBoxLayout();
    btnLayout->addWidget(startBtn);
    btnLayout->addWidget(stopBtn);

    QHBoxLayout* eventLogBtnLayout = new QHBoxLayout();
    eventLogBtnLayout->addWidget(eventLog);
    eventLogBtnLayout->addLayout(btnLayout);

    QVBoxLayout* mainLayout = new QVBoxLayout(this);
    mainLayout->addWidget(moveArea);
    mainLayout->addLayout(eventLogBtnLayout);

    connect(startBtn, &QPushButton::clicked, this, [=]() {
        moveMode = true;
        eventLog->append("进入移动模式");
        });
    connect(stopBtn, &QPushButton::clicked, this, [=]() {
        moveMode = false;
        eventLog->append("退出移动模式");
        });

}

KeyboardEventDemo::~KeyboardEventDemo()
{}

void KeyboardEventDemo::keyPressEvent(QKeyEvent * event)
{
    if (moveMode) {
        QPoint pos = movableLabel->pos();
        switch (event->key())
        {
        case Qt::Key_W:
        case Qt::Key_Up:
            pos.ry() -= 10;
            break;
        case Qt::Key_S:
        case Qt::Key_Down:
            pos.ry() += 10;
            break;
        case Qt::Key_A:
        case Qt::Key_Left:
            pos.rx() -= 10; 
            break;
        case Qt::Key_D:
        case Qt::Key_Right:
            pos.rx() += 10;
            break;
        default:
            eventLog->append(QString("未知控制键:%1").arg(event->text()));
            return;
        }

        //边界限制
        pos.setX(qBound(0, pos.x(), moveArea->width() - movableLabel->width()));
        pos.setY(qBound(0, pos.y(), moveArea->height() - movableLabel->height()));

        movableLabel->move(pos);
        eventLog->append(QString("移动到(%1,%2)").arg(pos.x()).arg(pos.y()));
    }
    else {
        eventLog->append(QString("常规模式按下:%1").arg(event->text()));
    }
}

void KeyboardEventDemo::keyReleaseEvent(QKeyEvent* event)
{
    if (!moveMode)eventLog->append(QString("按键释放:%1").arg(event->text()));
}
效果展示

初始界面:

image.png

常规状态:

image.png

移动状态:

image.png

2.3 计时器事件(timerEvent / QTimer)

在 Qt 中,计时器广泛用于周期性任务,如动画播放、定时刷新、数据轮询等。Qt 提供两种常见方式:

类型描述示例适用场景
timerEvent低级方式,继承类中实现自定义事件循环
QTimer更常用,高级方式,信号槽界面更新、倒计时等

常用函数与机制

  1. QObject::startTimer(int interval)
  • 启动一个计时器,单位为毫秒。
  • 返回值是计时器 ID(int)。
  • 需要重写 void timerEvent(QTimerEvent *event) 来处理。
  1. QObject::killTimer(int id)
  • 停止一个指定 ID 的计时器。
  1. QTimer
  • 提供更直观的控制方式(start, stop, timeout 信号)。
  • 支持单次 / 重复计时。
  • 可与 lambda、槽函数结合使用。

示例

模拟钟摆循环运动

功能组成:

组件功能描述
QLabel模拟钟摆,通过水平位置来回摆动
QTimer控制定时移动(来回)
QTextEdit记录每次摆动循环的开始 / 结束时间
QProgressBar显示当前完成的循环次数
QPushButton启动 / 停止运动
示例代码

QTimerDemo.h

#pragma once

#include <QtWidgets/QWidget>
#include<qtimer.h> 
#include<qlabel.h>
#include<qpushbutton.h>
#include<qtextedit.h>
#include<qprogressbar.h>    
#include<QHBoxLayout>
#include<qdatetime.h>
#include<qframe.h>  

class QTimerDemo : public QWidget
{
    Q_OBJECT

public:
    QTimerDemo(QWidget *parent = nullptr);
    ~QTimerDemo();

private slots:
    void startSwing();
    void stopSwing();
    void updateSwing();

private:
    QTimer* timer;
    QLabel* swingLabel;
    QFrame* labelFrame;
    QTextEdit* logBox;
    QPushButton* startButton;
    QPushButton* stopButton;
    QProgressBar* progressBar;

    int direction;  //标志  1:向左 -1:向右
    int swingCount; //单边摆动计数
    int currentCycle; //来回循环数
    const int maxCycles; //最大循环次数

    
};

QTimerDemo.cpp

#include "QTimerDemo.h"

QTimerDemo::QTimerDemo(QWidget* parent)
    : QWidget(parent),
    timer(new QTimer(this)),
    labelFrame(new QFrame(this)),
    swingLabel(new QLabel("🔵", this)),
    logBox(new QTextEdit(this)),
    startButton(new QPushButton("开始运动",this)),
    stopButton(new QPushButton("停止运动",this)),
    progressBar(new QProgressBar(this)),
    direction(1),
    currentCycle(0),
    maxCycles(10),
    swingCount(0)
{
    setFixedSize(400, 300);
    swingLabel->setFixedSize(20, 20);
    swingLabel->move(10, 10);
    labelFrame->setFrameShape(QFrame::Box);
    labelFrame->setFixedHeight(40);
    swingLabel->setParent(labelFrame);

    logBox->setReadOnly(true);
    progressBar->setRange(0, maxCycles);
    

    QHBoxLayout* hlayout = new QHBoxLayout();
    hlayout->addWidget(startButton);
    hlayout->addWidget(stopButton);

    QVBoxLayout* vlayout = new QVBoxLayout(this);
    vlayout->addWidget(labelFrame);
    vlayout->addLayout(hlayout);
    vlayout->addWidget(progressBar);
    vlayout->addWidget(logBox);

    connect(timer, &QTimer::timeout, this, &QTimerDemo::updateSwing);
    connect(startButton, &QPushButton::clicked, this, &QTimerDemo::startSwing);
    connect(stopButton, &QPushButton::clicked, this, &QTimerDemo::stopSwing);
}

void QTimerDemo::startSwing() {
    if (!timer->isActive()) {
        currentCycle = 0;
        swingCount = 0;
        logBox->append("启动钟摆运动...");
        progressBar->setValue(0);
        timer->start(50); //每50ms更新位置
   }
}
void QTimerDemo::stopSwing() {
    if (timer->isActive()) {
        timer->stop();
        logBox->append("手动停止运动");
    }
}
void QTimerDemo::updateSwing(){
    const int leftLimit = 0;
    const int rightLimit = labelFrame->width() - swingLabel->width();
    int step = 5;

    //当前 x 坐标
    int x = swingLabel->x();
    int newX = x + direction * step;
    swingLabel->move(newX, swingLabel->y());

    if (newX <= leftLimit || newX >= rightLimit) {
        direction *= -1;
        swingCount++;

        //每两次摆动算一圈
        if (swingCount % 2 == 0) {
            currentCycle++;
            QString  timeStr = QDateTime::currentDateTime().toString("hh:mm:ss");
            logBox->append(QString("[%1]完成%2次循环").arg(timeStr).arg(currentCycle));
            progressBar->setValue(currentCycle);
        }

        //达到最大摆动次数,停止
        if (currentCycle >= maxCycles) {
            timer->stop();
            QString finishTime = QDateTime::currentDateTime().toString("hh:mm:ss");
            logBox->append(QString("达到%1次循环,运动结束,时间 %2").arg(maxCycles).arg(finishTime));
        }
    }
}

QTimerDemo::~QTimerDemo()
{}

效果展示

初始界面

image.png

点击“开始运动”

image.png

点击“停止运动”

image.png

摆动达到最大次数

image.png

2.4 拖动与拖放事件(dragEnterEvent/dropLeaveEvent/dragMoveEvent/dropEvent)

基础知识

Qt 中实现拖放功能的核心步骤:

① 拖动源(Drag Source)

  • 发起拖动操作:QDrag
  • 设置拖动数据:QMimeData
  • 常用函数:
    • QDrag(this)->setMimeData(...)
    • exec(Qt::CopyAction | Qt::MoveAction)

② 接收目标(Drop Target)

  • 事件函数需重载:
    • dragEnterEvent(QDragEnterEvent*)
    • dragMoveEvent(QDragMoveEvent*)
    • dropEvent(QDropEvent*)
    • dragLeaveEvent(QDragLeaveEvent*)
  • 启用接收:setAcceptDrops(true)

③ 拖放类型

  • 文件拖放:拖进来一个或多个文件路径
  • 文本拖放:拖放文本片段
  • 控件拖放:拖一个控件在界面中移动

示例

功能概述

使用 QTabWidget 管理多个可拖动标签页;

每个标签页支持拖放:

  • 拖入 图片文件 → 自动缩放并显示;
  • 拖入 文本文件 → 弹窗提示后显示内容;

标签页可拖动排序;

支持 Ctrl + 鼠标滚轮 缩放内容。

示例代码

QDragDemo.h

#pragma once

#include <QtWidgets/QWidget>
#include<qtablewidget.h>
#include<qlabel.h>
#include<qtextedit.h>
#include<QVBoxLayout>
#include<qmimedata.h>
#include<QWheelEvent>
#include<qimagereader.h>
#include<qfiledialog.h>
#include<QTabBar>
#include<QDragEnterEvent>
#include<QDropEvent>
#include<qfileinfo.h>
#include<qmessagebox.h>


class QDragDemo : public QWidget
{
    Q_OBJECT

public:
    QDragDemo(QWidget *parent = nullptr);
    ~QDragDemo();
protected:
    //拖入事件
    void dragEnterEvent(QDragEnterEvent* event)override; 
    //放下事件
    void dropEvent(QDropEvent* event) override;
    //鼠标滚轮事件
    void wheelEvent(QWheelEvent* event)override;

private:
    QLabel* imageLabel;
    QTextEdit* textEdit;
    double scaleFactor; //缩放比例

    void displayImage(const QString& filePath);
    void displayText(const QString& filePath);
};

QDragDemo.cpp

#include "QDragDemo.h"
#include<qapplication.h>

QDragDemo::QDragDemo(QWidget *parent)
    : QWidget(parent),
    scaleFactor(1.0)
{
    
    imageLabel = new QLabel("拖入图片或文本文件", this);
    imageLabel->setAlignment(Qt::AlignCenter);
    imageLabel->setScaledContents(true);

    textEdit = new QTextEdit(this);
    textEdit->setReadOnly(true);
    textEdit->hide();

    QVBoxLayout* mainLayout = new QVBoxLayout(this);
    mainLayout->addWidget(imageLabel);
    mainLayout->addWidget(textEdit);

    setAcceptDrops(true); //启用拖放功能
}

QDragDemo::~QDragDemo()
{}

void QDragDemo::dragEnterEvent(QDragEnterEvent * event)
{
    //如果是文件
    if (event->mimeData()->hasUrls())
        event->acceptProposedAction();
}

void QDragDemo::dropEvent(QDropEvent* event)
{
    QList<QUrl> urls = event->mimeData()->urls();
    if (urls.isEmpty())return;

    //获取文件路径
    QString filePath = urls.first().toLocalFile();
    QFileInfo fileInfo(filePath);
    //获取文件后缀
    QString suffix = fileInfo.suffix().toLower();

    if (suffix == "png" || suffix == "jpg" || suffix == "bmp") {
        displayImage(filePath);
    }
    else if (suffix == "txt" || suffix == "cpp") {
        QMessageBox::information(this, "打开文本", "即将打开文件:" + fileInfo.fileName());
        displayText(filePath);
    }
}

void QDragDemo::wheelEvent(QWheelEvent* event)
{
    //按下Ctrl键,执行缩放逻辑
    if (QApplication::keyboardModifiers() == Qt::ControlModifier) {
        int delta = event->angleDelta().y();
        scaleFactor += (delta > 0) ? 0.1 : -0.1; //放大或缩小
        scaleFactor = qBound(0.1, scaleFactor, 5.0); //限制缩放范围
        //根据新比例调整图片大小
        if (!imageLabel->pixmap().isNull()) {
            imageLabel->setPixmap(imageLabel->pixmap().scaled(imageLabel->size() * scaleFactor, Qt::KeepAspectRatio));
        }
    }
    
}

void QDragDemo::displayImage(const QString& filePath)
{
    QPixmap pix(filePath);
    if (!pix.isNull()) {
        //缩放图片
        imageLabel->setPixmap(pix.scaled(imageLabel->size() * scaleFactor, Qt::KeepAspectRatio));
        imageLabel->show();
        textEdit->hide();
    }
}

void QDragDemo::displayText(const QString& filePath)
{
    QFile file(filePath);
    if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        textEdit->setPlainText(file.readAll());
        textEdit->show();
        imageLabel->hide();
    }
}

DragDropViewer.h

#pragma once
#include "QDragDemo.h"
#include<qtabwidget.h>

class DragDropViewer :public QWidget  
{
	Q_OBJECT
public:
	DragDropViewer(QWidget* parent = nullptr);
private:
	QTabWidget* tabWidget;
	void setupTabs(); //多标签页
};

DragDropViewer.cpp

#include "DragDropViewer.h"

DragDropViewer::DragDropViewer(QWidget* parent)
{
	tabWidget = new QTabWidget(this);
	tabWidget->setTabsClosable(false);        // 不显示关闭按钮
	tabWidget->tabBar()->setMovable(true);    // 支持拖动标签页

	setupTabs();  // 添加初始标签

	QVBoxLayout* layout = new QVBoxLayout(this);
	layout->addWidget(tabWidget);
}

void DragDropViewer::setupTabs()
{
	//添加三个可拖放的页面
	for (int i = 0; i < 3; ++i) {
		QDragDemo* page = new QDragDemo(this);
		tabWidget->addTab(page, QString("标签 %1").arg(i + 1));
	}
}

main.cpp

#include"DragDropViewer.h"
#include <QtWidgets/QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    DragDropViewer viewer;
    viewer.setWindowTitle("多标签拖放");
    viewer.setFixedSize(600, 400);
    viewer.show();
    return a.exec();
}

效果展示

初始界面

image.png

拖入图片

image.png

拖入文件

image.png

Ctrl+滚轮控制文本放大显示

image.png

2.5 绘图事件(paintEvent)

绘图事件 paintEvent 是 Qt 中用于自定义绘制的重要机制,核心在于通过 QPainter 进行图形绘制。

基本知识

1. paintEvent 概述
  • paintEvent(QPaintEvent *event) 是 QWidget 的虚函数,用于窗口部件的自定义绘制
  • 必须在函数中使用 QPainter 进行绘图。
  • 建议不要在 paintEvent 外部直接使用 QPainter 对窗口绘制。
2. QPainter 基本功能
  • 绘制图形:线条、矩形、圆、弧线、多边形等;
  • 绘制文本;
  • 绘制图像;
  • 支持画笔(QPen)、画刷(QBrush)、字体、抗锯齿、透明度等样式设置。

示例

实现一个 Qt 示例程序,包含两个图形控件:

  • FunctionPlotWidget:绘制数学函数曲线(如 y = sin(x)),带坐标轴;
  • TemperatureChartWidget:绘制温度折线图,带关键点标注(如 10点:30℃);
示例代码

QPaintDemo.h

#pragma once

#include <QtWidgets/QWidget>


//主窗口
class FunctionPlotWidget;//绘制数学图形
class TemperatureChartWidget;//绘制温度曲线
class QPaintDemo : public QWidget
{
    Q_OBJECT

public:
    QPaintDemo(QWidget *parent = nullptr);
    ~QPaintDemo();

private:
    
};

QPaintDemo.cpp

#include "QPaintDemo.h"
#include"FunctionPlotWidget.h"
#include"TemperatureChartWidget.h"
#include<QVBoxLayout>
#include<QGroupBox>

QPaintDemo::QPaintDemo(QWidget *parent)
    : QWidget(parent)
{
    setWindowTitle("函数图像与温度折线图");
    resize(800, 600);

    QGroupBox* funcBox = new QGroupBox("数学函数图", this);
    QGroupBox* tempBox = new QGroupBox("温度变化图", this);

    FunctionPlotWidget* funcPlot = new FunctionPlotWidget(this);
    TemperatureChartWidget* tempPlot = new TemperatureChartWidget(this);

    auto* funcLayout = new QVBoxLayout(funcBox);
    auto* tempLayout = new QVBoxLayout(tempBox);

    funcLayout->addWidget(funcPlot);
    tempLayout->addWidget(tempPlot);

    auto* mainLayout = new QVBoxLayout(this);
    mainLayout->addWidget(funcBox);
    mainLayout->addWidget(tempBox);
}

QPaintDemo::~QPaintDemo()
{}

FunctionPlotWidget.h

#pragma once
#include <QtWidgets/QWidget>
class FunctionPlotWidget :
    public QWidget
{
    Q_OBJECT

public:
    FunctionPlotWidget(QWidget* parent = nullptr);
protected:
    void paintEvent(QPaintEvent* event)override;
};


FunctionPlotWidget.cpp

#include "FunctionPlotWidget.h"
#include<qpainter.h>
#include<cmath>
#include<qpainterpath.h>

FunctionPlotWidget::FunctionPlotWidget(QWidget* parent)
{
	setMinimumHeight(200);
}

void FunctionPlotWidget::paintEvent(QPaintEvent* event)
{
	QPainter p(this);
	p.setRenderHint(QPainter::Antialiasing);

	int w = width();
	int h = height();

	//坐标轴
	p.setPen(QPen(Qt::gray, 1));
	p.drawLine(10, h / 2, w - 10, h / 2); //x轴
	p.drawLine(w / 2, 10, w / 2, h - 10); //y轴

	//sin(x) 绘图区域[-π,π]
	p.setPen(QPen(Qt::blue, 2));
	QPainterPath path;
	const double PI = 3.1415926;
	int left = 10, right = w - 10;
	double step = (2 * PI) / (right - left);
	
	path.moveTo(left, h / 2);
	for (int x = left; x < right; ++x) {
		double angle = (x - w / 2) * step;
		double y = std::sin(angle);
		int yPixel = static_cast<int>(-y * h / 4 + h / 2);
		path.lineTo(x, yPixel);
	}
	p.drawPath(path);
}

TemperatureChartWidget.h

#pragma once
#include <QtWidgets/QWidget>
#include<qvector.h>
class TemperatureChartWidget :
    public QWidget
{
    Q_OBJECT

public:
    TemperatureChartWidget(QWidget* parent = nullptr);
protected:
    void paintEvent(QPaintEvent* event)override;
private:
    QVector<QPair<QString, int>> data; //时间+温度
};

TemperatureChartWidget.cpp

#include "TemperatureChartWidget.h"
#include<qpainter.h>
#include<qpainterpath.h>

TemperatureChartWidget::TemperatureChartWidget(QWidget* parent)
{
	setMinimumHeight(200);
	data = {
		{"10:00",30},
		{"11:00",32},
		{"12:00",33},
		{"13:00",35},
		{"14:00",34},
		{"15:00",32}
	};
}
void TemperatureChartWidget::paintEvent(QPaintEvent* event)
{
	QPainter p(this);
	p.setRenderHint(QPainter::Antialiasing);

	int margin = 40;
	int w = width() - 2 * margin;
	int h = height() - 2 * margin;

	int pointCount = data.size();
	int maxTemp = 40;

	//坐标轴
	p.drawLine(margin, margin, margin, margin + h);
	p.drawLine(margin, margin + h, margin + w, margin + h);

	//画折线&标记点
	QPainterPath path;
	for (int i = 0; i < pointCount; ++i) {
		int x = margin + i * (w / (pointCount - 1));
		int y = margin + h - (data[i].second * h / maxTemp);
		if (i == 0)path.moveTo(x, y);
		else path.lineTo(x, y);

		p.setBrush(Qt::red);
		p.drawEllipse(QPointF(x, y), 4, 4);
		p.drawText(x - 10, y - 10, QString("%1℃").arg(data[i].second));
		p.drawText(x - 20, margin + h + 15, data[i].first);
	}
	p.setBrush(Qt::NoBrush);
	p.setPen(QPen(Qt::darkGreen, 2));
	p.drawPath(path);
}

效果展示

image.png

2.6 鼠标右键菜单(contextMenuEvent)

在图形用户界面中,**右键菜单(Context Menu)**提供了一种快捷方式,允许用户在界面中的特定位置快速执行相关操作。例如,在文本框中右键弹出“复制、粘贴”菜单,或在图像中右键显示图像操作选项等。

Qt 中实现右键菜单,主要通过以下两种方式:

  • 事件重载方式:重写 contextMenuEvent(QContextMenuEvent*) 事件函数。
  • 信号槽方式:设置控件的上下文菜单策略为 Qt::CustomContextMenu,使用 customContextMenuRequested 信号。

核心函数

1. contextMenuEvent(QContextMenuEvent* event)
  • 这是 QWidget 的一个虚函数,可以在子类中重写此函数以定制右键菜单的响应行为。
  • 使用方式通常如下:
cpp复制编辑void MyWidget::contextMenuEvent(QContextMenuEvent* event) {
    QMenu menu(this);
    QAction* action1 = menu.addAction("操作1");
    QAction* action2 = menu.addAction("操作2");
    QAction* selected = menu.exec(event->globalPos());
    // 判断并执行
}
2. 控件的 setContextMenuPolicy(...)

设置控件支持右键菜单的策略:

策略说明
Qt::NoContextMenu禁用右键菜单
Qt::DefaultContextMenu使用控件自带的菜单(如 QTextEdit)
Qt::CustomContextMenu启用自定义菜单,需要手动连接信号
Qt::ActionsContextMenu使用控件的 QAction 列表作为菜单
3. customContextMenuRequested(const QPoint&) 信号
  • 搭配 Qt::CustomContextMenu 策略使用;
  • 你可以根据点击位置弹出菜单。

示例

做一个简单的“文本编辑器”窗口,当用户在文本区域点击右键时,弹出一个自定义菜单,包含常见操作如:

  • 复制
  • 粘贴
  • 清空
  • 全选
  • 插入当前时间
示例代码

QContextMenuDemo.h

#pragma once

#include <QtWidgets/QWidget>
#include<qtextedit.h>

class QContextMenuDemo : public QWidget
{
    Q_OBJECT

public:
    QContextMenuDemo(QWidget *parent = nullptr);
    ~QContextMenuDemo();
protected:
    void contextMenuEvent(QContextMenuEvent* event) override;
private:
    QTextEdit* textEdit;

};

QContextMenuDemo.cpp

#include "QContextMenuDemo.h"
#include <QVBoxLayout>
#include <QMenu>
#include <QContextMenuEvent>
#include <QDateTime>
#include <QClipboard>

QContextMenuDemo::QContextMenuDemo(QWidget *parent)
    : QWidget(parent)
{
    setWindowTitle("右键菜单");
    resize(400, 300);

    textEdit = new QTextEdit(this);
    textEdit->setContextMenuPolicy(Qt::NoContextMenu);
    QVBoxLayout* layout = new QVBoxLayout(this);
    layout->addWidget(textEdit);
    textEdit->setPlaceholderText("点击右键打开菜单");
}

QContextMenuDemo::~QContextMenuDemo()
{}

void QContextMenuDemo::contextMenuEvent(QContextMenuEvent * event)
{
    if (!textEdit->geometry().contains(event->pos()))return;

    QMenu menu(this);
    QAction* copyAction = menu.addAction("复制");
    QAction* pasteAction = menu.addAction("粘贴");
    QAction* clearAction = menu.addAction("清空");
    QAction* selectAllAction = menu.addAction("全选");
    menu.addSeparator();
    QAction* insertTimeAction = menu.addAction("插入当前时间");

    QAction* selected = menu.exec(event->globalPos());
    if (!selected) return;

    if (selected == copyAction) {
        textEdit->copy();
    }
    else if (selected == pasteAction) {
        textEdit->paste();
    }
    else if (selected == clearAction) {
        textEdit->clear();
    }
    else if (selected == selectAllAction) {
        textEdit->selectAll();
    }
    else if (selected == insertTimeAction) {
        QString currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
        textEdit->insertPlainText(currentTime + "\n");
    }
}

效果展示

image.png

image.png


3 事件过滤与自定义事件

3.1 使用事件过滤器拦截事件

基本原理

Qt 允许你在某个对象上安装事件过滤器(installEventFilter()),从而在事件传递前对其进行拦截和处理。

适用场景包括:

  • 多个控件共享统一的事件处理逻辑;
  • 临时对某个控件进行监控;
  • 对 Qt 不提供默认接口的事件响应进行拦截。

使用步骤

使你的类继承自 QObjectQWidget

重写 eventFilter(QObject* watched, QEvent* event)

使用 installEventFilter() 将过滤器安装到目标对象上。

示例:拦截多个按钮的鼠标进入和点击事件

#include <QWidget>
#include <QPushButton>
#include <QVBoxLayout>
#include <QDebug>
#include <QEvent>

class EventFilterDemo : public QWidget {
    Q_OBJECT
public:
    EventFilterDemo(QWidget* parent = nullptr) : QWidget(parent) {
        QVBoxLayout* layout = new QVBoxLayout(this);

        for (int i = 0; i < 3; ++i) {
            QPushButton* btn = new QPushButton(QString("按钮 %1").arg(i + 1), this);
            btn->installEventFilter(this); // 安装事件过滤器
            layout->addWidget(btn);
        }

        setLayout(layout);
    }

protected:
    bool eventFilter(QObject* watched, QEvent* event) override {
        if (event->type() == QEvent::Enter) {
            qDebug() << watched->objectName() << "鼠标进入";
        } else if (event->type() == QEvent::MouseButtonPress) {
            auto* btn = qobject_cast<QPushButton*>(watched);
            if (btn)
                qDebug() << btn->text() << "被点击";
        }
        return QWidget::eventFilter(watched, event); // 不阻止事件继续传播
    }
};

3.2 创建并使用自定义事件

基本原理

Qt 允许我们定义自己的事件类型,用于组件间通信或状态控制。自定义事件继承 QEvent 类,并指定一个唯一的事件类型(类型编号 ≥ QEvent::User)。

适用于:

  • 模拟异步消息队列;
  • 跨模块事件派发;
  • 替代信号槽进行某些封装场景。

创建流程

步骤一:定义事件类

#include <QEvent>

class MyCustomEvent : public QEvent {
public:
    static const QEvent::Type EventType;

    MyCustomEvent(const QString& msg)
        : QEvent(EventType), message(msg) {}

    QString message;
};

// 注册唯一类型编号
const QEvent::Type MyCustomEvent::EventType = static_cast<QEvent::Type>(QEvent::User + 1);

步骤二:在接收者类中重写 customEvent()

#include <QWidget>
#include <QDebug>

class CustomEventReceiver : public QWidget {
    Q_OBJECT
public:
    CustomEventReceiver(QWidget* parent = nullptr) : QWidget(parent) {}

protected:
    void customEvent(QEvent* event) override {
        if (event->type() == MyCustomEvent::EventType) {
            auto* myEvent = static_cast<MyCustomEvent*>(event);
            qDebug() << "收到自定义事件:" << myEvent->message;
        }
    }
};

步骤三:发送事件

// 例如在按钮点击中发送事件
MyCustomEvent* evt = new MyCustomEvent("这是一个自定义事件");
QCoreApplication::postEvent(receiverWidget, evt); // 异步派发

4 事件系统底层原理

Qt 的事件系统是基于一个事件循环(event loop)机制,本质上是一个消息分发机制,用于接收操作系统或自定义事件,并将它们派发到对应的 QObject 实例中。

事件流转大致流程如下:

  1. 操作系统向 Qt 应用发出事件(如鼠标、键盘、窗口变动);
  2. Qt 内部的事件循环接收这些事件;
  3. 通过 Qt 的分发系统找到事件目标对象;
  4. 调用目标对象的 event() 函数;
  5. event() 会进一步调用如 mousePressEvent()keyPressEvent() 等更具体的处理函数。

4.1 事件循环机制

Qt 使用 QEventLoop 对象来维持一个无限循环,它在后台不停调用操作系统的消息队列,从而实现:

  • 处理窗口事件(移动、关闭等)
  • 响应用户操作(点击、输入)
  • 分发定时器事件
  • 执行异步任务(如网络、IO)

事件循环的启动点

最核心的一行:

return app.exec();

这会启动 Qt 的主事件循环。Qt 内部会调用 QEventLoop::exec(),进而挂起主线程,等待事件。

你也可以手动创建子事件循环,例如在模态对话框中:

QDialog dlg;
dlg.exec(); // 启动局部事件循环

4.2 事件压缩机制

Qt 会自动合并一些“重复且无害的事件”,例如:

  • QResizeEvent
  • QMoveEvent
  • QPaintEvent

例如,如果在短时间内你多次调用 resize(),Qt 不会立即处理所有事件,而是只保留最后一次 QResizeEvent

这就是所谓的事件压缩机制,可以减少无意义的中间处理,提升效率

触发方式

Qt 在 QCoreApplication::postEvent() 中会判断当前事件是否可以与已有事件合并,若可以就覆盖旧事件:

QCoreApplication::postEvent(receiver, new QResizeEvent(...));

小结

机制描述
事件循环Qt 的核心机制,阻塞主线程等待事件
事件分发将事件投递给目标 QObject
事件压缩减少重复事件,提高处理效率
自定义事件用于模块解耦、异步通信
信号槽与事件关系信号跨线程底层使用事件传递
操作系统集成基于系统原生消息机制进行适配

5 总结

  1. 事件循环(Event Loop)
  • Qt 事件系统的心脏,由 QCoreApplication::exec() 启动。
  • 使用 QEventLoop 处理系统事件,实现 GUI 响应和异步机制。
  • 支持手动启动局部事件循环,常用于模态对话框、等待任务完成等场景
  1. 事件分发机制
  • 所有事件最终都会传递到目标对象的 QObject::event() 方法。
  • 可以通过重写 event() 或具体事件处理函数(如 mousePressEvent())来自定义行为。
  1. 事件压缩机制
  • Qt 会自动合并连续产生但未处理的部分事件(如 QPaintEvent)。
  • 减少重复事件处理,提升渲染与响应性能。
  1. 自定义事件循环
  • 使用 QEventLoop 可在不冻结界面的前提下实现“阻塞式”等待。
  • 常见于异步任务等待、用户确认等待等场景。