Qt事件系统一:Qt中的事件处理与传递
一、简介
在 Qt 中,事件作为一个对象,继承自 QEvent 类,常见的有键盘事件 QKeyEvent、鼠标事件 QMouseEvent 和定时器事件 QTimerEvent 等,与 QEvent 类的继承关系图如下所示。本章会详细讲解这 3 个常见的事件,还会涉及事件过滤器和自定义事件的知识。关于本章的相关内容,可以在 Qt 帮助中通过 The Event System 关键字查看。
二、Qt中的事件
事件是对各种应用程序需要知道的由应用程序内部或者外部产生的事情或者动作的通称。Qt 中使用一个对象来表示一个事件,继承自 QEvent 类。
需要说明的是,事件与信号并不相同,比如单击一下界面上的按钮,那么就会产生鼠标事件 QMouseEvent (不是按钮产生的 ),而因为按钮被按下了 ,所以它会发出 clicked() 单击信号(是按钮产生的)。这里一般只关心按钮的单击信号,而不用考虑鼠标事件,但是如果要设计一个按钮,或者当单击按钮时让它产生别的效果,那么就要关心鼠标事件了。可以看到,事件与信号是两个不同层面的东西,发出者不同,作用也不同。在 Qt 中,任何 QObject 子类实例都可以接收和处理事件。
2.1 事件的处理
一个事件由一个特定的 QEvent 子类来表示,但是有时一个事件又包含多个事件类型,比如鼠标事件又可以分为鼠标按下、双击和移动等多种操作。这些事件类型都由 QEvent 类的枚举型 QEvent::Type 来表示,其中包含了 一百多种事件类型,可以在 QEvent 类的帮助文档中查看。虽然 QEvent 的子类可以表示一个事件,但是却不能用来处理事件,那么应该怎样来处理一个事件呢?在 QCoreApplication 类的 notify() 函数的帮助文档处给出了 5 种处理事件的方法:
-
方法一:重新实现部件的 paintEvent()、mousePressEvent() 等事件处理函数。这是最常用的一种方法,不过它只能用来处理特定部件的特定事件。
-
方法二:重新实现 notify() 函数。这个函数功能强大,提供了完全的控制,可以在事件过滤器得到事件之前就获得它们。但是,它一次只能处理一个事件。
-
方法三:向 QApplication 对象上安装事件过滤器。因为一个程序只有一个 QApplication 对象,所以这样实现的功能与使用 notify() 函数是相同的,优点是可以同时处理多个事件。
-
方法四:重新实现 event() 函数。QObject 类的 event() 函数可以在事件到达默认的事件处理函数之前获得该事件。
-
方法五:在对象上安装事件过滤器。使用事件过滤器可以在一个界面类中同时处理不同子部件的不同事件。
在实际编程中,最常用的是方法一,其次是方法五。因为方法二需要继承自 QApplication 类;而方法三要使用一个全局的事件过滤器,这将减缓事件的传递,所以,虽然这两种方法功能很强大,但是却很少被用到。
2.2 事件的传递
在每个程序的 main() 函数的最后都会调用 QApplication 类的 exec() 函数,它会使 Qt 应用程序进人事件循环,这样就可以使应用程序在运行时接收发生的各种事件。一旦有事件发生,Qt 便会构建一个相应的 QEvent 子类的对象来表示,然后将它传递给相应的 QObject 对象或其子对象。下面通过例子来看一下 Qt 中的事件传递过程。
新建 Qt Gui 应用,项目名称为 myEvent,基类选择 QWidget,然后类名保持 Widget 不变。建立完成后向项目中添加新文件,模板选择 C++ 类,类名为 MyLineEdit,基类手动填写为 QLineEdit,自定义了一个 MyLineEdit 类。
mylineEdit. h 文件:
#ifndef MYLINEEDIT_H
#define MYLINEEDIT_H
#include <QLineEdit>
class MyLineEdit : public QLineEdit
{
Q_OBJECT
public:
explicit MyLineEdit(QWidget *parent = nullptr);
// event()函数获取事件的类型
bool event(QEvent *event);
protected:
// MyLineEdit类的键盘按下事件
void keyPressEvent(QKeyEvent *event);
};
#endif // MYLINEEDIT_H
这里添加了 keyPressEvent() 函数和 event() 函数的声明。
mylineEdit. cpp 文件:
#include "mylineedit.h"
#include <QKeyEvent>
#include <QDebug>
MyLineEdit::MyLineEdit(QWidget *parent) :
QLineEdit(parent)
{
}
// MyLineEdit类的键盘按下事件
void MyLineEdit::keyPressEvent(QKeyEvent *event)
{
qDebug() << tr("MyLineEdit键盘按下事件");
// 让MyLineEdit输入栏能输入字符
QLineEdit::keyPressEvent(event); // 执行QLineEdit类的默认事件处理
event->ignore(); // 忽略该事件
}
//event()函数获取事件的类型
bool MyLineEdit::event(QEvent *event)
{
// 判断触发事件类型是否为键盘按下事件
if(event->type() == QEvent::KeyPress)
qDebug() << tr("MyLineEdit的event()函数");
return QLineEdit::event(event); // 执行QLineEdit类event()函数的默认操作
}
这里自定义了一个 MyLineEdit 类,它继承自 QWidget,并且实现了 MyLineEdit 类的 keyPressEvent() 函数和 event() 函数。event() 函数中使用了 event->type() 来获取事件的类型。如果是键盘按下事件 QEvent::KeyPress,则输出信息,另外返回父类的 event() 函数的操作结果。
widget.h 文件:
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
class MyLineEdit;
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = 0);
~Widget();
// Widget类的事件过滤器
bool eventFilter(QObject *obj, QEvent *event);
private:
Ui::Widget *ui;
MyLineEdit *lineEdit;
protected:
// Widget类的键盘按下事件
void keyPressEvent(QKeyEvent *event);
};
#endif // WIDGET_H
这里也添加了keyPressEvent()函数的声明。
widget.cpp 文件:
#include "widget.h"
#include "ui_widget.h"
#include "mylineedit.h"
#include <QKeyEvent>
#include <QDebug>
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
lineEdit = new MyLineEdit(this);
lineEdit->move(100, 100);
}
Widget::~Widget()
{
delete ui;
}
// Widget类的键盘按下事件
void Widget::keyPressEvent(QKeyEvent *event)
{
qDebug() << tr("Widget键盘按下事件");
}
// Widget类的事件过滤器
bool Widget::eventFilter(QObject *obj, QEvent *event) // 事件过滤器
{
// 如果是lineEdit部件上的事件
if(obj == lineEdit)
{
if(event->type() == QEvent::KeyPress)
qDebug() << tr("Widget的事件过滤器");
}
return QWidget::eventFilter(obj, event);
}
这里也实现了 Widget 类的 keyPressEvent() 函数,并且会调用 MyLineEdit 类的 keyPressEvent() 函数。在事件过滤器中,先判断该事件的对象是不是 lineEdit,如果是,再判断事件类型,最后返回 QWidget 类默认的事件过滤器的执行结果。
运行程序,然后按下键盘的任意键,比如这里按下 a 键,执行结果如下图所示。
可以看到,事件的传递顺序是这样的:先是事件过滤器,然后是焦点部件的 event() 函数,最后是焦点部件的事件处理函数,例如这里的键盘按下事件函数;如果焦点部件忽略了该事件,那么会执行父部件的事件处理函数,如上图所示。注意,event() 函数和事件处理函数,是在该部件内进行重新定义的,而事件过滤器却是在该部件的父部件中进行定义的。
Qt事件系统二:事件过滤器和事件的发送
Qt提供了事件过滤器来实现在一个部件中监控其他多个部件的事件。事件过滤器与其他部件不同,它不是一个类,只是由两个函数组成的一种操作,用来完成一个部件对其他部件的事件的监视。这两个函数分别是 installEventFilter() 和 eventFilter(),都是QObject类中的函数。下面通过具体的例子来讲解。
新建Qt Gui应用,将项目名称更改为myEventFilter,基类选择QWidget,类名保持Widget不变。完成后在设计模式中向界面上拖入一个QTextEdit和一个QSpinBox。在widget.h文件中添加函数声明:
//事件过滤器
bool eventFilter(QObject *obj, QEvent *event);
在构造函数中添加代码:
//为textEdit和spinBox在本窗口上安装事件过滤器
ui->textEdit->installEventFilter(this);
ui->spinBox->installEventFilter(this);
要对一个部件使用事件过滤器,那么就要先使用installEventFilter() 函数为该部件安装事件过滤器,这个函数的参数表明了监视对象。比如这里就是为textEdit部件和spinBox部件安装了事件过滤器,其参数this表明要在本部件即Widget中监视textEdit和spinBox的事件。这样,就需要重新实现Widget类的eventFilter()函数,在其中截获并处理两个子部件的事件。
//事件过滤器
bool Widget::eventFilter(QObject *obj, QEvent *event)
{
if (obj == ui->textEdit)
{
if (event->type() == QEvent::Wheel)
{
// 将event强制转换为发生的事件的类型
QWheelEvent *wheelEvent = static_cast<QWheelEvent*>(event);
// 缩放textEdit
if (wheelEvent->delta() > 0)
ui->textEdit->zoomIn();
else
ui->textEdit->zoomOut();
}
}
else if (obj == ui->spinBox)
{
if (event->type() == QEvent::KeyPress)
{
QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Space)
ui->spinBox->setValue(0);
}
}
return QWidget::eventFilter(obj, event);
}
在这个事件过滤器中先判断部件的类型,然后再判断事件的类型,如果是我们需要的事件就将其进行强制类型转换,然后进行相应的处理。这里需要说明,如果要对一个 特定的事件进行处理,而且不希望它在后面的传递过程中再被处理,那么就返回true, 否则返回false。这个函数中实现了在textEdit部件中使用滚轮进行内容的放大或缩小,在spinBox部件中使用空格来使数值设置为0。
可以看到,使用事件过滤器可以很容易地处理多个部件的多个事件,如果不使用它,我们就得分别子类化各个部件,然后重新实现它们对应的各个事件处理函数,那样就会很麻烦了。
Qt也提供发送一个事件的功能,由 QCoreApplication 类的 sendEvent() 函数或者 postEvent() 函数来实现。这两个函数的主要区别是:sendEvent() 会立即处理给定的事件,而 postEvem() 则会将事件放到等待调度队列中,当下一次Qt的主事件循环运行时才会处理它。
这两个函数还有其他一些区别,比如 sendEvent() 中的QEvent对象参数在事件发送完成后无法自动删除,所以需要在栈上创建QEvent对象;而 postEvent() 中的QEvent对象参数必须在堆上进行创建(例如使用new),当事件被发送后事件队列会自动删除它。
下面在widget.cpp文件中的构造函数里添加代码来向spinBox部件发送一个向上方向键被按下的事件:
//向spinBox发送一个向上按钮触发键盘事件,所以可以看到最开始spinBox的值不是0,而是1
QKeyEvent myEvent(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier);
qApp->sendEvent(ui->spinBox, &myEvent); //qApp是Application对象的全局指针
这里使用了 sendEvent() 函数,其中 QKeyEvent 类对象是在栈上创建的。其中的qApp是Q Application对象的全局指针,每一个应用程序中只能使用一个QApplication 对象,这里等价于使用 QApplication:: sendEvent()。 现在运行程序可以发现 spinBox部件中初始值变为了1,这说明已经在这个部件上按下了向上方向键。
滚轮放大前后的对比图如下:
Qt事件系统三:鼠标事件和滚轮事件
一、 QMouseEvent - 鼠标事件
QMouseEvent 类用来表示一个鼠标事件,当在窗口部件中按下鼠标、释放鼠标和移动鼠标指针时,都会产生鼠标事件 QMouseEvent。利用 QMouseEvent 类可以获知鼠标是哪个键按下释放了、鼠标指针的当前位置等信息。通常是重定义窗口部件的鼠标事件处理函数来进行一些自定义的操作。
- Qt 中的 QMouseEvent 一般只涉及按下鼠标、释放鼠标和移动鼠标指针等操作,而对鼠标滚轮的响应则通过 QWheeEvent 来处理。
- 鼠标移动事件只会在按下鼠标按键的情况下才会发生,除非通过显式调用。
- QWidget::setMouseTracking() 函数来开启鼠标轨迹,这种情况下只要鼠标指针在移动,就会产生一系列的 Qt 鼠标事件。
QMouseEvent 的传递
多个重叠的窗口在实现里好比一个递归的倒立树,鼠标事件会沿着鼠标指针所在的父窗口的链表向上传递,直到某个窗口调用 accept() 函数进行事件处理,否则该事件将被过滤销毁掉。
-
如果想要鼠标指针所在的父窗口不接收该事件,则可以调用函数 ignore() 予以忽略。
-
如果一个鼠标事件传递给鼠标指针所在的窗口,而该窗口的 QT::WA_NoMousePropagation 位置为TRUE,则该事件不会通过父窗口继续向上传递。
-
可以使用 QWidget::setEnabled() 来开启/关闭对应的窗口是否接受键盘和鼠标事件。
鼠标事件实例
鼠标事件使用的时候,加头文件 <QMouseEvent>,然后在头文件中重写这些函数:
#include <QMouseEvent>
protected:
// 鼠标按下事件
void mousePressEvent(QMouseEvent * event);
// 鼠标释放事件
void mouseReleaseEvent(QMouseEvent *event);
// 鼠标移动事件
void mouseMoveEvent(QMouseEvent *event);
然后就是在源文件中去实现具体的逻辑了:
// 鼠标按下事件
void MainWindow::mousePressEvent(QMouseEvent * event)
{
if(event->button() == Qt::LeftButton)
{
qDebug() << "左键按下" ;
}
else if(event->button() == Qt::RightButton)
{
qDebug() << "右键按下" ;
}
// 获取按下时的鼠标指针坐标
qDebug() << event->x() << ":" << event->y();
}
// 鼠标释放事件
void myMouseEvent::mouseReleaseEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
{
qDebug() << "左键释放" ;
}
else if(event->button() == Qt::RightButton)
{
qDebug() << "右键释放" ;
}
// 获取释放时的鼠标指针坐标
qDebug() << "release:" << event->x() << " " << event->y();
}
// 鼠标移动事件
void myMouseEvent::mouseMoveEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
{
qDebug() << "左键移动" ;
}
else if(event->button() == Qt::RightButton)
{
qDebug() << "右键移动" ;
}
// 获取移动过程中的鼠标指针坐标
qDebug() << "move:" << event->x() << " " << event->y();
}
输出如下所示:
15:49:08: Starting F:\Project\C++ Project\Qt\build-myMouseEvent-Desktop_Qt_5_9_7_MinGW_32bit-Debug\debug\myMouseEvent...
左键按下
press: 96 75
左键释放
release: 100 98
move: 75 81
move: 76 81
还有一个鼠标双击事件 mouseDoubleClickEvent() ,与鼠标按下事件大同小异,这里就不再详细讲解了。
QMouseEvent 常用成员函数
- globalPos():返回鼠标指针的全局坐标值(类型为 QPoint),即相对于 PC 屏幕的坐标值,而不是相对于当前打开窗口;
- globalX():返回鼠标事件发生时鼠标指针全局坐标的 X 值;
- globalY():返回鼠标事件发生时鼠标指针全局坐标的 Y 值;
- pos():返回鼠标指针和接受该鼠标事件窗口的相对位置,其中的坐标值为整型;
- posF():返回鼠标指针在接受该鼠标事件窗口的相对位置,该坐标值用float类型表示可以增加精确度。
二、QWheelEvent - 滚轮事件
QWheelEvent 类用来表示鼠标滚轮事件,包含用于描述鼠标滑轮事件的相关参数。函数原型:
QWheelEvent::QWheelEvent(const QPoint &pos, int delta, Qt::MouseButtons buttons,Qt::KeyboardModifiers modifiers, Qt::Orientation orient = Qt::Vertical )
功能与参数:
/*** 创建一个wheelEvent对象, ***/
(1)参数pos代表鼠标指针在窗口中的当前位置,通常用globalPos()初始化QCursor::pos(),但并不总是正确的。如果需要显示指定一个全局位置,可以用其他的构造函数;
(2)参数button用于描述在鼠标事件过程中鼠标按键的状态(state);
(3)delta()可以返回滑动的距离,正数值表示滑轮相对于用户在向前滑动,相反,负数值表示滑轮相对于用户是向后滑动的。;
(4)参数modifiers用于描述在鼠标事件中鼠标状态位改变时的参数值(比如鼠标的左中右按键的切换);
(5)参数orient用于指示鼠标滑轮滚动的方向(水平或者垂直)。
示例如下:
// 滚轮事件
void myMouseEvent::wheelEvent(QWheelEvent *event)
{
if(event->delta() > 0) // 当滚轮向上滑,远离使用者
{
ui->textEdit->zoomIn(); // 进行放大,textEdit的光标和字体都放大
}
else // 当滚轮向下滑,靠近使用者
{
ui->textEdit->zoomOut(); // 进行缩小,textEdit的光标和字体都缩小
}
}
用户向上滑动鼠标,则放大;而向下滑动鼠标,则缩小。
Qt事件系统四:键盘事件
一、简介
QKeyEvent 类用来描述一个键盘事件。当键盘按键被按下或者被释放时,键盘事件便会被发送给拥有键盘输人焦点的部件。
QKeyEvent 的 key() 函数可以获取具体的按键,对于 Qt 中给定的所有按键,可以在帮助中查看 Qt: :Key 关键字。需要特别说明的是,回车键在这里是 Qt::Key_Return;键盘上的一些修饰键,比如 Ctrl 和 Shift 等, 这里需要使用 QKeyEvent 的 modifiers() 函数来获取,可以在帮助中使用 Qt:: KeyboardModifier 关键字来査看所有的修饰键。
QKeyEvent 有两个键盘事件成员函数,在头文件.h中进行声明:
#include <QKeyEvent>
protected:
void keyPressEvent(QKeyEvent *event); //键盘按下事件
void keyReleaseEvent(QKeyEvent *event); //键盘松开事件
二、常用操作
下面是些常用操作:
// 键盘按下事件
void Widget::keyPressEvent(QKeyEvent * event)
{
// 普通键
switch (event->key())
{
// ESC键
case Qt::Key_Escape:
qDebug() <<"ESC";
break;
// 回车键
case Qt::Key_Return:
qDebug() <<"Enter";
break;
// F1键
case Qt::Key_F1:
qDebug() <<"F1";
break;
}
// 两键组合
if(event->modifiers() == Qt::ControlModifier) { // 如果按下了CTRL键
if(event->key() == Qt::Key_M){
qDebug()<<"CTRL + M";
}
}
if(event->modifiers() == Qt::AltModifier) { // 如果按下了ALT键
if(event->key() == Qt::Key_M)
qDebug()<<"ALT + M";
}
if(event->modifiers() == Qt::ShiftModifier){ // 如果按下了Shift键
if(event->key() == Qt::Key_M)
qDebug()<<"Shift + M";
}
// 三键组合Shift + Ctrl + A的实现
if (event->modifiers() == (Qt::ShiftModifier | Qt::ControlModifier) && event->key() == Qt::Key_A) {
qDebug() << "CTRL + Shift + A";
}
}
// 键盘释放事件
void Widget::keyReleaseEvent(QKeyEvent *event)
{
// 方向UP键
if(event->key() == Qt::Key_Up)
{
qDebug() << "release: "<< "up";
}
}
分别按下 ESC、Enter、CTRL + M、ALT + M 等键,“应用程序输出”窗口输出如下:
ESC
Enter
CTRL + M
ALT + M
release: up
下图列出了所有的修饰键:
三、按键与自动重复
自动重复是指按下键盘上的键(修饰键除外)不放时,会不断重复的发送键按下事件,Qt 默认是启用自动重复的,若要实现类似按键 A+D 之类的快捷键,就需要关闭自动重复。可使用如下方法来关闭自动重复:
// 若自动重复则什么也不做
if(QKeyEvent::isAutoRepeat())
return;
Qt 的键盘事件整体表现为,按住一个键时:
1、第一次触发 keyPressEvent(),isAutoRepeat() 返回 false
2、没有触发 keyReleaseEvent(),停顿一会
3、再一次触发 keyPressEvent(),isAutoRepeat() 返回true
4、触发 keyReleaseEvent()
5、若没松开按键,isAutoRepeat() 返回 true,返回第 3 步;若松开按键,isAutoRepeat() 返回 false
四、键盘捕获
可以只指定窗口中的某个控件捕获键盘事件,使其他控件无法获得键盘事件,示例如下。
MyButton.h:
#ifndef MYBUTTON_H
#define MYBUTTON_H
#include <QWidget>
#include <QPushButton>
#include <QKeyEvent>
#include <QDebug>
class MyButton : public QPushButton
{
Q_OBJECT
public:
explicit MyButton(QWidget *parent = nullptr);
protected:
void keyPressEvent(QKeyEvent *event);
};
#endif // MYBUTTON_H
MyButton.cpp
#include "MyButton.h"
MyButton::MyButton(QWidget *parent) : QPushButton(parent)
{
}
void MyButton::keyPressEvent(QKeyEvent *event)
{
qDebug() << "button键盘按键事件:" << this->objectName();
QWidget *ww = keyboardGrabber(); // 返回正在捕获键盘输入的部件,若没有则返回 0
qDebug() << "正在捕获的控件:" << ww;
if(event->key() == Qt::Key_Q) {
qDebug() << "按下了Q键";
this->releaseKeyboard(); // 释放捕获的键盘输入
}
}
Widget.h:
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QDebug>
#include <QKeyEvent>
#include "MyButton.h"
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
protected:
void keyPressEvent(QKeyEvent *event);
private:
MyButton* m_pBtn1;
MyButton* m_pBtn2;
};
#endif // WIDGET_H
Widget.cpp:
#include "widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
this->resize(300,400);
// 初始化按钮1
m_pBtn1=new MyButton();
m_pBtn1->setParent(this);
m_pBtn1->setText("AAA");
m_pBtn1->move(10,10);
m_pBtn1->resize(100,100);
m_pBtn1->setObjectName("aaa");
// 初始化按钮2
m_pBtn2=new MyButton();
m_pBtn2->setParent(this);
m_pBtn2->setText("BBB");
m_pBtn2->move(150,10);
m_pBtn2->resize(100,100);
m_pBtn2->setObjectName("bbb");
// 指定控件捕获键盘
m_pBtn1->grabKeyboard();
/*使按钮 AAA 捕获键盘,此时产生的键盘事件都只会发送给按钮 AAA,也就是说
其他部件无法获得键盘事件。
只有可见的部件才能捕获键盘输入,若 isVisible()返回 false,则该部件不能调用grabKeyboard()函数
*/
}
void Widget::keyPressEvent(QKeyEvent *event)
{
Q_UNUSED(event);
qDebug()<<"Widget发生键盘事件";
}
按下任何键,发现只有按钮 AAA 才能获得键盘事件,按钮 BBB 无法获得键盘事件。
五、键盘按键单击、双击
首先键盘按键的单击、双击实现用的 QTimer,一说到这估计大部分人都知道怎么回事了,但这里也有个误区,那就是如何区分单击和双击的问题。这里使用两次按键的时间间隔来区分,这里在按下、或释放里实现都是可以的,这里我最后选择在释放里实现,后面再说原因。
头文件里定义几个相关变量:
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QKeyEvent>
#include <QTimer>
#include <QDebug>
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
protected:
void keyReleaseEvent(QKeyEvent *event);
private:
QTimer* m_pTimer; // Ctrl双击定时器
int m_nClickCnt = 0; // 点击次数
bool m_bLongPress = false; // 是否为长按
};
#endif // WIDGET_H
再看键盘按键单击、双击实现:
#include "widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
// 定时器
m_pTimer = new QTimer(this);
connect(m_pTimer, &QTimer::timeout, [=]{
m_nClickCnt = 0; // 计数清零
m_pTimer->stop(); // 停止计时
qDebug() << "键盘单击";
});
}
void Widget::keyReleaseEvent(QKeyEvent *event)
{
Q_UNUSED(event);
if(event->key() == Qt::Key_A) {
// 计数期间,如果QTimer已开始,则不重新开始
if(!m_pTimer->isActive())
m_pTimer->start(500); // 500ms是判断双击的时间间隔,不唯一,可根据实际情况改变
m_nClickCnt++; // 点击计数,在500ms内如果点击两次认为是双击
if(m_nClickCnt >= 2) {
m_nClickCnt = 0; // 计数清零
m_pTimer->stop(); // 停止计时
qDebug() << "键盘双击";
}
}
}
单击,在 500ms 内未达到双击次数,也就是未执行 timer_->stop(); 时间耗尽触发 timeout 信号,执行单击动作。这里提一下 stop() 函数,QTimer 执行 start(n) 后,如果不 stop(),那它会循环执行。
六、键盘按键长按
至此实现键盘单击和双击复用,那么我们再来看一下长按怎么处理呢?
为了区分是否是长按,QKeyEvent 提供了一个 isAutoRepeat() 函数自动检测按键是否长按
- 长按返回 true
- 非长按返回 false
前面提到单击和双击的区分,其实在void keyPressEvent(QKeyEvent *event)
、void keyReleaseEvent(QKeyEvent *event)
函数里都可以,反正都是记录时间差,press-press 或 release-release 没分别,那最后为什么选择在按键释放函数里实现呢?
问题就在还得同时实现长按功能,刚刚分析得出无论你长按还是非长按,第一次的 press 动作他都是按下非长按的,如果在void keyPressEvent(QKeyEvent *event)
里实现,那长按必然会附加一次单击,这当然不是我们想要的;
至此分析完毕,我想我们该开始写代码了。
#include "widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
// 定时器
m_pTimer = new QTimer(this);
connect(m_pTimer, &QTimer::timeout, [=]{
m_nClickCnt = 0; // 计数清零
m_pTimer->stop(); // 停止计时
qDebug() << "键盘单击";
});
}
void Widget::keyReleaseEvent(QKeyEvent *event)
{
Q_UNUSED(event);
if(event->key() == Qt::Key_A) { // Qt::Key_Control经实测,长按永远不会使isAutoRepeat()为true
// 是否是长按可以从release中直接判断
if (!event->isAutoRepeat()) {
// LongPress_初始值为false,如果非长按执行单击或双击动作判断
// 如果长按会在长按里将其置true,在最后的Relese(非长按)里就不会执行单击、双击判断的动作
if (!m_bLongPress) {
if (!m_pTimer->isActive()) {
m_pTimer->start(500);
}
m_nClickCnt++;
if (m_nClickCnt == 2){
m_nClickCnt = 0; // 计数清零
m_pTimer->stop(); // 停止计时
qDebug() << "键盘双击";
}
}
m_bLongPress = false; // 置false
}
else{
if (!m_bLongPress) {
qDebug() << "键盘长按";
// 限定长按只执行一次,最后会在Relese(非长按)里将LongPress_重新置false
m_bLongPress = true;
}
}
}
}
Qt事件系统五:定时器与定时事件
一、定时器
在头文件.h中进行声明:
private slots:
void timeOut(); // 定时器超时槽函数
在.cpp中进行实现相应的功能:
// 构造函数
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
// 创建一个新的定时器
QTimer *timer = new QTimer(this);
// 设置定时器1秒钟超时
timer->setInterval(100);
// 关联定时器的超时信号到槽上
connect(timer, SIGNAL(timeout()), this, SLOT(timeOut()));
// 开始计时
timer->start();
}
// 定时器超时槽函数
void Widget::timeOut()
{
QTime time = QTime::currentTime(); // 获取当前时间
QString text = time.toString("hh:mm:ss"); // 转换为字符串
if((time.second() % 2) == 0)
{
// 每隔一秒就将“:”显示为空格
text[2]=' ';
text[5]=' ';
}
qDebug() << text;
}
这里在构造函数中开启了一个 1 秒的定时器,当它溢出时就会发射 timeout() 信号,这时就会执行我们的定时器溢出处理函数。在槽里我们获取了当前的时间,并且将它转换为可以显示的字符串。
“应用程序输出”窗口输出如下:
"15:36:23"
"15 36 24"
"15:36:25"
"15 36 26"
如果我们想让这个计时器只计时一次,那么必须使用void setSingleShot(bool singleShot)
函数。
QTimer *timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(timeOut()));
timer->setsetSingleShot(true)
timer->start(60000);
这样计时器只会倒计时 1 分钟,然后结束。
二、定时事件
QTimerEvent 类用来描述一个定时器事件。对于一个 QObject 的子类,只需要使用 int QObject::startTimer ( int interval) 函数来开启一个定时器,这个函数需要输人一个以毫秒为单位的整数作为参数来表明设定的时间,它返回一个整型编号来代表这个定时器。当定时器溢出时就可以在 timerEvent() 函数中获取该定时器的编号来进行相关操作。
使用 QTimerEvent 的 timerId() 函数来获取定时器的编号,然后判断是哪一个定时器并分别进行不同的操作。
在头文件.h中进行声明:
private:
Ui::Widget *ui;
int id1,id2,id3; // 定时器的编号
protected:
void timerEvent(QTimerEvent *event); // 定时器事件
在.cpp中进行实现相应的功能:
// 构造函数
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
id1 = startTimer(1000); // 开启一个1秒定时器,并返回其id
id2 = startTimer(2000);
id3 = startTimer(4000);
}
// 定时器事件
void Widget::timerEvent(QTimerEvent *event)
{
// 1秒钟时间到,则定时器1溢出
if (event->timerId() == id1)
{
qDebug()<<"timer1";
}
else if(event->timerId() == id2)
{
qDebug()<<"timer2";
}
else if (event->timerId() == id3)
{
qDebug()<<"timer3";
}
}
“应用程序输出”窗口输出如下:
timer1
timer1
timer2
timer1
timer1
timer2
timer3
三、扩展:随机数
在头文件.h中进行声明:
private slots:
void timeOut(); // 定时器超时函数
在.cpp中进行实现相应的功能:
// 构造函数
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
// 创建一个新的定时器
QTimer *timer = new QTimer(this);
// 设置定时器1秒钟超时
timer->start(1000);
// 关联定时器的超时信号到槽上
connect(timer,SIGNAL(timeout()),this,SLOT(timeOut()));
// 使用qsrand()函数为随机数设置初值
qsrand(static_cast<uint>( QTime(0, 0, 0).secsTo(QTime::currentTime()) ));
}
// 定时器超时函数
void Widget::timeOut()
{
int rand = qrand()%300; // 产生300以内的正整数
qDebug()<< rand;
}
在使用 qrand() 函数产生随机数之前,一般要使用 qsrand() 函数为其设置初值,如果不设置初值,那么每次运行程序,qrand() 都会产生相同的一组随机数。
为了每次运行程序时,都可以产生不同的随机数,我们要使用 qsrand() 设置一个不同的初值。这里使用了 QTime 类的 secsTo() 函数,它表示两个时间点之间所包含的秒数,比如代码中就是指从零点整到当前时间所经过的秒数。
当使用 qrand() 要获取一个范围内的数值时,一般是让它与一个整数取余,比如这里与 300 取余,就会使所有生成的数值在 0-299 之间。
“应用程序输出”窗口输出如下:
112
212
223
102