Qt进阶开发:鼠标及键盘事件

一、Qt中事件的概念

  在 Qt 中,事件(Event) 是程序和用户、系统之间交互的一种机制。它是 Qt 应用响应用户操作(如鼠标点击、键盘输入、窗口变化、定时器等)的核心组成部分。Qt 通过其事件系统实现了灵活、统一的事件处理模型。

事件是继承自 QEvent 的对象,用来描述“发生了什么事情”。比如:
在这里插入图片描述

二、Qt中事件处理方式

  一个事件由一个特定的QEvent子类来表示,但是有时一个事件又包含多个事件类型,比如鼠标事件可以分为鼠标按下、双击和移动等多种操作。这些事件类型都由 QEvent类的枚举型QEvent::Type来表示,其中包含了一百多种事件类型,虽然QEvent的子类可以表示一个事件,但是却不能用来处理事件,那么应该怎样来处理一个事件呢?QCoreApplication类的notify()函数的帮助文档给出了5种处理事件的方法:

  1. 重新实现部件的paintEvent()、mousePressEvent()等事件处理函数。
  2. 重新实现notify()函数。这个函数功能强大,提供了完全的控制,可以在事件过滤器得到事件之前就获得它们。但是,它一次只能处理一个事件。
  3. 向QApplication对象上安装事件过滤器。因为一个程序只有一个QApplication对象,所以这样实现的功能与使用notify()函数是相同的,优点是可以同时处理多个事件。
  4. 重新实现event()函数。QObiect类的event()函数可以在事件到达默认的事件处理函数之前获得该事件。
  5. 在对象上安装事件过滤器。使用事件过滤器可以在一个界面类中同时处理不同子部件的不同事件。

在实际编程中,最常用的是方法一,其次是方法五。因为方法二需要继承自QApplication类;而方法三要使用一个全局的事件过滤器,这将减缓事件的传递,所以,虽然这两种方法功能很强大,但是却很少被用到。

三、重新实现部件的事件处理函数

3.1 常用事件处理函数

在这里插入图片描述

3.2 自定义控件处理鼠标和绘图事件

示例目标:

  • 鼠标点击时在点击位置画一个红色的圆
  • 每次点击都重新绘制控件

3.2.1 自定义类头文件(MyWidget.h)

#ifndef MYWIDGET_H
#define MYWIDGET_H

#include <QWidget>
#include <QPoint>
#include <QVector>

class MyWidget : public QWidget {
    Q_OBJECT
public:
    explicit MyWidget(QWidget *parent = nullptr);

protected:
    void paintEvent(QPaintEvent *event) override;
    void mousePressEvent(QMouseEvent *event) override;

private:
    QVector<QPoint> points;  // 存储所有点击点
};
#endif // MYWIDGET_H

3.2.2 实现文件(MyWidget.cpp)

#include "MyWidget.h"
#include <QPainter>
#include <QMouseEvent>

MyWidget::MyWidget(QWidget *parent)
    : QWidget(parent) {
    setMinimumSize(400, 300);
}

void MyWidget::mousePressEvent(QMouseEvent *event) {
    if (event->button() == Qt::LeftButton) {
        points.append(event->pos());  // 保存点击位置
        update();  // 触发重绘
    }
}

void MyWidget::paintEvent(QPaintEvent *event) {
    Q_UNUSED(event);

    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    painter.setPen(Qt::red);
    painter.setBrush(Qt::red);

    for (const QPoint &pt : points) {
        painter.drawEllipse(pt, 5, 5);  // 在每个点击点画个小圆
    }
}

3.2.3 使用自定义控件(main.cpp 示例)

#include "MyWidget.h"
#include <QApplication>

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);
    MyWidget w;
    w.show();
    return a.exec();
}

3.3 常用事件处理函数说明

paintEvent():

  • 这是唯一推荐的绘图入口
  • 使用 QPainter 来进行绘图操作
  • 通过 update() 来主动触发重绘(会间接调用 paintEvent())

