开始学习“事件”!
Qt 学习之路 2(18):事件
Qt 中的事件和信号槽却并不是可以相互替代的。信号由具体的对象发出,然后会马上交给由connect()函数连接的槽进行处理;而对于事件,Qt 使用一个事件队列对所有发出的事件进行维护,当新的事件产生时,会被追加到事件队列的尾部。前一个事件完成后,取出后面的事件进行处理。但是,必要的时候,Qt 的事件也可以不进入事件队列,而是直接处理。信号一旦发出,对应的槽函数一定会被执行。但是,事件则可以使用“事件过滤器”进行过滤,对于有些事件进行额外的处理,另外的事件则不关心。总的来说,如果我们使用组件,我们关心的是信号槽;如果我们自定义组件,我们关心的是事件。因为我们可以通过事件来改变组件的默认操作。比如,如果我们要自定义一个能够响应鼠标事件的EventLabel,我们就需要重写QLabel的鼠标事件,做出我们希望的操作,有可能还得在恰当的时候发出一个类似按钮的clicked()信号(如果我们希望让这个EventLabel能够被其它组件使用)或者其它的信号。
在所有组件的父类QWidget中,定义了很多事件处理的回调函数,如keyPressEvent()、keyReleaseEvent()、mouseDoubleClickEvent()、mouseMoveEvent()、mousePressEvent()、mouseReleaseEvent()等。这些函数都是 protected virtual 的,也就是说,我们可以在子类中重新实现这些函数。
来看例子:
/*EventLabel继承了QLabel,覆盖了mousePressEvent()、mouseMoveEvent()和MouseReleaseEvent()
三个函数。代码功能:在鼠标按下(press)、鼠标移动(move)和鼠标释放(release)的时候,
把当前鼠标的坐标值显示在这个Label上面。由于QLabel是支持 HTML 代码的,因此直接使用了HTML
代码来格式化文字。*/
class EventLabel : public QLabel
{
protected:
void mouseMoveEvent(QMouseEvent *event);
void mousePressEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
};
void EventLabel::mouseMoveEvent(QMouseEvent *event)
{
/*QString的arg()函数可以自动替换掉QString中出现的占位符。占位符以 % 开始,后面是
占位符的位置,例如 %1,%2。*/
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())));
}
/*在mouseReleaseEvent()函数中,我们使用了另外一种QString的构造方法,用类似C风格的格式
化函数sprintf()来构造QString。*/
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);
/*为什么要点击鼠标之后才能在mouseMoveEvent()函数中显示鼠标坐标值?因为QWidget中有
一个mouseTracking属性,该属性用于设置是否追踪鼠标。只有鼠标被追踪时,mouseMoveEvent()
才会发出。如果mouseTracking是 false(默认值),组件在至少一次鼠标点击之后,才能够
被追踪,也就是能够发出mouseMoveEvent()事件。如果mouseTracking为 true,则mouseMoveEvent()
直接可以被发出。*/
label->setMouseTracking(true);
label->show();
return a.exec();
}
Qt 学习之路 2(19):事件的接受与忽略中总结几点:
1
当重写事件回调函数时,时刻注意是否需要通过调用父类的同名函数来确保原有实现仍能进行!
2
Qt 的事件对象有两个函数:accept()和ignore()。前者用来告诉 Qt,这个类的事件处理函数想要处理这个事件;后者则告诉 Qt,这个类的事件处理函数不想要处理这个事件。在事件处理函数中,可以使用isAccepted()来查询这个事件是不是已经被接收了。具体来说:如果一个事件处理函数调用了一个事件对象的accept()函数,这个事件就不会被继续传播给其父组件;如果它调用了事件的ignore()函数,Qt 会从其父组件中寻找另外的接受者。
3
如果我们在子类中直接忽略事件,Qt 会去寻找其他的接收者,该子类的父类的操作会被忽略(因为没有调用父类的同名函数),这可能会有潜在的危险。为了避免自己去调用accept()和ignore()函数,而是尽量调用父类实现,Qt 做了特殊的设计:事件对象默认是 accept 的,而作为所有组件的父类QWidget的默认实现则是调用ignore()。这么一来,如果你自己实现事件处理函数,不调用QWidget的默认实现,你就等于是接受了事件;如果你要忽略事件,只需调用QWidget的默认实现。
4
事件的传播是在组件层次上面的,而不是依靠类继承机制。
关于event()函数:
Qt 学习之路 2(20):event()
事件对象创建完毕后,Qt 将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler)。
event()是一个集中处理不同类型的事件的地方。如果你不想重写一大堆事件处理器,就可以重写这个event()函数,通过QEvent::type()判断不同的事件。鉴于重写event()函数需要十分小心注意父类的同名函数的调用,一不留神就可能出现问题,所以一般还是建议只重写事件处理器(当然,也必须记得是不是应该调用父类的同名处理器)。这其实暗示了event()函数的另外一个作用:屏蔽掉某些不需要的事件处理器。
Q&A:
1.这个label指针不用手动delete吗?
理论上是需要的,这里只是代码简单起见,没有去 delete。实际上正如前面说过的,main() 中的 label 应该在栈上创建,而不是堆上面。
2.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中就没事。
是的,函数实现不能放在头文件中,否则会报重复定义的错误。
3.为什么只用EventLabel *label = new EventLabel; 就把三个虚函数都调用了?
这三个函数并不是有我们调用的,而是系统调用的(这样不由我们调用的函数被称为回调函数)。Qt 在检测到对应的事件时,会自动调用这些函数。我们要做的就是重写这些函数,以便实现我们需要的操作。因为这些函数都是虚函数,因此,Qt 在调用父类版本时,我们的具体实现会被执行。这相当于一个占位符,绝大多数框架都依赖于类似的机制实现。
4.对于槽onButtonClicked,只能接收鼠标左键发出的信号还是左键右键的信号都能接收?
信号只有左键单击时才会触发。这一点可以用代码测试得出,也可以查看 Qt 源代码。你疑问的原因是,把信号同 mousePressEvent() 混为一谈了。信号是左键单击时发出,事件是左右键都会监听到。查看源代码即可得知,Qt 在监听到左键单击时才会发出 clicked() 信号。
5.[*] 这种语法?
[*] 是 Qt 提供的一种占位符,不是新的语法。底层上应该是 Qt 查询字符串进行替换,没有什么特别之处。
6.“父组件”和“父类”有什么联系和区别?
父组件是这个组件所在的组件,比如一个 QDialog 里面添加了一个 QLabel,那么,QDialog 就是 QLabel 的父组件。父类是从类设计的角度来说的,比如 QLabel 继承自 QWidget,那么,QWidget 就是 QLabel 的父类。
一般组件的构造函数里都会有个一个 parent指针, 指的其实就是这个组件的父组件吧?
一般是的,不过这个 parent 的主要作用还在于在 parent 析构时,它的 children 都会自动析构,也就是一种半自动化的内存管理。所以,也有些组件会把 parent 指针设置为 MainWindow,这不是直接父组件,但就内存管理而言也是合理的。