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(); // 接受事件,窗口可以被关闭。
else
event->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() 信号编写槽函数即可。
三、事件过滤器
我们知道,事件产生后会被派发给接收者,由接收者的 event() 函数去处理。QObject 还提供了另一种处理事件的方法:事件过滤器。它可以将一个对象的事件委托给另一个对象来监视并处理,例如,一个窗口可以作为其界面上的 QLabel 组件的事件过滤器,派发给 QLabel 组件的事件由窗口去处理。要实现事件过滤器功能,需要完成两项操作:
- 被监视对象使用函数 installEventFilter() 将自己注册给监视对象,监视对象就是事件过滤器。
- 监视对象重新实现函数 eventFilter(),对监视到的事件进行处理。
void QObject::installEventFilter(QObject *filterObj)
bool QObject::eventFilter(QObject *watched, QEvent *event)
被监视的对象调用函数 installEventFilter(),将对象 filterObj 设置为自己的事件过滤器。 作为事件过滤器的监视对象需要重新实现函数 eventFilter(),参数 watched 是被监视对象,event 是产生的事件,这个函数有一个返回值,如果返回 true,事件就不会再传播给其他对象,事件处理结束;如果返回false,事件会继续传播给事件接收者做进一步处理。 接下来演示一下事件过滤器的用法。定义一个继承于 QWidget 的窗口类 Widget,重新定义它的 eventFilter() 函数:
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) {
ui->setupUi(this);
// 两个QLabel组件将窗口对象设置为自己的事件过滤器
ui->labHover->installEventFilter(this);
ui->labDBClick->installEventFilter(this);
}
bool Widget::eventFilter(QObject *watched, QEvent *event) {
// labHover组件的事件处理
if (watched == ui->labHover) {
if (event->type()== QEvent::Enter) { // 鼠标光标移入
ui->labHover->setStyleSheet("background-color: rgb(170, 255, 255);");
} else if (event->type()== QEvent::Leave) { // 鼠标光标离开
ui->labHover->setStyleSheet("");
ui->labHover->setText("靠近我,点击我");
} else if (event->type()== QEvent::MouseButtonPress) { // 鼠标键按下
ui->labHover->setText("button pressed");
} else if (event->type()== QEvent::MouseButtonRelease) { // 鼠标键释放
ui->labHover->setText("button released");
}
}
// labDBClick组件的事件处理
if (watched == ui->labDBClick) {
if (event->type()== QEvent::Enter) { // 鼠标光标移入
ui->labDBClick->setStyleSheet("background-color: rgb(85, 255, 127);");
} else if (event->type()== QEvent::Leave) { // 鼠标光标离开
ui->labDBClick->setStyleSheet("");
ui->labDBClick->setText("可双击的标签");
} else if (event->type()== QEvent::MouseButtonDblClick) { // 鼠标双击
ui->labDBClick->setText("double clicked");
}
}
return QWidget::eventFilter(watched,event); // 运行父类的 eventFilter()函数
// return true; // 有问题,不能直接返回true
}
函数 eventFilter() 最后一行代码是运行父类的 eventFilter() 函数。不能用 return true 替代这一行代码。如果函数 eventFilter() 直接返回 true,事件过滤器拦截的事件将不会传播给被监视对象,而在这个类的 eventFilter() 函数里,我们只处理了被监视对象的少数几个事件,例如 QEvent::Paint 类型的事件就没有处理。
四、拖放事件与拖放操作
拖放由两个操作组成:拖动(drag)和放置(drop)。拖动点与放置点可以是同一个组件,也可以是不同的组件,甚至是不同的应用程序。 整个拖放操作可以分解为两个过程:
- 拖动点启动拖动操作。被拖动组件通过 mousePressEvent() 和 mouseMoveEvent() 这两个事件处理函数的处理,检测到鼠标左键按下并移动时就可以启动拖动操作。启动拖动操作需要创建一个 QDrag 对象描述拖动操作,以及创建一个 QMimeData 类的对象用于存储拖动操作的格式信息和数据,并将其赋值为 QDrag 对象的 mimeData 属性。
- 放置点处理放置操作。当拖动操作移动到放置点范围内时,首先触发 dragEnterEvent() 事件处理函数,在此函数里一般要通过 QDrag 对象的 mimeData 数据判断拖动操作的来源和参数,以决定是否接受此拖动操作。只有被接受的拖动操作才可以被放置,并触发 dropEvent() 事件处理函数。函数 dropEvent() 用于处理放置时的具体操作。
从这个过程可以看到,拖动点和放置点最好是各自实现相关事件处理的类,如果要在同一个窗口上实现这些事件的处理,需要用到事件过滤器。QWidget 类有一个属性 acceptDrops,如果设置为 true,那么对应的这个组件就可以作为一个放置点。属性 acceptDrops 的默认值为 false。QWidget 类中没有定义拖动操作相关的函数,所以一般的界面组件是不能作为拖动点的。QAbstractItemView 类定义了更多与拖动操作相关的函数,所以,QListWidget、QTreeWidget、QTableWidget 等组件既可以作为拖动点,也可以作为放置点。
本节演示一个放置点功能的实现:从 Windows 资源管理器中将一个 JPG 图片文件拖动到程序窗口上,程序会显示拖动事件的 mimeData 数据,并显示图片。窗口基类是 QWidget,窗体上只放置一个 QPlainTextEdit 组件和一个 QLabel 组件。QLabel 组件的 scaledContents 属性设置为 true,使图片适应 QLabel 组件的大小。在窗口类 Widget 中的 protected 部分定义了需要重新实现的2个事件处理函数,定义如下:
protected:
void dragEnterEvent(QDragEnterEvent *event); // 拖动文件进入窗口时触发的事件处理函数
void dropEvent(QDropEvent *event); // 拖动文件在窗口上放置时触发的事件处理函数
Widget 的构造函数如下:
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) {
ui->setupUi(this);
ui->labPic->setScaledContents(true); // 图片适应组件大小
this->setAcceptDrops(true); // 由窗口接受放置操作
ui->plainTextEdit->setAcceptDrops(false); // 不接受放置操作,由窗口去处理
ui->labPic->setAcceptDrops(false); // 不接受放置操作,由窗口去处理
}
注意,组件 plainTextEdit 和 labPic 的 acceptDrops 属性都设置为false,但是窗口的 acceptDrops 属性设置为true,这样,在这两个组件上的拖放操作事件都会自动传播给窗口,由窗口去处理。所以,我们只需在Widget类中定义事件处理函数,而不需要将窗口注册为这两个界面组件的事件过滤器,从而简化了处理流程。窗口的 dragEnterEvent() 事件处理函数,该函数的代码如下:
void Widget::dragEnterEvent(QDragEnterEvent *event) {
// 显示 MIME 信息
ui->plainTextEdit->clear();
ui->plainTextEdit->appendPlainText("dragEnterEvent 事件 mimeData()->formats()");
for(int i = 0; i < event->mimeData()->formats().size(); i++)
ui->plainTextEdit->appendPlainText(event->mimeData()->formats().at(i));
ui->plainTextEdit->appendPlainText("\n dragEnterEvent 事件 mimeData()->urls()");
for(int i = 0; i < event->mimeData()->urls().size(); i++) {
QUrl url= event->mimeData()->urls().at(i); // 带路径文件名
ui->plainTextEdit->appendPlainText(url.path());
}
if (event->mimeData()->hasUrls()) {
QString filename= event->mimeData()->urls().at(0).fileName(); // 获取文件名
QFileInfo fileInfo(filename); // 获取文件信息
QString ext= fileInfo.suffix().toUpper(); // 获取文件后缀
if (ext == "JPG")
event->acceptProposedAction(); // 接受拖动操作
else
event->ignore(); // 忽略事件
} else
event->ignore();
}
函数 dragEnterEvent() 的输入参数 event 是 QDragEnterEvent 类型指针,event->mimeData() 返回一个 QMimeData 对象,这个对象记录了拖动操作数据源的一些关键信息。多用途互联网邮件扩展(multipurpose internet mail extensions,MIME)被设计的最初目的是在发送电子邮件时附加多媒体数据,使邮件客户端程序能根据其类型进行处理。QMimeData 是对 MIME 数据的封装,在拖放操作和剪贴板操作中都用 QMimeData 类描述传输的数据。函数 QMimeData::formats() 返回对象支持的 MIME 格式的字符串列表。示例程序将函数 formats() 返回的格式全部显示出来。在本示例程序运行时,从 Windows 资源管理器中将一个 JPG 文件拖放到窗口上时,函数 formats() 返回的格式列表如下:
application/x-qt-windows-mime;value="Shell IDList Array"
application/x-qt-windows-mime;value="UsingDefaultDragImage"
application/x-qt-windows-mime;value="DragImageBits"
application/x-qt-windows-mime;value="DragContext"
application/x-qt-windows-mime;value="DragSourceHelperFlags"
application/x-qt-windows-mime;value="InShellDragLoop"
text/uri-list
application/x-qt-windows-mime;value="FileName"
application/x-qt-windows-mime;value="FileContents"
application/x-qt-windows-mime;value="FileNameW"
application/x-qt-windows-mime;value="FileGroupDescriptorW"
其中,application/x-qt-windows-mime 是 Windows 平台上自定义的 MIME 格式;text/uri-list 是标准的 MIME 格式,表示 URL 或本机上的文件来源。本示例接收从 Windows 资源管理器拖动来的一
个 JPG 文件,所以MIME的格式中有 text/uri-list。 知道 MIME 的数据格式是text/uri-list后,就可以用QMimeData 的函数 urls() 获取一个列表。程序中用代码显示了函数 urls() 返回的列表的内容。函数 QMimeData::urls() 返回的结果是 QUrl 类的列表数据,函数 QUrl::path() 返回 URL 的路径,对于本机上的文件就是带路径的文件名。本示例在 Windows 上运行时返回的文件名类似于下面的字符串,在字符串开头有一个额外的“/”:
/C:/Users/wwb/Pictures/Saved Pictures/IMG_110946.jpg
QMimeData 对常见的 MIME 格式有相应的判断函数、获取数据的函数和设置函数,如下表所示:

dragEnterEvent() 中判断 MIME 数据格式以及来源文件是否为 JPG 文件,然后调用 QDragEnterEvent 的 acceptProposedAction() 函数或 ignore() 函数进行相应的处理:
- 函数 acceptProposedAction() 表示接受拖动操作,允许后续的放置操作。
- 函数 ignore() 表示不接受拖动操作,不允许后续的放置操作。
当一个被接受的拖动操作在窗口上放置时,会触发事件处理函数 dropEvent(),函数的代码如下:
void Widget::dropEvent(QDropEvent *event) {
QString filename= event->mimeData()->urls().at(0).path(); // 完整文件名
filename = filename.right(filename.length()-1); // 去掉最左边的“/”
QPixmap pixmap(filename);
ui->labPic->setPixmap(pixmap);
event->accept();
}
五、具有拖放操作功能的组件
例如 QLineEdit、QAbstractItemView、QStandardItem 等类都有一个函数 setDragEnabled(bool),当设置参数为 true 时,组件就可以作为一个拖动点。QAbstractItemView 类定义了拖放操作相关的各种函数,通过这些函数的设置,QListView、QTableView、QTreeView 及其对应的便利类都会具有非常方便的节点拖放操作功能。本节设计一个示例项目,演示 QListWidget、QTableWidget、QTreeWidget 的拖放操作功能。示例程序窗口上有4个具有拖放操作功能的界面组件:

- 一个 QListWidget 组件,对象名称为 listSource;
- 一个 QListWidget 组件,对象名称是 listWidget;
- 一个 QTreeWidget 组件,对象名称是treeWidget;
- 一个 QTableWidget 组件,对象名称是 tableWidget。
窗口基类是 QWidget,窗口类 Widget 的定义代码如下:
class Widget : public QWidget {
Q_OBJECT
private:
int getDropActionIndex(Qt::DropAction actionType); // 将枚举值转换为index
Qt::DropAction getDropActionType(int index); // 将index转换为枚举值
QAbstractItemView *m_itemView = nullptr; // 当前设置属性的组件
void refreshToUI(QGroupBox *curGroupBox); // 将组件的属性显示到界面上
protected:
bool eventFilter(QObject *watched, QEvent *event);
public:
Widget(QWidget *parent = nullptr);
private:
Ui::Widget *ui;
}
Widget 类重新定义了事件过滤器函数 eventFilter(),这是为了将窗口作为4个项数据组件的事件过滤器,来处理它们的 QEvent::KeyPress 类型事件。Widget 类的构造函数代码如下:
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {
ui->setupUi(this);
// 安装事件过滤器,由窗口处理4个项数据组件的事件
ui->listSource->installEventFilter(this);
ui->listWidget->installEventFilter(this);
ui->treeWidget->installEventFilter(this);
ui->tableWidget->installEventFilter(this);
// 设置 4 个项数据组件的拖放操作相关属性
ui->listSource->setAcceptDrops(true); // 使组件可以作为放置点接受放置操作
ui->listSource->setDragDropMode(QAbstractItemView::DragDrop);
ui->listSource->setDragEnabled(true); // 使组件可以作为拖动点启动拖动操作
ui->listSource->setDefaultDropAction(Qt::CopyAction);
ui->listWidget->setAcceptDrops(true);
ui->listWidget->setDragDropMode(QAbstractItemView::DragDrop);
ui->listWidget->setDragEnabled(true);
ui->listWidget->setDefaultDropAction(Qt::CopyAction);
ui->treeWidget->setAcceptDrops(true);
ui->treeWidget->setDragDropMode(QAbstractItemView::DragDrop);
ui->treeWidget->setDragEnabled(true);
ui->treeWidget->setDefaultDropAction(Qt::CopyAction);
ui->tableWidget->setAcceptDrops(true);
ui->tableWidget->setDragDropMode(QAbstractItemView::DragDrop);
ui->tableWidget->setDragEnabled(true);
ui->tableWidget->setDefaultDropAction(Qt::MoveAction);
}
在窗口上的“设置对象”分组框里点击某个单选按钮时,右侧的“拖放参数设置”分组框里就会显示选中组件的拖放操作属性。这4个单选按钮的 clicked() 信号的槽函数以及相关的两个自定义函数的代码如下:
void Widget::on_radio_Source_clicked() { // listSource 单选按钮
m_itemView= ui->listSource; // 当前设置属性的组件
refreshToUI(ui->groupBox_1); // 属性刷新显示到界面上
}
void Widget::on_radio_List_clicked() { // listWidget 单选按钮
m_itemView= ui->listWidget;
refreshToUI(ui->groupBox_2);
}
void Widget::on_radio_Tree_clicked() { // treeWidget 单选按钮
m_itemView= ui->treeWidget;
refreshToUI(ui->groupBox_3);
}
void Widget::on_radio_Table_clicked() { // tableWidget 单选按钮
m_itemView= ui->tableWidget;
refreshToUI(ui->groupBox_4);
}
void Widget::refreshToUI(QGroupBox *curGroupBox) { // 组件的属性显示到界面上
ui->chkBox_AcceptDrops->setChecked(m_itemView->acceptDrops()); // acceptDrops 复选框
ui->chkBox_DragEnabled->setChecked(m_itemView->dragEnabled()); // dragEnabled 复选框
ui->combo_Mode->setCurrentIndex((int)m_itemView->dragDropMode()); // dragDropMode 下拉列表框
int index = getDropActionIndex(m_itemView->defaultDropAction());
ui->combo_DefaultAction->setCurrentIndex(index); // defaultDropAction下拉列表框
QFont font = ui->groupBox_1->font();
font.setBold(false);
ui->groupBox_1->setFont(font);
ui->groupBox_2->setFont(font);
ui->groupBox_3->setFont(font);
ui->groupBox_4->setFont(font);
font.setBold(true);
curGroupBox->setFont(font); // 当前设置属性的组件所在分组框文字用粗体
}
int Widget::getDropActionIndex(Qt::DropAction actionType) { // 根据Qt::DropAction 的枚举值,获取下拉列表框中的索引
switch (actionType) {
case Qt::CopyAction:
return 0;
case Qt::MoveAction:
return 1;
case Qt::LinkAction:
return 2;
case Qt::IgnoreAction:
return 3;
default:
return 0;
}
}
- 函数 acceptDrops():返回一个bool类型的值,表示组件是否可以作为放置点接受放置操作;
- 函数 dragEnabled():返回一个bool类型的值,表示组件是否可以作为拖动点启动拖动操作;
- 函数 dragDropMode():返回结果是枚举类型 QAbstractItemView::DragDropMode,表示拖放操作模式,各枚举值如下表所示:

- 函数 defaultDropAction():返回结果是枚举类型 Qt::DropAction。当组件作为放置点时,它表示在完成拖放时数据操作的模式,各枚举值如下表所示:

选择一个设置对象后,就可以通过窗口上“拖放参数设置”分组框里的4个组件设置所选组件的拖放操作属性,相关代码如下:
void Widget::on_chkBox_AcceptDrops_clicked(bool checked) { // acceptDrops 复选框
m_itemView->setAcceptDrops(checked);
}
void Widget::on_chkBox_DragEnabled_clicked(bool checked) { // dragEnabled 复选框
m_itemView->setDragEnabled(checked);
}
void Widget::on_combo_Mode_currentIndexChanged(int index) { // dragDropMode 下拉列表框
QAbstractItemView::DragDropMode mode = (QAbstractItemView::DragDropMode)index;
m_itemView->setDragDropMode(mode);
}
void Widget::on_combo_DefaultAction_currentIndexChanged(int index) { // defaultDropAction 下拉列表框
Qt::DropAction actionType= getDropActionType(index);
m_itemView->setDefaultDropAction(actionType);
}
Qt::DropAction Widget::getDropActionType(int index) { // 根据下拉列表框的索引,返回Qt::DropAction类型的枚举值
switch (index) {
case 0:
return Qt::CopyAction;
case 1:
return Qt::MoveAction;
case 2:
return Qt::LinkAction;
case 3:
return Qt::IgnoreAction;
default:
return Qt::CopyAction;
}
}
- 函数 setAcceptDrops(bool):设置为 true 时,组件作为放置点,可接受放置操作;
- 函数 setDragEnabled(bool):设置为 true 时,组件作为拖动点,可以启动拖动操作;
- 函数 setDragDropMode(mode):参数 mode 是枚举类型 QAbstractItemView::DragDropMode,用于设置拖放操作模式。使用函数 setDragDropMode() 设置拖放操作模式时,相当于用不同的组合调用了 setAcceptDrops() 和 setDragEnabled(),例如运行下面的语句:
setDragDropMode(QAbstractItemView::DragOnly)
就相当于运行了
setAcceptDrops(false);
setDragEnabled(true);
- 函数 setDefaultDropAction(dropAction):参数 dropAction 是枚举类型 Qt::DropAction,用于设置完成拖放操作时源组件的数据操作方式。
在 Widget 类的构造函数中,4个可拖放操作组件通过函数 installEventFilter() 将窗口作为自己的事件过滤器。Widget 类重新实现了函数 eventFilter(),对这4个组件的事件进行处理:
bool Widget::eventFilter(QObject *watched, QEvent *event) {
if (event->type() != QEvent::KeyPress) // 不是 KeyPress 事件,退出
return QWidget::eventFilter(watched,event);
QKeyEvent *keyEvent= static_cast<QKeyEvent *>(event);
if (keyEvent->key() != Qt::Key_Delete) // 按下的不是 Delete 键,退出
return QWidget::eventFilter(watched,event);
if (watched == ui->listSource) {
QListWidgetItem *item= ui->listSource->takeItem(ui->listSource->currentRow());
delete item;
} else if (watched == ui->listWidget) {
QListWidgetItem *item= ui->listWidget->takeItem(ui->listWidget->currentRow());
delete item;
} else if (watched == ui->treeWidget) {
QTreeWidgetItem *curItem= ui->treeWidget->currentItem();
if (curItem->parent() != nullptr) {
QTreeWidgetItem *parItem= curItem->parent();
parItem->removeChild(curItem);
} else {
int index= ui->treeWidget->indexOfTopLevelItem(curItem);
ui->treeWidget->takeTopLevelItem(index);
}
delete curItem;
} else if (watched == ui->tableWidget) {
QTableWidgetItem *item= ui->tableWidget->takeItem(
ui->tableWidget->currentRow(),
ui->tableWidget->currentColumn());
delete item;
}
return true; // 表示事件已经被处理
}
这个函数只处理了4个项数据组件的 QEvent::KeyPress 类型的事件,且在按下的是Delete键时才处理。程序通过输入参数 watched 判断是哪个界面组件,然后删除该组件的当前项。
更多推荐
所有评论(0)