mousePressEvent():

  • 接收 QMouseEvent*,可以通过 event->pos() 获取鼠标坐标
  • event->button() 检测是哪个键(左、中、右)

其他可重写事件:

  • enterEvent() / leaveEvent():鼠标进入/离开控件
  • focusInEvent() / focusOutEvent():控件获取/失去焦点
  • wheelEvent():鼠标滚轮
  • keyReleaseEvent():键盘释放
  • contextMenuEvent():右键菜单

注意:update() ≠ repaint():update() 是异步(推荐);repaint() 是立即刷新。

四、重写notify()函数

  QCoreApplication::notify() 或 QApplication::notify() 是 Qt 事件系统中最底层的事件分发函数之一。重写它可以让你在任何事件被分发到对象前先拦截并处理它,因此功能极其强大,常用于:

  • 全局事件监控(比如捕获所有按键、鼠标事件)
  • 调试/日志记录所有事件
  • 自定义事件处理行为

注意:它一次只处理一个事件,而且要注意别滥用,因为它拦截的是所有事件,非常频繁。

notify() 函数的原型:

virtual bool QApplication::notify(QObject *receiver, QEvent *event) override;
  • receiver: 将要接收事件的对象
  • event: 即将被分发的事件
  • 返回值:如果你处理了事件,返回 true;否则通常应该调用父类的 notify() 再返回结果。

示例:重写 notify() 监听所有按键和鼠标事件
自定义 QApplication 类:

// MyApplication.h
#include <QApplication>
#include <QEvent>
#include <QDebug>

class MyApplication : public QApplication {
public:
    using QApplication::QApplication;

    bool notify(QObject *receiver, QEvent *event) override {
        // 监听所有键盘事件
        if (event->type() == QEvent::KeyPress) {
            qDebug() << "全局键盘事件:receiver =" << receiver << " key =" << static_cast<QKeyEvent *>(event)->key();
        }

        // 监听所有鼠标点击事件
        if (event->type() == QEvent::MouseButtonPress) {
            qDebug() << "全局鼠标点击事件:receiver =" << receiver;
        }

        // 一定要调用父类 notify,否则事件无法继续传递!
        return QApplication::notify(receiver, event);
    }
};

使用这个自定义 QApplication:

#include "MyApplication.h"
#include <QPushButton>
#include <QWidget>

int main(int argc, char *argv[]) {
    MyApplication app(argc, argv);

    QWidget window;
    QPushButton btn("Click me", &window);
    window.show();

    return app.exec();
}

notify() 与 eventFilter() 的区别:
在这里插入图片描述
注意事项:

  • 不要忘记调用 QApplication::notify() 否则事件会被截断!
  • notify() 中不能做太重的操作,否则会拖慢整个程序
  • 因为它是全局入口,处理频率非常高,要保证稳定性

五、QApplication对象上安装事件过滤器

  在 Qt 中,如果向 QApplication 对象安装事件过滤器,就可以实现类似 notify() 的全局事件监控功能。不过,相比于重写 notify(),使用事件过滤器(eventFilter()) 有几个明显优点:

  • 也能捕获整个应用中所有控件的事件
  • 不需要继承 QApplication
  • 可以封装多个过滤器类,分别处理不同的事件
  • 更安全:不会意外截断事件流(只拦截你关心的事件)

示例:向 QApplication 安装事件过滤器,监控鼠标和键盘事件
创建事件过滤器类(继承 QObject):

// EventFilter.h
#include <QObject>
#include <QEvent>
#include <QMouseEvent>
#include <QKeyEvent>
#include <QDebug>

