鼠标事件
- 0 事件
- 1 需求
- 2 查看控件的事件处理函数
- 3 UI设计
- 4 新建一个类,继承QLabel
- 5 对已有对象进行类型提升
- 6 重写事件处理函数
- 7 项目进一步拓展
- (1)获取鼠标按键
- (2)鼠标移动
- (3)显示多个按键
- (4)设置默认鼠标跟踪
- 8 QT事件分发机制
0 事件
用户对界面中的控件进行操作后,控件需要进行响应,响应一般有两种方式,一种是之前学过的槽函数,另一种是事件,部分操作无法通过槽函数响应,只能通过事件,例如鼠标移动,键盘输入等。
当某个事件(鼠标、键盘)发生的时候,相关控件就会收到这个事件,并且调用相应的事件处理函数进行相应,事件处理函数的命名都是以Event结尾的。
1 需求
在窗口内的任意位置点击,然后窗口会有一个标签(使用QLabel)显示鼠标所在位置的坐标:随着鼠标在窗口内不同位置进行点击,标签显示的坐标也在变换。效果如下图所示:
2 查看控件的事件处理函数
在帮助界面搜索QWidget,然后点击Protected Functions
可以看到很多以“Event”结尾的函数,这些函数的参数都是各个事件,并且这些函数都使用了virtual修饰,即他们都是虚函数,继承时表现为多态特性。
我们可以往下滑,找到鼠标事件
截图中红色方框框出来的几个分别为:鼠标单击、鼠标移动、鼠标点击、鼠标释放,说明这个控件可以对这四种鼠标事件进行响应,它们的参数均为鼠标事件指针(QMouseEvent *)。
3 UI设计
打开QT设计师,在Display Widgets中选择Label,将其拖到窗口中:
给主窗口来个布局
右下角可以看到类的继承层次结构,我们刚刚拖到窗口中的控件,类型是QLabel
在QFrame的frameShape属性中选择Box
窗口中会出现黑色的框线
4 新建一个类,继承QLabel
设计好UI后,之所以还要新建类去继承已有类,是因为原先的事件处理函数无法满足需求,我们需要重写相应的事件处理函数(这部分稍后会说)。
由于新建类的时候,父类选项没有QLabel,所以这里新建类的时候先选择继承QWidget(也可以选其他),产生代码后手动改父类
打开新建类的头文件,将父类由QWidget改成QLabel
对应的cpp文件也要改,在构造函数的初始化列表中,将父类改过来
5 对已有对象进行类型提升
在UI设计界面的对象树中,右击我们刚刚在UI设计阶段,拖到窗口中的空间(QLabel对象),然后点击“提升为”
在弹出的对话框中,填写提升的类名称,然后点击添加
再次填写提升的类名称,然后点击提升
此时可以看到,我们刚刚拖到窗口中的控件,类型变成了MyLabel
然后Ctrl+R,看看有没有错。
6 重写事件处理函数
MyLabel.h内容如下:
#ifndef MYLABEL_H
#define MYLABEL_H
#include <QWidget>
#include <QLabel>
class MyLabel : public QLabel
{
Q_OBJECT
public:
explicit MyLabel(QWidget *parent = nullptr);
signals:
public:
void mousePressEvent(QMouseEvent *event);
};
#endif // MYLABEL_H
输入完函数声明后,按下alt+回车,会显示提示消息,然后选择“在mylabel.cpp添加定义”,就能在mylabel.cpp中自动生成函数头,十分方便。
MyLabel.cpp内容如下:
#include "mylabel.h"
#include "QMouseEvent"
MyLabel::MyLabel(QWidget *parent) : QLabel(parent)
{
}
void MyLabel::mousePressEvent(QMouseEvent *event)
{
//获取鼠标事件的一些信息
//获取坐标
int x = event->x();
int y = event->y();
//格式化显示坐标,并居中
QString str = QString("<h1><center>[%1, %2]</center></h1>").arg(x).arg(y);
this->setText(str);
}
这里QString str = QString("<h1><center>[%1, %2]</center></h1>").arg(x).arg(y);
是将字符串进行格式化匹配,QString中是可以使用html语法的,随后的this->setText(str)
是输出指定的字符串。
现在我们按下Ctrl+R运行,然后点击窗口中的任意一点,然后就能看到坐标了
至此,大功告成。
我们这个项目的结构如下图所示:
项目中的main.cpp、widget.h和widget.cpp都没有修改,这几个文件一般很少去动,除非要和后端进行联动。
7 项目进一步拓展
(1)获取鼠标按键
在mylabel.cpp中输入event->button();
,然后移动光标到函数上,按两遍F1(笔记本电脑可能和某些屏幕、声音设置的快捷键冲突,这种情况下需要按住键盘左下角的fn键,然后按两遍F1)
可以看到这个函数的返回值类型是Buttons,点击QT::MouseButton
QT 5.14.2的帮助文档中,button()返回值是Qt3DInput::QMouseEvent::Buttons
类型,而用这个类来定义event->button的返回值却报错,我不知道怎么回事,所以上面使用的是QT 5.9文档内容。也就是说,5.14.2并不是稳定版本,编译器和帮助文档有冲突,我们以后查看返回值类型的时候,用ctrl,然后鼠标点击要查看的函数,找到源码查看返回值类型,然后再调用帮助文档。
可以看到QT::MouseButton
枚举类型(一般Qt后面跟两个冒号的,都是枚举类型),红框框出来的为需要掌握的键,分别是:未按下、任意键、左键、右键、中键(滚轮)、中键,最后两个含义相同。
将mylabel.cpp的代码修改如下:
#include "mylabel.h"
#include "QMouseEvent"
MyLabel::MyLabel(QWidget *parent) : QLabel(parent)
{
}
void MyLabel::mousePressEvent(QMouseEvent *event)
{
//获取鼠标事件的一些信息
//获取坐标
int x = event->x();
int y = event->y();
//获取鼠标按键
Qt::MouseButtons btns = event->buttons();
QString strButton ="" ;
if(btns & Qt::LeftButton)
{
strButton += "LeftButton;";
}
if(btns & Qt::RightButton)
{
strButton += "RightButton;";
}
if(btns & Qt::MidButton)
{
strButton += "MidButton";
}
//格式化显示坐标,并居中
QString str = QString("<h1><center>[%1, %2][%3]</center></h1>").
arg(x).arg(y).arg(strButton);
this->setText(str);
}
运行结果如下:
(2)鼠标移动
在MyLabel类中新增一个函数,定义如下(mylabel.h中也要声明,这里不再赘述):
void MyLabel::mouseMoveEvent(QMouseEvent *ev)
{
//获取坐标
int x = ev->x();
int y = ev->y();
QString str=QString("<h1><center>Move[%1, %2][%3]</center></h1>").arg(x).arg(y);
this->setText(str);
}
点击运行后,按下鼠标,会调用mousePressEvent,移动鼠标会调用mouseMoveEvent
(3)显示多个按键
对于按下按键后(不松开)拖动的情况,会先调用mousePressEvent,然后调用mouseMoveEvent,我们希望标签能显示Move和坐标的同时,也显示按下了哪个按钮,例如Move[300, 300][LeftButton]
,对于鼠标移动过程中,同时按下左键和右键的情况,我们也希望能够检测到。则可以对mouseMoveEvent做如下修改:
void MyLabel::mouseMoveEvent(QMouseEvent *ev)
{
//获取坐标
int x = ev->x();
int y = ev->y();
//获取鼠标按键
Qt::MouseButtons btns = ev->buttons(); //可以一次获取多个按键
QString strButton ="" ;
if(btns & Qt::LeftButton) //按位与操作
{
strButton += "LeftButton "; //+是字符串拼接,因为有可能按了多个键
}
if(btns & Qt::RightButton)
{
strButton += "RightButton "; //+是字符串拼接,因为有可能按了多个键
}
if(btns & Qt::MidButton)
{
strButton += "MidButton ";
}
QString str=QString("<h1><center>Move[%1, %2][%3]</center></h1>").
arg(x).arg(y).arg(strButton);
this->setText(str);
}
这里需要注意的是,ev->buttons()返回一个数,我们用二进制的角度来看,假如它返回的是000101,那么它是000100(MidButton)和000001(LeftButton)按位与的结果,表示同时按下了两个按键。
后面的几个if判断条件,其实是按位与运算。因为每一种Qt::MouseButton常量,只有某一位是1,其他位都为0,因此要从ev->buttons()的返回值中将某一位抽取出来,可以使用按位与运算,例如要判断是否按下了RightButton(000010),可以用RightButton与返回值进行按位与运算,结果为0则表示没按下,非0则表示按下了,假设返回值为000011,000010&000011结果是000010,结果非0,因此按下了。
运行后,按下鼠标拖动如下:
在按下鼠标(左键)拖动的那一瞬间,是否调用了mousePressEvent?可以在mousePressEvent函数中增加一行打印功能,来查看是否调用。
我自己尝试了,点下的那一瞬间,确实是先调用mousePressEvent,然后调用mouseMoveEvent,即点下的一瞬间先相应mousePressEvent,随后移动鼠标时相应mouseMoveEvent,此时由于鼠标左键还没松开,因此能被ev->buttons()检测到。
对于同时按下多个按键进行拖动,也能正常显示
(4)设置默认鼠标跟踪
上面的程序,在开始的时候,没有跟踪鼠标,也就是下面这个样子:
只有点击了窗口以后,才会显示“Move+坐标+按键名称”
如果我们想在窗口打开后,鼠标移动到窗口中(未点击)就开始跟踪鼠标的移动,那么可以在构造函数中进行设置
MyLabel::MyLabel(QWidget *parent) : QLabel(parent)
{
//默认情况下,窗口不会主动跟踪鼠标
//只有当某个鼠标按键按下的情况下才开始跟踪
//如果想一开始跟踪,就要使用以下函数
this->setMouseTracking(true);
}
8 QT事件分发机制
为什么我们点击了鼠标,就会调用mousePressEvent,移动了鼠标就会调用mouseMoveEvent?
当某个事件(鼠标、键盘)发生的时候,相应的控件就会收到这个事件,并且根据事件的类型调用相应的事件处理函数,这个过程都在一个名为event的事件中,这个函数可以在帮助文档中查看,它是一个虚函数,可以被子类重写。
一般情况下,不会去重写这个函数,我们能这里为了了解事件分发机制,因此对其进行重写。
这里仅仅以鼠标移动为例
bool MyLabel::event(QEvent *e)
{
//返回值:true表示该事件得到处理,如果是false,没被处理,事件会继续传递到父窗口
//QEvent就是所有Event类的父类,这里其实是使用了多态
//判断event的类型
if(e->type()==QEvent::MouseMove) //e->type()返回的是鼠标事件的类型
{
this->mouseMoveEvent(static_cast<QMouseEvent *>(e)); //这句如果注释掉,则鼠标移动将不会有反应
return true;
}
//其他类型的事件,交给父类的event函数去处理
return QLabel::event(e); //父类的event函数因为被覆盖了,所以需要域作用符
}
如果将this->mouseMoveEvent(static_cast<QMouseEvent *>(e));
注释掉,则当鼠标移动时将不会有反应,此时就把鼠标移动给拦截过滤了。
事件的机制,可以用下面这张图来总结,事件过滤器这里没讲,这个并不重要: