Qt事件处理
文章目录
- 前言
- 一、Qt的事件系统
- 二、事件与信号
- 三、事件过滤器
- 四、拖放事件与拖放操作
- 五、具有拖放操作功能的组件
前言
GUI 应用程序是由事件(event)驱动的,点击鼠标、按下某个按键、改变窗口大小、最小化窗口等都会产生相应的事件,应用程序对这些事件进行相应的处理以实现程序的功能。
一、Qt的事件系统
窗口系统采用事件驱动机制,QWidget 类是所有界面组件类的基类,它定义了众多与事件处理相关的数据类型和接口函数。
事件代表着程序运行过程中发生的各种交互行为或状态变更,这些事件都被封装为特定的对象实例,继承自 QEvent 基类,如 QKeyEvent 是按键事件类,QMouseEvent 是鼠标事件类,QPaintEvent 是绘制事件类,QTimerEvent 是定时器事件类。按事件的来源,将事件划分为3类:
- 自生事件,由窗口系统产生的事件,例如 QKeyEvent、QMouseEvent。事件进入系统队列后,被应用程序的事件循环逐个处理。
- 发布事件,由 Qt 或应用程序产生的事件。例如 QTimer 发生定时溢出时 Qt 会发布 QTimerEvent 事件。应用程序使用静态函数 QCoreApplication::postEvent() 产生发布事件。事件会进入 Qt 事件队列,由应用程序的事件循环进行处理。
- 发送事件,由 Qt 或应用程序定向发送给某个对象的事件。应用程序使用静态函数 QCoreApplication::sendEvent() 产生发送事件,由对象的 event() 函数直接处理。
应用程序使用静态函数 QCoreApplication::postEvent() 发布事件,这个函数是异步的,发布完立刻退出,不会等到事件处理完之后再退出。这个函数的原型定义如下:
/**********************
receiver 是接收事件的对象
event 是事件对象
priority 是事件的优先级
**********************/
void QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority = Qt::NormalEventPriority)
应用程序使用静态函数 QCoreApplication::sendEvent() 向某个对象定向发送事件,它是以同步模式运行的,也就是需要等待对象处理完事件后才退出。函数定义如下:
/**********************
receiver 是接收事件的对象
event 是事件对象
**********************/
bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event)
那么事件是怎么派发的呢?应用程序的 main() 函数代码一般是这样的结构:
int main(int argc, char *argv[]) { QApplication a(argc, argv); // 创建了一个QApplication对象Widget w; // 创建了一个窗口w.show(); // 显示窗口return a.exec(); // 开始应用程序的事件循环
}
QApplication::exec() 不断地检查系统队列和 Qt 事件队列里是否有未处理的自生事件和发布事件,如果有事件就派发(dispatch)给接收事件的对象去处理。事件循环还对队列中的相同事件进行合并处理,例如如果队列中有一个界面组件的多个 QPaintEvent 事件,应用程序就只派发一次 QPaintEvent 事件。 应用程序的事件循环不会处理发送事件,因为发送事件由应用程序直接派发给某个对象,是以同步模式运行的。
在某些情况下,例如执行一个大的循环,并且在循环内进行大量的计算或数据传输,同时又要求更新界面显示内容,这时就可能出现界面响应迟滞甚至无响应的情况,这是因为事件队列未能被及时处理。 要解决这样的问题可以采用多线程方法,将界面更新与数据传输分别用两个线程去处理。 另外一种简单的处理方法是在长时间占用 CPU 的代码段中,偶尔调用 QCoreApplication 的静态函数 processEvents(),将事件队列里未处理的事件派发出去,让事件接收对象及时处理。这个函数的原型定义如下:
/*
flags 默认值是 QEventLoop::AllEvents
QEventLoop::ProcessEventsFlag 有以下几种枚举值:
QEventLoop::AllEvents:处理所有事件
QEventLoop::ExcludeUserInputEvents:排除用户输入事件,如键盘和鼠标的事件
QEventLoop::ExcludeSocketNotifiers:排除网络 socket 的通知事件
QEventLoop::WaitForMoreEvents:如果没有未处理的事件,等待更多事件
*/
void QCoreApplication::processEvents(QEventLoop::ProcessEventsFlags flags = QEventLoop::AllEvents)
QCoreApplication 还有一个派发事件的静态函数 sendPostedEvents,功能是把前面用 QCoreApplication::postEvent() 发送到 Qt 事件队列里的事件立刻派发出去。如果不指定 event_type,只指定 receiver,就派发所有给这个接收者的事件;如果 event_type 和 receiver 都不指定,就派发所有用 QCoreApplication::postEvent() 发布的事件。定义如下:
/*
receiver: 接收事件的对象
event_type: 事件类型
*/
void QCoreApplication::sendPostedEvents(QObject *receiver = nullptr, int event_type = 0)
QEvent 是所有事件类的基类,但它不是一个抽象类,它也可以用于创建事件。QEvent有以下几个主要的接口函数:
void accept() // 接受事件,设置事件对象的接受标志(accept flag)
void ignore() // 忽略事件,清除事件对象的接受标志
bool isAccepted() // 是否接受事件,true表示接受,false表示忽略
bool isInputEvent() // 事件对象是不是 QInputEvent 或其派生类的实例
bool isPointerEvent() // 事件对象是不是 QPointerEvent 或其派生类的实例
bool isSinglePointEvent() // 事件对象是不是 QSinglePointEvent 或其派生类的实例
bool spontaneous() // 是不是自生事件,也就是窗口系统的事件
QEvent::Type type() // 事件类型
每个事件都有唯一的事件类型,也有对应的事件类,但是有的事件类可以处理多种类型的事件。例如,QMouseEvent 是鼠标事件类,用它创建的事件的类型可以是鼠标双击事件 QEvent::MouseButtonDblClick 或鼠标移动事件 QEvent::MouseMove 等。 常见的事件类型及其所属的事件类如下表所示:
任何从 QObject 派生的类都可以处理事件,但其中主要是从 QWidget 派生的窗口类和界面组件类需要处理事件。 一个类接收到应用程序派发来的事件后,首先会由函数 event() 处理。event()是 QObject 类中定义的一个虚函数,其函数原型定义如下:
/*
e是事件对象,通过e->type()可以得到事件的具体类型
*/
bool QObject::event(QEvent *e)
任何从 QObject 派生的类都可以重新实现函数 event(),如果一个类重新实现了函数 event(),需要在函数 event() 的实现代码里设置是否接受事件。QEvent 类有两个函数,函数 accept() 接受事件,表示事件接收者会对事件进行处理;函数 ignore() 忽略事件,表示事件接收者不接受此事件。被接受的事件由事件接收者处理,被忽略的事件则传播到事件接收者的父容器组件,由父容器组件的 event() 函数去处理,这称为事件的传播(propagation),事件最后可能会传播给窗口。
QWidget 类是所有界面组件类的基类,它重新实现了函数 event(),并针对一些典型类型的事件定义了专门的事件处理函数,函数 event() 会根据事件类型自动去运行相应的事件处理函数。例如:
void QWidget::mouseMoveEvent(QMouseEvent *event) // 对应 QEvent::MouseMove 类型事件
void QWidget::paintEvent(QPaintEvent *event) // 对应 QEvent::Paint 类型事件
如果一个自定义的类从 QWidget 派生而来,例如窗口类 Widget,如果不重新实现函数 event(),而只是要对一个典型事件进行处理,就可以重新实现 QWidget 类中定义的典型事件的处理函数。例如,我们要在窗口上绘制背景图片,就可以在窗口类 Widget 中重新定义事件处理函数 paintEvent(),在这个函数的代码里实现绘制窗口背景图片。
如果从 QWidget 或其派生类继承自定义了一个类,但需要处理的事件在 QWidget 中没有定义事件处理函数,就需要重新实现函数 event(),判断事件类型后调用自己定义的事件处理函数。
下面编写一个示例,演示对典型事件的处理。创建一个 GUI 应用程序,窗口基类选择 QWidget,窗体上放置了一个QPushButton 和一个 QLabel。在窗口类 Widget 中重新定义了一些事件处理函数:
class Widget : public QWidget {Q_OBJECT
protected:void paintEvent(QPaintEvent *event);void closeEvent(QCloseEvent *event);void keyPressEvent(QKeyEvent *event);void showEvent(QShowEvent *event);void hideEvent(QHideEvent *event);void mousePressEvent(QMouseEvent *event);
public:Widget(QWidget *parent = 0);
private: Ui::Widget *ui;
};
在窗口需要重绘时,应用程序会向窗口发送 QEvent::Paint 类型的事件,窗口对象会自动运行事件处理函数 paintEvent()。我们重新实现这个函数,在窗口上绘制背景图片,代码如下:
void Widget::paintEvent(QPaintEvent *event) {Q_UNUSED(event); QPainter painter(this);painter.drawPixmap(0, 0, this->width(), this->height(),QPixmap(":/pics/images/background.jpg"));// QWidget::paintEvent(event); // 表示运行父类的paintEvent()函数,以便父类执行其内建的一些操作。如果父类// 的事件函数里没有特殊的处理,可以不运行这行代码
}
当点击窗口右上角的关闭按钮或调用 QWidget 的 close() 函数时,系统会产生 QEvent::Close 类型的事件,事件处理函数closeEvent() 会被自动运行。我们重新实现这个函数,使用一个对话框询问是否关闭窗口,代码如下:
void Widget::closeEvent(QCloseEvent *event) {QString dlgTitle= "消息框";QString strInfo = "确定要退出吗?";QMessageBox::StandardButton result=QMessageBox::question(this, dlgTitle, strInfo,QMessageBox::Yes|QMessageBox::No |QMessageBox::Cancel);if (result == QMessageBox::Yes)event->accept(); // 接受事件,窗口可以被关闭。elseevent->ignore(); // 忽略事件,事件被传播到父容器,但是窗口不再有父容器,所以是忽略事件,窗口不能被关闭。
}
在窗口上点击鼠标按键时,会触发运行事件处理函数 mousePressEvent()。函数代码如下:
void Widget::mousePressEvent(QMouseEvent *event) {if (event->button() == Qt::LeftButton) { // 鼠标左键QPoint pt= event->pos(); // 点击点在窗口上的相对坐标QPointF relaPt= event->position(); // 相对坐标QPointF winPt= event->scenePosition(); // 相对坐标QPointF globPt= event->globalPosition(); // 屏幕或虚拟桌面上的绝对坐标}QWidget::mousePressEvent(event);
}
函数 keyPressEvent() 在键盘上的按键按下时被触发运行,编写如下代码:
void Widget::keyPressEvent(QKeyEvent *event) {QPoint pt = ui->btnMove->pos();if ((event->key() == Qt::Key_A) || (event->key() == Qt::Key_Left))ui->btnMove->move(pt.x()-20, pt.y());else if((event->key() == Qt::Key_D) || (event->key() == Qt::Key_Right))ui->btnMove->move(pt.x()+20, pt.y());else if((event->key() == Qt::Key_W) || (event->key() == Qt::Key_Up))ui->btnMove->move(pt.x(), pt.y()-20);else if((event->key() == Qt::Key_S) || (event->key() == Qt::Key_Down))ui->btnMove->move(pt.x(), pt.y()+20);event->accept(); // 接受事件,不会再传播到父容器组件
}
在窗口显示/隐藏或组件的 visible 属性变化时,事件处理函数 showEvent() 或 hideEvent() 会被触发运行。重新实现这两个函数,代码如下:
void Widget::showEvent(QShowEvent *event) {Q_UNUSED(event);qDebug("showEvent()函数被触发");
}
void Widget::hideEvent(QHideEvent *event) {Q_UNUSED(event);qDebug("hideEvent()函数被触发");
}
程序运行时我们会发现,应用程序最小化或窗口关闭时会触发函数 hideEvent(),在系统任务栏上点击应用程序重新显示其窗口时,会触发函数showEvent()。
二、事件与信号
事件通常是由窗口系统或应用程序产生的,信号则是 Qt 定义或用户自定义的。Qt 为界面组件定义的信号通常是对事件的封装,例如 QPushButton 的 clicked() 信号可以看作对 QEvent::MouseButtonRelease 类型事件的封装。
前面提到,应用程序派发给界面组件的事件首先会由其函数 event() 处理,如果函数 event() 不做任何处理,组件就会自动调用 QWidget 中与事件类型对应的默认事件处理函数。从 QWidget 派生的界面组件类一般不需要重新实现函数 event(),如果要对某种类型事件进行处理,可以重新实现对应的事件处理函数。
但是某些类型的事件没有对应的事件处理函数。例如,对于 QEvent::HoverEnter 和 QEvent::HoverLeave 类型的事件,QWidget 类中就没有对应的事件处理函数。这种情况下,如果要对这两种类型的事件进行处理,就需要自定义一个类,重新实现函数 event(),判断事件类型,针对 QEvent::HoverEnter 和 QEvent::HoverLeave 类型的事件进行相应的处理。
接下来演示如何针对事件设计自定义信号,以及如何针对事件设计自定义的事件处理函数。我们设计一个标签类 TMyLabel,它从 QLabel(QLabel 没有与鼠标双击事件对应的信号) 继承而来。TMyLabel 为鼠标双击事件定义了信号 doubleClicked(),并且对 QEvent::HoverEnter 和 QEvent::HoverLeave 类型的事件进行了处理。鼠标光标移动到标签上(HoverEnter 事件)时,标签的文字变为红色;鼠标光标离开标签(HoverLeave 事件)时,标签的文字变为黑色。TMyLabel 类的定义代码如下:
class TMyLabel : public QLabel {Q_OBJECT
public:TMyLabel(QWidget *parent = nullptr); // 构造函数需要按此参数改写bool event(QEvent *e); // 重新实现event()函数
protected:void mouseDoubleClickEvent(QMouseEvent *event); // 重新实现鼠标双击事件的默认处理函数
signals:void doubleClicked(); // 自定义信号
};
实现代码如下:
TMyLabel::TMyLabel(QWidget *parent):QLabel(parent) {this->setAttribute(Qt::WA_Hover, true); // 必须设置这个属性,才能产生hover事件
}
bool TMyLabel::event(QEvent *e) {if (e->type() == QEvent::HoverEnter) { // 鼠标光标移入QPalette plet = this->palette();plet.setColor(QPalette::WindowText, Qt::red);this->setPalette(plet);} else if (e->type() == QEvent::HoverLeave) { // 鼠标光标移出QPalette plet = this->palette();plet.setColor(QPalette::WindowText, Qt::black);this->setPalette(plet);}return QLabel::event(e); // 运行父类的 event(),处理其他类型事件
}
void TMyLabel::mouseDoubleClickEvent(QMouseEvent *event) {Q_UNUSED(event);emit doubleClicked(); // 发射信号
}
在构造函数里,将 TMyLabel 的 Qt::WA_Hover 属性设置为true(默认值是false)。这样,鼠标光标移入和移出 TMyLabel 组件时,才会分别产生 QEvent::HoverEnter 和 QEvent::HoverLeave 类型的事件。注意,函数 event() 里的最后一行代码是必需的,它表示要运行父类 QLabel 的 event() 函数,因为在 TMyLabel 的 event() 函数里只对两个事件进行了处理,对于其他典型事件,还需要交给父类去处理。 mouseDoubleClickEvent() 是鼠标双击事件的默认处理函数,重新实现的这个函数里就发射了自定义信号 doubleClicked()。这样,我们就把鼠标双击事件转换为发射一个信号,如果要对 TMyLabel 组件的鼠标双击事件进行处理,只需为其 doubleClicked() 信号编写槽函数即可。