首页 Qt 学习之路 2 Qt 学习之路 2(23):自定义事件

Qt 学习之路 2(23):自定义事件

32 3.7K

尽管 Qt 已经提供了很多事件,但对于更加千变万化的需求来说,有限的事件都是不够的。例如,我要支持一种新的设备,这个设备提供一种崭新的交互方式,那么,这种事件如何处理呢?所以,允许创建自己的事件 类型也就势在必行。即便是不说那种非常极端的例子,在多线程的程序中,自定义事件也是尤其有用。当然,事件也并不是局限在多线程中,它可以用在单线程的程序中,作为一种对象间通讯的机制。那么,为什么我需要使用事件,而不是信号槽呢?主要原因是,事件的分发既可以是同步的,又可以是异步的,而函数的调用或者说是槽的回调总是同步的。事件的另外一个好处是,它可以使用过滤器。

Qt 自定义事件很简单,同其它类库的使用很相似,都是要继承一个类进行扩展。在 Qt 中,你需要继承的类是QEvent

继承QEvent类,最重要的是提供一个QEvent::Type类型的参数,作为自定义事件的类型值。回忆一下,这个 type 是我们在处理事件时用于识别事件类型的代号。比如在event()函数中,我们使用QEvent::type()获得这个事件类型,然后与我们定义的实际类型对比。

QEvent::TypeQEvent定义的一个枚举。因此,我们可以传递一个 int 值。但是需要注意的是,我们的自定义事件类型不能和已经存在的 type 值重复,否则会有不可预料的错误发生。因为系统会将你新增加的事件当做系统事件进行派发和调用。在 Qt 中,系统保留 0 - 999 的值,也就是说,你的事件 type 要大于 999。这种数值当然非常难记,所以 Qt 定义了两个边界值:QEvent::UserQEvent::MaxUser。我们的自定义事件的 type 应该在这两个值的范围之间。其中,QEvent::User的值是 1000,QEvent::MaxUser的值是 65535。从这里知道,我们最多可以定义 64536 个事件。通过这两个枚举值,我们可以保证我们自己的事件类型不会覆盖系统定义的事件类型。但是,这样并不能保证自定义事件相互之间不会被覆盖。为了解决这个问题,Qt 提供了一个函数:registerEventType(),用于自定义事件的注册。该函数签名如下:

static int QEvent::registerEventType ( int hint = -1 );

这个函数是 static 的,因此可以使用QEvent类直接调用。函数接受一个 int 值,其默认值是 -1;函数返回值是向系统注册的新的 Type 类型的值。如果 hint 是合法的,也就是说这个 hint 不会发生任何覆盖(系统的以及其它自定义事件的),则会直接返回这个值;否则,系统会自动分配一个合法值并返回。因此,使用这个函数即可完成 type 值的指定。这个函数是线程安全的,不必另外添加同步。

我们可以在QEvent子类中添加自己的事件所需要的数据,然后进行事件的发送。Qt 中提供了两种事件发送方式:

1.

static bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event);

直接将event事件发送给receiver接受者,使用的是QCoreApplication::notify()函数。函数返回值就是事件处理函数的返回值。在事件被发送的时候,event对象并不会被销毁。通常我们会在栈上创建event对象,例如:

QMouseEvent event(QEvent::MouseButtonPress, pos, 0, 0, 0);
QApplication::sendEvent(mainWindow, &event);

2.

static void QCoreApplication::postEvent(QObject *receiver, QEvent *event);

event事件及其接受者receiver一同追加到事件队列中,函数立即返回。

因为 post 事件队列会持有事件对象,并且在其 post 的时候将其 delete 掉,因此,我们必须在堆上创建event对象。当对象被发送之后,再试图访问event对象就会出现问题(因为 post 之后,event对象就会被 delete)。

当控制权返回到主线程循环时,保存在事件队列中的所有事件都通过notify()函数发送出去。

事件会根据 post 的顺序进行处理。如果你想要改变事件的处理顺序,可以考虑为其指定一个优先级。默认的优先级是Qt::NormalEventPriority

这个函数是线程安全的。

Qt 还提供了一个函数:

static void QCoreApplication::sendPostedEvents(QObject *receiver, int event_type);