class GlobalEventFilter : public QObject {
    Q_OBJECT
public:
    explicit GlobalEventFilter(QObject *parent = nullptr) : QObject(parent) {}

protected:
    bool eventFilter(QObject *watched, QEvent *event) override {
        if (event->type() == QEvent::MouseButtonPress) {
            auto mouseEvent = static_cast<QMouseEvent *>(event);
            qDebug() << "[全局鼠标] 控件:" << watched << "位置:" << mouseEvent->pos();
        } else if (event->type() == QEvent::KeyPress) {
            auto keyEvent = static_cast<QKeyEvent *>(event);
            qDebug() << "[全局键盘] 控件:" << watched << "按键:" << keyEvent->key();
        }

        // 返回 false 表示继续分发事件,true 表示拦截事件
        return false;
    }
};

在主函数中安装过滤器:

#include <QApplication>
#include "EventFilter.h"
#include <QPushButton>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    GlobalEventFilter *filter = new GlobalEventFilter(&app);
    app.installEventFilter(filter);  // 安装到 QApplication 上

    QPushButton btn("点我");
    btn.resize(200, 100);
    btn.show();

    return app.exec();
}

安装到 QApplication vs 安装到某个控件:
在这里插入图片描述
和 notify() 的对比:
在这里插入图片描述

六、重写event()事件

event() 函数的原型:

virtual bool QObject::event(QEvent *event);
  • 这是 Qt 事件分发机制中的默认实现。
  • 通常用于 QWidget、QGraphicsItem、QTimer 等基类,在具体事件类型处理前,做统一的判断或过滤。

常见用途
在这里插入图片描述
示例:重写 event() 捕获鼠标进入、离开、按下事件
自定义一个 QLabel,当鼠标进入/离开/点击时改变颜色:

#include <QLabel>
#include <QEvent>
#include <QMouseEvent>
#include <QDebug>
#include <QPalette>

class MyLabel : public QLabel {
    Q_OBJECT
public:
    explicit MyLabel(QWidget *parent = nullptr)
        : QLabel(parent) {
        setAutoFillBackground(true);
        setText("鼠标请进入/点击我");
        setAlignment(Qt::AlignCenter);
    }

protected:
    bool event(QEvent *event) override {
        if (event->type() == QEvent::Enter) {
            QPalette pal = palette();
            pal.setColor(QPalette::Window, Qt::yellow);
            setPalette(pal);
            return true;  // 事件已处理
        } else if (event->type() == QEvent::Leave) {
            QPalette pal = palette();
            pal.setColor(QPalette::Window, Qt::white);
            setPalette(pal);
            return true;
        } else if (event->type() == QEvent::MouseButtonPress) {
            qDebug() << "你点击了标签!";
            return true;
        }

        return QLabel::event(event);  // 其他事件用默认方式处理
    }
};

在 main.cpp 中使用:

#include <QApplication>
#include "MyLabel.h"

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    MyLabel label;
    label.resize(300, 100);
    label.show();

    return app.exec();
}

event() vs 事件处理函数 vs notify() vs 事件过滤器
在这里插入图片描述

七、在对象上安装事件过滤器

  Qt 中,在对象上安装事件过滤器(installEventFilter()) 是一种非常灵活的机制,可以让你在一个类中集中管理多个控件的不同事件,非常适合做界面行为控制、统一交互逻辑、复杂控件间的协调等工作。

事件过滤器机制简介:

  • 所有 QObject(包括 QWidget)都有 installEventFilter() 函数。
  • 一旦将某个对象(比如控件)安装到另一个对象的过滤器列表中,那么:目标对象每接收到一个事件时,安装它的对象会先收到事件副本,调用其 eventFilter() 方法,可以选择拦截这个事件(返回 true),或者放行(返回 false)。

eventFilter() 函数的原型:

bool eventFilter(QObject *watched, QEvent *event);
  • watched:当前接收到事件的对象(你安装过滤器的对象)
  • event:当前事件(类型需转换,如 QMouseEvent)
  • 返回 true:事件被拦截,不再继续传播
  • 返回 false:继续传播,可能会进入目标对象的事件函数

示例:在主窗口类中统一处理多个控件的事件
主窗口类中安装过滤器:

#include <QMainWindow>
#include <QPushButton>
#include <QLabel>
#include <QEvent>
#include <QMouseEvent>
#include <QDebug>

