事件(event)是由系统或者 Qt 本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件在对用户操作做出响应时发出,如键盘事件等;另一些事件则是由系统自动发出,如计时器事件。
事件也就是我们通常说的“事件驱动(event drive)”程序设计的基础概念。事件的出现,使得程序代码不会按照原始的线性顺序执行。想想看,从最初的 C 语言开始,我们的程序就是以一种线性的顺序执行代码:这一条语句执行之后,开始执行下一条语句;这一个函数执行过后,开始执行下一个函数。这种类似“批处理”的程序设计风格显然不适合于处理复杂的用户交互。我们来想象一下用户交互的情景:我们设计了一堆功能放在界面上,用户点击了“打开文件”,于是开始执行打开文件的操作;用户点击了“保存文件”,于是开始执行保存文件的操作。我们不知道用户究竟想进行什么操作,因此也就不能预测接下来将会调用哪一个函数。如果我们设计了一个“文件另存为”的操作,如果用户不点击,这个操作将永远不会被调用。这就是所谓的“事件驱动”,我们的程序的执行顺序不再是线性的,而是由一个个事件驱动着程序继续执行。没有事件,程序将阻塞在那里,不执行任何代码。
在 Qt 中,事件的概念似乎同信号槽类似。的确如此,一般来说,使用 Qt 组件时,我们并不会把主要精力放在事件上。因为在 Qt 中,我们关心的更多的是事件关联的一个信号。比如,对于QPushButton
的鼠标点击,我们不需要关心这个鼠标点击事件,而是关心它的clicked()
信号的发出。这与其他的一些 GUI 框架不同:在 Swing 中,你所要关心的是JButton
的ActionListener
这个点击事件。由此看出,相比于其他 GUI 框架,Qt 给了我们额外的选择:信号槽。
但是,Qt 中的事件和信号槽却并不是可以相互替代的。信号由具体的对象发出,然后会马上交给由connect()
函数连接的槽进行处理;而对于事件,Qt 使用一个事件队列对所有发出的事件进行维护,当新的事件产生时,会被追加到事件队列的尾部。前一个事件完成后,取出后面的事件进行处理。但是,必要的时候,Qt 的事件也可以不进入事件队列,而是直接处理。信号一旦发出,对应的槽函数一定会被执行。但是,事件则可以使用“事件过滤器”进行过滤,对于有些事件进行额外的处理,另外的事件则不关心。总的来说,如果我们使用组件,我们关心的是信号槽;如果我们自定义组件,我们关心的是事件。因为我们可以通过事件来改变组件的默认操作。比如,如果我们要自定义一个能够响应鼠标事件的EventLabel
,我们就需要重写QLabel
的鼠标事件,做出我们希望的操作,有可能还得在恰当的时候发出一个类似按钮的clicked()
信号(如果我们希望让这个EventLabel
能够被其它组件使用)或者其它的信号。
在前面我们也曾经简单提到,Qt 程序需要在main()
函数创建一个QCoreApplication
对象,然后调用它的exec()
函数。这个函数就是开始 Qt 的事件循环。在执行exec()
函数之后,程序将进入事件循环来监听应用程序的事件。当事件发生时,Qt 将创建一个事件对象。Qt 中所有事件类都继承于QEvent
。在事件对象创建完毕后,Qt 将这个事件对象传递给QObject
的event()
函数。event()
函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(event handler)。关于这一点,我们会在以后的章节中详细说明。
在所有组件的父类QWidget
中,定义了很多事件处理的回调函数,如keyPressEvent()
、keyReleaseEvent()
、mouseDoubleClickEvent()
、mouseMoveEvent()
、mousePressEvent()
、mouseReleaseEvent()
等。这些函数都是 protected virtual 的,也就是说,我们可以在子类中重新实现这些函数。下面来看一个例子:
class EventLabel : public QLabel { protected: void mouseMoveEvent(QMouseEvent *event); void mousePressEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); }; void EventLabel::mouseMoveEvent(QMouseEvent *event) { this->setText(QString("<center><h1>Move: (%1, %2)</h1></center>") .arg(QString::number(event->x()), QString::number(event->y()))); } void EventLabel::mousePressEvent(QMouseEvent *event) { this->setText(QString("<center><h1>Press: (%1, %2)</h1></center>") .arg(QString::number(event->x()), QString::number(event->y()))); } void EventLabel::mouseReleaseEvent(QMouseEvent *event) { QString msg; msg.sprintf("<center><h1>Release: (%d, %d)</h1></center>", event->x(), event->y()); this->setText(msg); } int main(int argc, char *argv[]) { QApplication a(argc, argv); EventLabel *label = new EventLabel; label->setWindowTitle("MouseEvent Demo"); label->resize(300, 200); label->show(); return a.exec(); }
我们编译运行上面的代码,就可以理解到有关事件的使用方法。
EventLabel
继承了QLabel
,覆盖了mousePressEvent()
、mouseMoveEvent()
和MouseReleaseEvent()
三个函数。我们并没有添加什么功能,只是在鼠标按下(press)、鼠标移动(move)和鼠标释放(release)的时候,把当前鼠标的坐标值显示在这个Label
上面。由于QLabel
是支持 HTML 代码的,因此我们直接使用了 HTML 代码来格式化文字。
QString
的arg()
函数可以自动替换掉QString
中出现的占位符。其占位符以 % 开始,后面是占位符的位置,例如 %1,%2 这种。
QString("[%1, %2]").arg(x, y);
语句将会使用 x 替换 %1,y 替换 %2,因此,这个语句生成的QString
为 [x, y]。
在mouseReleaseEvent()
函数中,我们使用了另外一种QString
的构造方法。我们使用类似 C 风格的格式化函数sprintf()
来构造QString
。
运行上面的代码,当我们点击了一下鼠标之后,label 上将显示鼠标当前坐标值。
为什么要点击鼠标之后才能在mouseMoveEvent()
函数中显示鼠标坐标值?这是因为QWidget
中有一个mouseTracking
属性,该属性用于设置是否追踪鼠标。只有鼠标被追踪时,mouseMoveEvent()
才会发出。如果mouseTracking
是 false(默认即是),组件在至少一次鼠标点击之后,才能够被追踪,也就是能够发出mouseMoveEvent()
事件。如果mouseTracking
为 true,则mouseMoveEvent()
直接可以被发出。知道了这一点,我们就可以在main()
函数中直接设置下:
EventLabel *label = new EventLabel; label->setWindowTitle("MouseEvent Demo"); label->resize(300, 200); label->setMouseTracking(true); label->show();
这样子就没有这个问题了。
80 评论
😀 感谢博主无私精神
楼主,不知道你有没有注意到关于keyPressEvent 和 keyReleaseEvent 这两个事件的一个奇怪现象,当按下键盘的时候,它是先调的keyReleaseEvent处理函数,然后再调的keyPressEvent处理函数,不知何解?
我用 debug 输出,是 press 先调用的,不知道你是怎么测试的?
🙁 为什么我测试的结果是按ESC键时是release先调,而按数字键时则是只调press呢?
(我是打断点调的……)
这个我也没有发现类似的情况…
class EventLabel为什么不需要加上 Q_OBJECT?
理论上说,只要继承 QObject 的类都应该添加 Q_OBJECT,但是由于这个 EventLabel 没有使用 Q_OBJECT 所带来的各个函数,例如 tr() 或者信号槽,所以没有添加上。另外一个原因是,如果添加了 Q_OBJECT 宏,再将其写在 cpp 文件中就有些不方便(qmake 不会自动处理 cpp 文件中出现的 Q_OBJECT,只处理 h 文件中的,需要额外增加代码),而这里仅仅是演示性质,将其与 main() 函数放在一个 cpp 文件中,因此也没有增加 Q_OBJECT。
哦,但是我把EventLabel类写在.h文件中,加上Q_OBJECT 编译出错: 错误:LNK1169: 找到一个或多个多重定义的符号
如果只写声明的话应该是没有问题的,不知道你是怎样分文件的呢?
不知道是不是我把那几个事件函数mouseMoveEvent等直接在头文件中实现的原因,我把它们放到cpp中就没事
是的,函数实现不能放在头文件中,否则会报重复定义的错误。
我也是在头文件加Q_OBJECT出现连接错误,E:\qt\event\event\eventlabel.h:4: 错误:undefined reference to `vtable for EventLabel'
不加Q_OBJECT就正常,我头文件是按前面例子的格式只写了声明。
啊,我没有重新qmake,没问题了 😉
先谢谢豆子!另外,这个label指针不用手动delete吗?
理论上是需要的,这里只是代码简单起见,没有去 delete。实际上正如前面说过的,main() 中的 label 应该在栈上创建,而不是堆上面。
豆子,label不是在堆上建立的吗,不是new出来之后的都是在堆上的吗?
是说“应该”在栈上建立
还是不能使用中文的问题。之前仿照书上的写了一个代码,可以显示中文的。(比如状态栏),现在写这个就是显示不出啊。编码也改成一样的了啊、
这代码似乎并没有创建一个窗口,这是不是说,就算没有显式的创建一个窗口,如果只是创建一个组件,并显示组件的话,QT也会自动的为其创建一个窗口呢?
你可以这样理解,不过这其实是操作系统提供的一个窗口。比如 Windows 不允许单独显示一个组件,因此它会为你要显示的组件增加一个窗口以便显示出来(相当于为这个组件添加一个窗口句柄)。
豆子老师,你好!鼠标点击事件重载是基于一个类吧,请问一个窗口中有六个label对象,如何实现如下功能:鼠标点击每个label,每个label显示一张指定的图片,但是每个label显示的图片不同。如果直接对label类的鼠标点击事件重载,无法实现具体对象的指定操作啊。
或者,我想用如下方法,不知可否:每个label声明成一个类,都重载一下鼠标事件,显示指定图片,这样每个对象都对同一个类的鼠标事件进行重载,可行吗?
谢谢!
如果没有理解错误的话,你的需求可以有两种实现:第一,创建一个 QLabel 的子类,这个类中保存它要显示的图片,并重写鼠标事件。第二,使用事件过滤器,判断是哪个 label 接收到事件。个人感觉第一种方案更好,因为如果使用第二种方案,事件过滤器中需要判断是哪个 label,如果以后需要增加更多 label,可能要修改代码,造成维护的不便。
针对这个问题,想请问下如果创建一个子类,那么多个Label应该赢这个类进行实例化吧,怎么样实现每个对象保存相应的图片呢,在子类中怎样实现每个对象的指定图片呢?谢谢!
本章末尾一段里面的“如果mouseTracking是 false(默认即是)”,最后这里应该是 “默认值是” 吗?
原意“默认即是”意思是“默认就是这个值”,所以你的理解应该也没有错误 ;-P
这么快回复,豆子你这是准备随时在线指导吗?
没有没有,只是看到了邮件就回复了下。这次就慢多了 ;-P
豆子老师你好 刚开始学习Qt 。请问 我希望在一个对话框中 左边是QTreeWidget 右边是几个显示屏(显示屏我在想用label 来表示) 实现QTreeWidget中每个item拖动到对应的屏上显示对应的item设置的图片 该如何实现?
这个需要使用拖放技术,你可以看看文档里面的 Drag and drop 这一章。
豆子哥你好,谢谢你的分享。
我现在在尝试Qt的ui,但是只能添加Qlabel这样的widget,那我如果要写一个Qlabel的子类,比如MyLabel,重写其鼠标事件,在Qt的ui里面能不能加入自定义的MyLabel呢?
谢谢豆子哥!
如果使用 UI 文件设计,可以先使用一个 QLabel 作为占位符,然后在上面点击右键有个“提升为...”,可以选择自己的子类(话说我也不知道为什么是这么个翻译,但就是这个用处)。
QTextCursor::setPosition: Position '17' out of range
求问这个信息总是反反复复的出现怎么办QWQ
看样子是超出范围了。看看光标位置是不是正确?
同样遇到这个问题, 在刷新QListWidget的内容时, 不断地出现这个信息.
有没有解决过的朋友帮助一下看看是什么原因导致的
豆子老师 ,我的EventLabel是写在头文件里的,不知道问什么会出现这样的错误:
mainwindow.obj:-1: error: LNK2005: "protected: virtual void __cdecl EventLabel::mouseMoveEvent(class QMouseEvent *)" (?mouseMoveEvent@EventLabel@@MEAAXPEAVQMouseEvent@@@Z)
我弄错了 不好意思啊
豆子你好,我将类的mouseMoveEvent函数改为MouseMoveEvent(后面两个函数亦是),为什么就无法显示了呢?虽然mouseMoveEvent是QLabel的虚函数,但是改了后的MouseMoveEvent不能看做子类自己定义的函数吗?后面也有定义该函数的操作,为什么就不能显示呢?谢谢豆子。
这些虚函数是由系统调用的,当你改了名字,系统依旧按照 mouseMoveEvent() 调用,根本不知道你有 MouseMoveEvent() 这个函数,所以不会起作用。
豆子你好,感觉QT这种,处理事件用继承的方式有点麻烦啊;每个事件选配一个 function进行回调,或每个事件触发一个信号应该回方便很多吧。你觉得呢?
应该是两种设计吧,毕竟 Qt 也提供了信号槽模式。或许有效率上面的考虑。
请问博主,Qt能不能处理Macbook 的触控板多点触控的事件来完成比如利用触控板缩放图片这样的应用?
楼主,为啥出现了setText不是类EventLevel函数的错误?setText不是库里面的函数吗?
自己解决了,请忽视
豆子老师你好,我想问一下鼠标的release事件的实现为什么和另外两个不一样呢?我把它改成和前两个一样,发现释放鼠标没有release显示,这个函数好像没有调用到,这是为什么呢?
不一样的只是用了不同的字符串拼接函数,这个并没有影响。如果因为修改之后变得没有调用,可能是因为别的什么问题,仔细检查下函数名字之类是不是一致?
谢谢老师,我犯了一个弱智的错误,我直接从Press那个函数体里把代码复制过去忘了改了,改了一下就好了。不过还是要谢谢老师^-^
豆子老师你好,我想问一下为什么只用EventLabel *label = new EventLabel; 就把三个虚函数都调用了? 我是c++初学者.... 只知道class里的函数用object.function 或 pointer->function来调用.... 而我试着创建了一个object 再试object.mouseMoveEvent(QMouseEvent *event) 反而说*event未declare ...
调用三个函数是因为这些函数分别对应着不同的操作,包括鼠标按键按下、鼠标按键释放等。这里并不需要强调虚函数(虽然它们的确就是虚函数)。event 找不到是因为没有 include 合适的头文件,与如何调用没有关系。
多谢回复,可是EventLabel里的三个函数究竟是怎么被调用的呢?main class里没有对这三个函数的调用啊?不太明白...
这三个函数并不是有我们调用的,而是系统调用的(这样不由我们调用的函数被称为回调函数)。Qt 在检测到对应的事件时,会自动调用这些函数。我们要做的就是重写这些函数,以便实现我们需要的操作。因为这些函数都是虚函数,因此,Qt 在调用父类版本时,我们的具体实现会被执行。这相当于一个占位符,绝大多数框架都依赖于类似的机制实现。
多谢豆子老师 终于弄明白了
豆子老师,有个问题不明白。
EventLabel 中的mouseMoveEvent, mousePressEvent, mouseReleaseEvent 这三个函数都是从QLabel中继承而来的虚函数,而且也都给予了新的实现。但是这三个函数的新实现从代码上来看是一样的的啊。那为什么这三个函数能够分别体现移动,按压,释放的鼠标位置呢?c++中虚函数在继承类中被重新定义,不是应该覆盖原有的实现吗?
这种类似于模板函数设计模式。这三个函数其实是另外一个判断的一部分,而不是全部,例如:
类似这样的实现可以看出,对鼠标动作的判断其实是在外部的,你的实现只是覆盖了具体的动作。
豆子你好!非常谢谢,你的《Qt学习之路》这个教程很适合我这样的初学者...最近在看事件这一章的时候,自己动手试了试...出现了一些问题,部分源码如下:
void mouseMoveEvent(QMouseEvent *event);
void mousePressEvent(QMouseEvent *event);
void enterEvent(QEvent *event);
void leaveEvent(QEvent *event);
void enterLabel::enterEvent(QEvent *event){
//QMessageBox::information(this,tr("enterEvent"),tr("you enter"));
emit enter();
}
void enterLabel::leaveEvent(QEvent *event){
//QMessageBox::warning(this ,tr("leaveEvent"),tr("you leave"));
emit leave();
}
enter和leave事件在里面直接调用QMessageBox总是出现错误,即便将他换成两个信号来间接调用也存在一些问题,就是不能够同时发送这两个信号...调用槽函数
SuspendThread (tid=0x1b68) failed. (winerr 2)
SuspendThread (tid=0xd48) failed. (winerr 2)
SuspendThread (tid=0x1b68) failed. (winerr 2)
SuspendThread (tid=0x2704) failed. (winerr 2)
这个是其他的一些问题
这个没看出来是怎么回事,方便的话可以发一下代码到邮箱
文章中提及:“总的来说,如果我们使用组件,我们关心的是信号槽;如果我们自定义组件,我们关心的是事件。”
个人感觉应该是:如果对某类组件的事件感兴趣,应该继承之自定义,重载事件相关函数捕获感兴趣的事件。
因为事件的函数都是 protected 的,所以你必须继承才能使用。
1 自定义组件的出发点并非仅仅是为了获取感兴趣的事件,而是如果你需要自己处理感兴趣的事件必须继承重载之。
2 还有函数是protected没错,然而事件处理函数的调用是由event()内部自动调用,我们重载的目的是添加我们感兴趣的处理。
3 说到protected也仅仅是起到保护作用且能够被子类继承并重载,我仅仅是说你的有歧义,谁说我们重载就一定关心事件呢,只是给我们了一种可以处理自己感兴趣事件的“机会”。
使用的默认构造函数,但构造函数中并没有提用调用三个event事件函数啊。为啥程序一跑起来三个 三个函数都能运行了
这是 Qt 通过虚函数机制调用的,具体是 Qt 会在自己的库函数中调用父类的函数,而这个函数是虚函数,因此会使用子类的实现
豆子哥讲的真棒,比书籍上的更有实用价值
豆子老师,有这个完整代码吗,刚学习头文件添加的不对,编译不出来,能给我发一下吗,
C:\Users\user\Documents\Qt\test\eventlabel.cpp:6: error: invalid use of incomplete type 'class QMouseEvent'
.arg(QString::number(event->x()), QString::number(event->y())));
^
请问下这个什么意思啊,
有可能是代码写错了,给出的错误信息看不出来是哪里的问题
遇到一样的问题,报错是非法使用不完整类QMouseEvent, 加上#include 就好了
我也是,。。。。头文件不对。。。也不知道调用哪些
引入QMouseEvent
豆子老师,我想请教一个Qt的安卓开发问题,就是我要用两个手指同时操作两个控件,请问这个要怎么办呢,为什么每次只能选中一个控件。
豆哥,想问一下您如果想要设置坐标原点在左下角怎么设置,您所演示的这个坐标原点位于左上角,这是默认设置的吗
Qt 默认坐标原点在左上角。如果是 Graphics View Framework 里面,可以使用 translate() 函数。不过对于普通控件好像没有办法移动原点。
关于前面所说的键盘事件可以看一下这个博客https://blog.csdn.net/wangqing_12345/article/details/50980127
豆子哥,你好!我在Qt5.7.1(ubuntu16.04)上运行你的代码,编译报错: error: invalid use of incomplete type ‘class QMouseEvent’ .arg(QString::number(event->x()), QString::number(event->y())));,但是将event->x()和event->y()换成固定值时就可以,自己也百度了好几个解决方法还是不对,想请教一下这是怎么回事?
已解决!原来是缺少了头文件!谢谢!
豆子老师你好,我用的是Qt5.9.2,运行上面代码,不设置这一行label->setMouseTracking(true);
鼠标在move时也可以显示坐标
明白了,setMouseTracking(true)不用第一次点击鼠标就可以显示
这个教程太棒了,已捐助
感谢
main.cpp里提示 ”unknown type name 'EventLable',这个错误来怎么处理呢?有知道的么?
看起来像是没有找到这个类 EventLable
盲猜没include头文件:p