这个函数的作用是,将事件队列中的接受者为receiver,事件类似为 event_type 的所有事件立即发送给 receiver 进行处理。需要注意的是,来自窗口系统的事件并不由这个函数进行处理,而是processEvent()。详细信息请参考 Qt API 手册。

现在,我们已经能够自定义事件对象,已经能够将事件发送出去,还剩下最后一步:处理自定义事件。处理自定义事件,同前面我们讲解的那些处理方法没有什么区别。我们可以重写QObject::customEvent()函数,该函数接收一个QEvent对象作为参数:

void QObject::customEvent(QEvent *event);

我们可以通过转换 event 对象类型来判断不同的事件:

void CustomWidget::customEvent(QEvent *event) {
    CustomEvent *customEvent = static_cast<CustomEvent *>(event);
    // ...
}

当然,我们也可以在event()函数中直接处理:

bool CustomWidget::event(QEvent *event) {
    if (event->type() == MyCustomEventType) {
        CustomEvent *myEvent = static_cast<CustomEvent *>(event);
        // processing...
        return true;
    }
    return QWidget::event(event);
}

32 评论

锁骨断了 2012年10月25日 - 14:30

😯 能不能讲讲udev->dbus->event的工作过程,嵌入式设备都是走的这种方案(dfb除外)

回复
DevBean 2012年10月25日 - 15:05

这个以后会考虑的,因为我对嵌入式不是很熟悉,所以一直没有这方面的计划

回复
adfae 2012年11月25日 - 18:29

你好,看完这篇文章后我就想亲自实现一回,不过实在不知要如何开头...
LZ能出个示例吗?

回复
DevBean 2012年11月25日 - 23:54

简单的示例,你可以尝试实现比如数据改变之类的事件——虽然这种事件可以使用信号槽来实现。只要了解了具体过程,在实际应用中再详细设计就好了。

回复
OryJuvog 2013年8月20日 - 22:42

您好,能不能讲解一下 processEvent()。我做了个程序 有一个按钮和一个进度条,我想按下按钮进度条每一秒前进1%,但是我按右上角关闭键的时候,程序界面是退出了,进程还在等待循环结束才退出,这怎么办,按钮按下对应的槽函数如下 void F() {
QElapsedTimer t;
for (int i = 1; i setValue(i);
t.start();
while (t.elapsed() < 1000)
{
QCoreApplication::processEvents();
}
}
}

回复
OryJuvog 2013年8月20日 - 22:43

QElapsedTimer t;
for (int i = 1; i setValue(i);
t.start();
while (t.elapsed() < 1000)
{
QCoreApplication::processEvents();
}
}
for 那里怎么少了一段

回复
豆子 2013年8月22日 - 17:10

具体不大清楚,不是不可以在点击关闭按钮的时候调用下 QApplication::quit() 槽?

回复
scutlpf 2014年2月19日 - 19:39

”事件的分发既可以是同步的,又可以是异步的,而函数的调用或者说是槽的回调总是同步的。“这句话怎么理解?

回复
豆子 2014年2月20日 - 09:46

同步就是调用者必须等到被调用者执行完毕之后才能继续执行;异步就是被调用者在被调用之后立刻返回给调用者,调用者继续执行。函数调用和槽的执行都是同步的,就是要等到被调用的函数或槽执行完毕,才返回给调用者继续执行,可以认为调用者被阻塞了。对于事件,使用 sendEvent 函数就是同步调用,使用 postEvent 就是异步调用。

回复
datde 2014年5月21日 - 19:54

你好,我写了一个事件监控类,
//事件监控者
class MyWatcher: public QObject
{
public:
bool eventFilter(QObject *watched, QEvent *event)
{
if(event->type() == MyCustomEventType)
{
qDebug()<<"I don't wanna filter MyEventType";
return false;
}
return QObject::eventFilter(watched,event);
}
};
该类可以正常输出I don't wanna filter MyEventType字串,可是
当在类前面加入Q_OBJECT宏后,该字串反倒不输出了,事件也不在继续传递了。请问Q_OBJECT不能用在事件相关类中么

回复
豆子 2014年5月23日 - 15:10

没有这个限制,可能是哪里出错了。添加过 Q_OBJECT 宏之后最好重新 qmake 一下再编译。

回复
11 2016年1月18日 - 10:58

当控制权返回到主线程循环是
是->时

回复
豆子 2016年1月18日 - 12:33

多谢指出

回复
syt 2016年3月20日 - 22:21

豆子你好,我是个Qt新手,非常感谢你的分享,我从中学到了很多。
我在自定义事件中遇到了些问题,希望能得到你的帮助:
下面是我的代码:
//customevent.h
const QEvent::Type MY_EVENT_TYPE = (QEvent::Type)QEvent::registerEventType(QEvent::User + 10);

class SytEvent:public QEvent{
public:
SytEvent(QEvent::Type type = MY_EVENT_TYPE):QEvent(type)
{
qDebug()<<"in constructor, MY_EVENT_TYPE == "<<MY_EVENT_TYPE;
qDebug()<type() == "<type();
qDebug()<<"send";
}

};

//mainwindow.h
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
bool event(QEvent *event);
};

//mainwindow.cpp

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
}

MainWindow::~MainWindow()
{
}
bool MainWindow::event(QEvent *event)
{
qDebug()<type() == "<type();
qDebug()<<"in event(), MY_EVENT_TYPE == "<type() == QEvent::Type(1010)){
qDebug()<<"got sytEvent";
return true;
}else{
qDebug()<<"didn't get";
return false;
}
}

//main.cpp
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
SytEvent sytEvent;
qDebug()<<"in main before send, MY_EVENT_TYPE == "<<MY_EVENT_TYPE;
QCoreApplication::sendEvent(&w,&sytEvent);
qDebug()<<"in main after send, MY_EVENT_TYPE == "<type() == QEvent::Type(1010)
send
in main before send, MY_EVENT_TYPE == QEvent::Type(1010)
in event(), event->type() == QEvent::Type(1010)
in event(), MY_EVENT_TYPE == QEvent::Type(MaxUser)
got sytEvent
in main after send, MY_EVENT_TYPE == QEvent::Type(1010)

其中我不明白的是,在进入event()函数后,我的 MY_EVENT_TYPE 为什么变成了QEvent::Type(MaxUser), 而不是我之前自己定义的QEvent::Type(User + 10)?

回复
syt 2016年3月21日 - 08:35

最后的main.cpp和输出结果内容缺失了,我再补充下:
//main.cpp
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
SytEvent sytEvent;

qDebug()<<"in main before send, MY_EVENT_TYPE == "<<MY_EVENT_TYPE;
QCoreApplication::sendEvent(&w,&sytEvent);
qDebug()<<"in main after send, MY_EVENT_TYPE == "<type() == QEvent::Type(1010)
send
in main before send, MY_EVENT_TYPE == QEvent::Type(1010)
in event(), event->type() == QEvent::Type(1010)
in event(), MY_EVENT_TYPE == QEvent::Type(MaxUser)
got sytEvent
in main after send, MY_EVENT_TYPE == QEvent::Type(1010)

回复
豆子 2016年3月21日 - 12:52

这里的代码贴出来少了很多东西,语句都不能通过编译。方便的话还是发到邮箱比较好一些

回复
时代 2016年4月28日 - 14:43

你好,豆子前辈,我不太明白,动作事件和信号之间怎么区分使用哪个,就是他们是不是各有各的用途?

回复
豆子 2016年4月29日 - 14:16

信号是使用组件时会用到的,事件一般是自定义组件时会用到。由于事件处理函数都是 protected 的,因此需要继承实现。不过这个也没有一个精准的答案,还是看需求而定

回复
sat 2017年2月7日 - 23:24

请问豆子哥,我写了一个QEvent的子类CustomEvent,字类调用了registerEventType,但是当我发送事件时并没有在窗口中发现我要的自定义事件,反倒是直接在构造函数里指定事件号,不调用registerEventType就可以在窗口中发现我要的自定义事件,这是我的qevent子类构造
CustomEvent(Type type = (Type)1010) :
QEvent(type)
{ }
然后在窗口中重新实现了按键, _e是QEvent *, this是窗口Qapplication的子类
void keyPressEvent(QKeyEvent *)
{
QCoreApplication::sendEvent(this, _e);
}
然后窗口类的event,t是 typename QEvent::Type _t;
bool event(QEvent *e)
{
if(e->type() == _t)
{
qDebug() << "my custom event: 1010";
return true;
}
return QWidget::event(e); // by default
}
不知道registerEventType函数怎么用?

回复
Alien You 2017年5月3日 - 19:55

豆哥,学习了一段时间您的写的qt文章,收获还是蛮大的,不知道您最近是不是太忙了,这边一直没有回复,不过我自己讲下我自己的一些想法吧,您这边给了很多有一些QT文档中一些英文看不太懂的解释,但是我个人感觉一些都是停留在文档中的解释上,没有实际的应用,就像前面notify函数还有这边的sendPostedEvents()函数等等,都是一些与QT文档中的解释相关的,我学习的时候只是感觉有这个函数及函数的意思和作用,但是不知道要怎么实际去用它,我觉得这一点在我看到的目前为止的文章里面没有看到的,我觉着您对于解释,可以多增加些要怎么用,与哪些函数一起配合使用什么的,这样可以加强理解,对于我这种相对比较笨的学习者来说我的感受就这些吧,但是这些只是我的一些想法,没有任何偏见。

回复
豆子 2017年5月6日 - 11:45

我是想先介绍这些函数,在用到的时候知道有这么个东西,然后去查阅文档的时候能够知道查哪方面的,看具体怎么使用。因为内容太多了,不可能把一些实际使用都罗列出来。

回复
Z 2019年7月11日 - 11:37

自定义事件一般在什么情况下才需要用阿?我看的一些代码很少看到自定义事件的

回复
qting 2019年7月11日 - 22:49

什么时候添加Q_OBJECT宏会报错呢

回复
ZONGHAN GAN 2020年2月27日 - 07:50

请问豆子哥,最近要改一个GUI pong游戏。在mainwindow.cpp 中ui->boardView->installEventFilter(*evtflt), 需要将原来evtfilt中的keypress事件type换成了自定义事件MyEvent (大体上需要使用遥感信号(通过UDP)代替左右键),可是自定义事件需要在main.cpp中通过 QCoreApplication::sendEvent函数手动发送给目标(qt自带的事件类型好像不用,按键,鼠标等似乎是自动在触发后发送的). main.cpp中定义了mainwindow w, ui在w内, boardView又在ui内,测试后发现事件根本没有被发送到evtflt,求解

回复
ZONGHAN GAN 2020年2月27日 - 07:55

更进一步,能不能不从主函数发送事件,比如说,定义接受信号的udpreceiver.cpp, 当socket发现有信号时readyread()自动发送信号给绑定的receive()槽,然后再receive中直接实现自定义事件的声明 (MyEvent myevent)并循环发送给eventfilter?
代码在stackoverflow https://stackoverflow.com/questions/60424216/how-to-use-eventfilter-under-child-widget-to-catch-self-define-event

回复
小学生Kane 2020年8月20日 - 11:23

最后的代码,CustomEvent 到底是函数还是类呢

回复
小学生Kane 2020年8月24日 - 11:37

弄懂了。。。

回复
enak 2020年8月24日 - 11:44

所以处理自定义事件的第一种方法,customEvent是需要手动调用吗,否则如何能确保传入的QEvent *event就是CustomEvent *类型的呢

回复
豆子 2020年8月28日 - 08:59

customEvent() 不需要手动调用,Qt 会自己调用;我们需要根据 event 的 type 或者强制转换来判断是哪种事件类型

回复
Nephren 2021年6月24日 - 14:17

总结:
1、通过派生QEvent实现自定义事件,registerEventType 注册新事件类型。
2、信号槽是同步的,信号发生立刻就会调用槽函数,事件则可以sendEvent同步立即处理事件可以postEvent加到事件队列由事件队列异步处理。
3、响应时重写QObject::customEvent即可,QObject::event中对于用户自定义事件会自动调用customEvent。处理可以用事件类型转换来判断,可以获取事件type做判断。

回复
jackxia 2023年1月12日 - 23:15

亲爱的豆子先生,你讲述的QT方面知识特别深入,难得的文章,为什么不考虑这书呢,你要这书我肯定买。

回复
jackxia 2023年1月12日 - 23:17

亲爱的豆子先生,你讲述的QT方面知识特别深入,难得的文章,为什么不考虑出书呢,你要出书我肯定买。你的讲解特别清晰。

回复

发表评论

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2