class MainWindow : public QMainWindow {
    Q_OBJECT

public:
    MainWindow() {
        resize(400, 200);

        btn = new QPushButton("按钮", this);
        btn->setGeometry(50, 50, 100, 40);

        label = new QLabel("点击我", this);
        label->setGeometry(200, 50, 100, 40);
        label->setStyleSheet("background: lightgray; text-align: center;");
        label->setAlignment(Qt::AlignCenter);

        // 安装事件过滤器
        btn->installEventFilter(this);
        label->installEventFilter(this);
    }

protected:
    bool eventFilter(QObject *watched, QEvent *event) override {
        if (watched == btn) {
            if (event->type() == QEvent::Enter) {
                btn->setStyleSheet("background-color: yellow;");
            } else if (event->type() == QEvent::Leave) {
                btn->setStyleSheet("");
            }
        } else if (watched == label) {
            if (event->type() == QEvent::MouseButtonPress) {
                qDebug() << "你点击了标签!";
            }
        }

        // 放行事件
        return false;
    }

private:
    QPushButton *btn;
    QLabel *label;
};

main 函数中调用:

#include <QApplication>
#include "MainWindow.h"

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    MainWindow w;
    w.show();

    return app.exec();
}

使用场景
在这里插入图片描述

与重写事件函数的比较:
在这里插入图片描述

八、事件的传递顺序

自定义MyLineEdit的实现:

#ifndef MYLINEEDIT_H
#define MYLINEEDIT_H

#include <QWidget>
#include <QLineEdit>
#include <QKeyEvent>
#include <QDebug>

class MyLineEdit : public QLineEdit
{
    Q_OBJECT
public:
    explicit MyLineEdit(QWidget *parent = nullptr) : QLineEdit(parent)  {

    }

protected:
    // 键盘按下事件
    void keyPressEvent(QKeyEvent * event) override {
        qDebug() << tr("MyLineEdit键盘按下事件");
        //QLineEdit::keyPressEvent(event); // 执行QLineEdit类的默认事件处理
        //event->ignore(); // 忽略该事件
    }
};
#endif // MYLINEEDIT_H

Widget类的实现:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QWidget>
#include <QKeyEvent>

#include "mylineedit.h"

class MainWindow : public QWidget
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr) : QWidget(parent) {
        m_pLineEdit = new MyLineEdit(this);
        m_pLineEdit->move(100, 100);
    }
    
protected:
    virtual void keyPressEvent(QKeyEvent *event) override {
        qDebug() << tr("Widget键盘按下事件");
    }

private:
    MyLineEdit *m_pLineEdit;
};
#endif // MAINWINDOW_H

  这里自定义了一个MyLineEdit类,它继承自QLineEdit类,然后在Widget界面中添加了一个MyLineEdit部件。注意,这里既实现了MyLineEdit类的键盘按下事件处理函数,也实现了Widget类的键盘按下事件处理函数。现在运行程序,这时光标焦点在行编辑器中,随便在键盘上按一个按键,比如按下A键,则QtCreator的应用程序输出栏中只会出现“MyLineEdit键盘按下事件”,说明这时只执行了MyLineEdit类中的keyPressEvent()函数。
在这里插入图片描述
  下面到mylineedit.cpp文件的keyPressEvent()函数最后添加如下一行代码,让它忽略掉这个事件:

event->ignore(); // 忽略该事件

  这时再运行程序,按下A键,那么在以前输出的基础上又输出了“Widget键盘按下事件”,说明这时也执行了Widget类中的keyPressEvent()函数。
在这里插入图片描述
  但是现在出现了一个问题,就是行编辑器中无法输入任何字符,为了让它还可以正常工作,还需要在 mylineedit.cpp文件中的keyPressEvent()函数中添加一行代码,整个函数定义如下:

 void keyPressEvent(QKeyEvent * event) override {
        qDebug() << tr("MyLineEdit键盘按下事件");
        QLineEdit::keyPressEvent(event); // 执行QLineEdit类的默认事件处理
        event->ignore(); // 忽略该事件
}

  这里调用了MyLineEdit父类QLineEdit的keyPressEvent()函数来实现行编辑器的默认操作。这里一定要注意代码的顺序,ignore()函数要在最后调用。从这个例子中可以看到,事件是先传递给指定窗口部件的,确切地说应该是先传递给获得焦点的窗口部件。但是如果该部件忽略掉该事件,那么这个事件就会传递给这个部件的父部件。重新实现事件处理函数时,一般要调用父类的相应事件处理函数来实现默认操作。
下面将这个例子再进行改进,看一下事件过滤器等其他方法获取事件的顺序:

 virtual bool event(QEvent *event) override {
       if (event->type() == QEvent::KeyPress) {
           qDebug() << tr("MyLineEdit的event()函数");
       }
        
       return QLineEdit::event(event);
 }

  MyLineEdit的event()函数中使用了QEvent的type()函数来获取事件的类型,如果是键盘按下事件QEvent::KeyPress,则输出信息。因为event()函数具有bool型的返回值。所以该函数的最后要使用return语句,这里一般是返回父类的event()函数。
下面进人widget.h文件中进行函数的定义:

 // 事件过滤器
 virtual bool eventFilter(QObject *watched, QEvent *event) override {
       if (watched == m_pLineEdit) {
           if (event->type() == QEvent::KeyPress) {
               qDebug() << tr("Widget的事件过滤求");
           }
       }
        
       return QWidget::eventFilter(watched, event);
 }

然后到widget.cpp文件中,在构造函数的最后添上一行代码:

m_pLineEdit->installEventFilter(this); // 在Widget上为m_pLineEdit安装事件过滤器

在这里插入图片描述
输出结果:
在这里插入图片描述
  可以看到,事件的传递顺序是这样的:先是事件过滤器,然后是焦点部件的event()函数,最后是焦点部件的事件处理函数;如果焦点部件忽略了该事件,那么会执行父部件的事件处理函数,注意,event()函数和事件处理函数是在焦点部件内重新定义的,而事件过滤器却是在焦点部件的父部件中定义的。

下图所示:事件传递顺序示意图
在这里插入图片描述

九、鼠标及键盘事件的完整实例

9.1 鼠标事件的完整实例

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QLabel>
#include <QMouseEvent> // 鼠标事件
#include <QStatusBar>  // 窗口状态栏实现

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr): QMainWindow(parent) {
        this->setWindowTitle("测试鼠标事件程序");
        this->resize(800, 600);

        m_pStatusLabel = new QLabel(this);
        m_pStatusLabel->setText("鼠标在当前窗口坐标为:");

        m_pMouseLabelPos = new QLabel(this);
        m_pMouseLabelPos->setText("");
        m_pMouseLabelPos->setFixedWidth(200);

        // 在状态栏当中添加窗口小控件对象
        statusBar()->addPermanentWidget(m_pStatusLabel);
        statusBar()->addPermanentWidget(m_pMouseLabelPos);

        this->setMouseTracking(true); // 鼠标实时追踪
    }

    ~MainWindow() {

    }

protected:
    virtual void mouseMoveEvent(QMouseEvent *e) override {
        m_pMouseLabelPos->setText("(" + QString::number(e->x()) + "," + QString::number(e->y()) + ")");
    }

    virtual void mousePressEvent(QMouseEvent *e) override {
        QString qstr = "(" + QString::number(e->x()) + "," + QString::number(e->y()) + ")";
        if (e->button() == Qt::LeftButton) {
            statusBar()->showMessage("用户已按下鼠标[左键]坐标" + qstr);
        } else if (e->button() == Qt::RightButton) {
            statusBar()->showMessage("用户已按下鼠标[右键]坐标" + qstr);
        } else if (e->button() == Qt::MidButton) {
            statusBar()->showMessage("用户已按下鼠标[中键]坐标" + qstr);
        }
    }

    virtual void mouseReleaseEvent(QMouseEvent *e) override {
        QString qstr = "(" + QString::number(e->x()) + "," + QString::number(e->y()) + ")";
        statusBar()->showMessage("用户已经释放鼠标坐标" + qstr, 20);
    }

private:
    QLabel *m_pStatusLabel;
    QLabel *m_pMouseLabelPos;
};
#endif // MAINWINDOW_H

输出结果:
请添加图片描述

9.2 键盘事件的完整实例

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QWidget>
#include <QKeyEvent>
#include <QPainter>
#include <QPalette>
#include <QPen>
#include <QDir>
#include <QCoreApplication>
#include <QDebug>

class MainWindow : public QWidget
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr) : QWidget(parent) {
        this->setWindowTitle("键盘事件测试:控制图形动向");

        QPalette plet = this->palette();
        plet.setColor(QPalette::Window, Qt::white);
        setPalette(plet);

        setMinimumSize(800, 600);
        setMaximumSize(800, 600);

        m_iWidth = size().width();
        m_iHeight = size().height();

        m_pix = new QPixmap(m_iWidth, m_iHeight);
        m_pix->fill(Qt::white);

        QDir dir(QCoreApplication::applicationDirPath());
        QString fullPath = dir.absoluteFilePath("cat.png");
        m_image.load(fullPath);

        m_iStartX = 30;
        m_iStartY = 30;
        m_iSetp = 30;

        drawpixfunc(); // 调用函数绘制

        resize(800, 600);
    }

    ~MainWindow() {

    }

    void drawpixfunc() {
        m_pix->fill(Qt::green);

        QPainter *painter = new QPainter(this);
        QPen pen(Qt::DashDotLine); // 点画线

        // 竖线
        for (int i = m_iSetp; i < m_iWidth; i = i+ m_iSetp) {
            painter->begin(m_pix);
            painter->setPen(pen);
            painter->drawLine(QPoint(i, 0), QPoint(i, m_iHeight));
            painter->end();
        }

        // 横线
        for (int j = m_iSetp;  j < m_iHeight; j = j+ m_iSetp) {
            painter->begin(m_pix);
            painter->setPen(pen);
            painter->drawLine(QPoint(0, j), QPoint(m_iWidth, j));
            painter->end();
        }

        painter->begin(m_pix);
        painter->drawImage(QPoint(m_iStartX, m_iStartY), m_image);
        painter->end();
    }

protected:
    virtual void paintEvent(QPaintEvent *event) override {
        QPainter pt;
        pt.begin(this);
        pt.drawPixmap(QPoint(0, 0 ), *m_pix);
        pt.end();
    }

    virtual void keyPressEvent(QKeyEvent *event) override {
        m_iStartX = m_iStartX - m_iStartX % m_iSetp;
        m_iStartY = m_iStartY - m_iStartY % m_iSetp;

        if (event->key() == Qt::Key_Left) {
            m_iStartX = (m_iStartX - m_iSetp < 0) ? m_iStartX : m_iStartX - m_iSetp;
        }

        if (event->key() == Qt::Key_Right) {
            m_iStartX = (m_iStartX + m_iSetp + m_image.width()  > m_iWidth) ? m_iStartX : m_iStartX + m_iSetp;
        }

        if (event->key() == Qt::Key_Up) {
            m_iStartY = (m_iStartY - m_iSetp < 0) ? m_iStartY : m_iStartY - m_iSetp;
        }

        if (event->key() == Qt::Key_Down) {
            m_iStartY = (m_iStartY + m_iSetp + m_image.height()  > m_iHeight) ? m_iStartY : m_iStartY + m_iSetp;
        }

        // 调用绘制
        drawpixfunc();
        update();
    }

private:
    QPixmap *m_pix;
    QImage m_image;

    int m_iStartX;
    int m_iStartY;
    int m_iWidth;
    int m_iHeight;
    int m_iSetp;
};
#endif // MAINWINDOW_H

输出结果:
请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值