QT跨平台应用程序开发框架(11)—— Qt系统相关
目录
一,事件
1.1 关于事件
1.2 处理事件
1.3 处理鼠标事件
1.3.1 点击事件
1.3.2 释放事件
1.3.3 双击事件
1.3.4 滚轮事件
1.3.5 注意事项
1.4 处理键盘事件
1.5 定时器事件
1.6 窗口移动和大小改变事件
二,文件操作
2.1 文件操作概述
2.2 QFile 介绍
2.3 QFile 使用
2.4 QFileInfo 使用
三,Qt多线程
3.1 介绍
3.2 多线程版倒计时
3.3 锁
3.4 条件变量和信号量
虽然Qt是跨平台的 C++ 开发框架,但是Qt 的很多能力其实是操作系统提供的,只不过 Qt 封装了系统的 API
一,事件
1.1 关于事件
用户进行的各种操作,就可能会产生信号,可以指定槽函数,当信号触发时,就能够自动执行到对应的槽函数
同时,用户的各种操作,也会产生“事件”,事件的概念和信号非常相似,同样也可以给事件关联上一些函数,当事件触发时,能够执行到对应代码
事件与信号槽的关系:
- 事件本身是操作系统提供的机制,Qt 也就是将其进行了封装
- 但是事件对应的代码编写起来不是很方便,所以Qt 对事件机制进行进一步的封装,就有了信号槽
- 所以信号槽是对于事件的进一步封装,事件是信号槽的底层机制
实际开发中,绝大部分和用户的交互都是通过“信号槽”来完成,但在有些特殊情况下,信号槽可能无法满足需求,所以此时就需要重写事件处理函数的形式,来手动处理事件的响应逻辑
常见的 Qt 事件如下:
常见事件描述:
名称 | 描述 |
---|---|
鼠标事件 | 鼠标左键、鼠标右键、鼠标滚轮,鼠标的移动,鼠标按键的按下和松开 |
键盘事件 | 按键类型、按键按下、按键松开 |
定时器事件 | 定时时间到达 |
进入离开事件 | 鼠标的进入和离开 |
滚轮事件 | 鼠标滚轮滚动 |
绘屏事件 | 重绘屏幕的某些部分 |
显示隐藏事件 | 窗口的显示和隐藏 |
移动事件 | 窗口位置的变化 |
窗口事件 | 是否为当前窗口 |
大小改变事件 | 窗口大小改变 |
焦点事件 | 键盘焦点移动 |
拖拽事件 | 用鼠标进行拖拽 |
1.2 处理事件
所谓处理事件,就是将事件和一段代码关联起来,当事件触发时,就能执行这段代码
之前我们通过 connect 将事件和槽关联,但是要想关联事件,需要重写某个事件处理函数
下面我们演示一下鼠标进入和鼠标离开事件,假设 有一个按钮,当鼠标移到上面时就会触发鼠标进入事件,移开时会触发离开事件,需要重写的虚函数如下:
我们先创建一个继承 QWidget 的项目,我们可以在界面上放一个 label,当鼠标移动到 label 里时,显示一些文字,离开 label 时显示另一些文字:
然后我们创建一个 QLabel 的子类,然后在这个子类里重写 enterEvent 和 leaveEvent:
然后修改下构造函数:
然后就是重写两个虚函数了,下面是 label.cpp 的内容:
#include "label.h"Label::Label(QWidget* parent) : QLabel(parent)
{}void Label::enterEvent(QEvent *event)
{this->setText("鼠标进来了");
}void Label::leaveEvent(QEvent *event)
{this->setText("鼠标出去了");
}
但是此时我们执行后,我们的 label 并没有什么变化,因为,我们在ui界面通过拖拽方式创建的 Label,还是 QLabel 类型,所以我们需要提升 label 的类型,如下:
然后就可以处理事件了,效果如下:
1.3 处理鼠标事件
1.3.1 点击事件
我们下面演示一下通过事件获取鼠标点击的位置
以1.2 中的代码为例进行扩展,先把label进行扩大:
需要重写的函数如下:
label.cpp 代码如下:
#include "label.h"
#include <QMouseEvent>
#include <QDebug>Label::Label(QWidget* parent) : QLabel(parent)
{}void Label::mousePressEvent(QMouseEvent *event)
{//当前 event 就包含了鼠标的位置qDebug() << "控件里位置:" << event->x() << ", " << event->y(); //原点是控件左上角而不是窗口左上角qDebug() << "屏幕上位置:" << event->globalX() << ", " << event->globalY(); //这个是相对于 “整个屏幕” 左上角为原点的位置//这个函数其实按下左键、右键、滚轮都能触发有些鼠标还带有前进后退键,也可以触发if(event->button() == Qt::LeftButton) qDebug() << "按下左键";else if(event->button() == Qt::RightButton) qDebug() << "按下右键";else qDebug() << "按下其它键";
}
效果如下:
1.3.2 释放事件
要重写的虚函数为:
文件还是前面的 label.cpp ,重写的事件函数如下:
void Label::mouseReleaseEvent(QMouseEvent *event)
{if(event->button() == Qt::LeftButton) qDebug() << "左键释放";else if(event->button() == Qt::RightButton) qDebug() << "右键释放";else qDebug() << "其它键释放";
}
效果和上面类似:
1.3.3 双击事件
要重写的槽函数为:
重写的函数如下:
void Label::mouseDoubleClickEvent(QMouseEvent *event)
{if(event->button() == Qt::LeftButton) qDebug() << "左键双击";else if(event->button() == Qt::RightButton) qDebug() << "右键双击";else qDebug() << "其它键双击";
}
只有当第二次按下时,才能识别为“双击”,所以一次双击按顺序会触发四个事件:按下,释放,双击,释放
1.3.4 滚轮事件
需要重写的虚函数为:
代码如下:
int total = 0;
void Label::wheelEvent(QWheelEvent *event) //QWheelEvent 是一个专门的鼠标滚轮的类
{total += event->delta();//可以获取鼠标滚动了多远qDebug() << total;
}
1.3.5 注意事项
注意一:如果想将上面鼠标的事件从label控件扩展到整个窗口,也只需要在 QWidget 类中重写对应的虚函数即可
注意二:关于鼠标移动事件:
- 鼠标移动事件不同于其它的鼠标事件,只要鼠标移动,就会产生巨量的鼠标移动事件,一旦该事件的逻辑比较多,系统就容易卡顿
- 所以 Qt 为了程序的流畅性,鼠标移动时,不会调用 mouseMoveEvent ,除非是显示告诉 Qt 就要追踪鼠标位置,需要在构造函数里设置:this->setMouseTracking(true); 告诉Qt我要追踪鼠标位置
1.4 处理键盘事件
按键事件是通过 QKeyEvent 类来实现,我们前面是通过 QShortCut 搭配 QKeySequence 的,先通过 QShortCut 绑定一个快捷键,当快捷键被按下,会产生一个信号,再通过槽进行代码逻辑
当然,上面是信号槽机制封装过的获取键盘按键的方式,站在更底层的角度,也可以通过事件获取到用户键盘按下的情况的,需要重写的虚函数如下:
重写的函数如下:
void Label::keyPressEvent(QKeyEvent *event)
{//可以检测单个按键,也可以检测组合键if(event->modifiers() == Qt::ControlModifier) //判断Ctrl键是否被按下{if(event->key() == Qt::Key_A)qDebug() << "Ctrl + A 被按下";}
}
Qt::KeyboardModifier 中定义了在处理键盘事件时对应的修改键。在Qt中,键盘事件可以与修改键 ⼀起使用,以实现⼀些复杂的交互操作。KeyboardModifier中修改键的具体描述如下:
- Qt::NoModifier:无修改键
- Qt::ShiftModifier:Shift 键
- Qt::ControlModifier:Ctrl 键
- Qt::AltModifier:Alt 键
- Qt::MetaModifier:Meta键(在Windows上指Windows键,在macOS上指Command键)
- Qt::KeypadModifier:使用键盘上的数字键盘进行输⼊时,Num Lock键处于打开状态
- Qt::GroupSwitchModifier:用于在输入法组之间切换
1.5 定时器事件
Qt 中的定时器分为 QTimerEvent 和 QTImer 两个类:
- QTimerEvent类:用来描述一个定时器事件。在使用时需要通过 startTimer() 函数来开启一个定时器,需要输入一个以 ms 为单位的整数作为参数来表明设定的时间,返回值代表这个定时器。当到达指定时时间时,就可以在 timerEvent() 函数中获取该定时器的编号来进行相关操作
- QTimer类:来实现一个定时器,它提供了更高层次的编程接口,如:可以使用信号和槽,还可以设置只运行一次的定时器
我们在 ui 界面上搞两个 Label 控件,一个每过1秒让数字累加一次,一个每过2秒让数字累加一次:
然后我们就可以重写 timerEvent函数了,先在 widget.h 里声明函数:
然后在 widget.cpp 里重写定时器事件:
#include "widget.h"
#include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);//启动定时器t1 = startTimer(1000);t2 = startTimer(2000);//此时这个 t1 和 t2 是一个定时器的标识,或者id//类似我们 Linux 里的文件描述符,起到身份标识的作用
}Widget::~Widget()
{delete ui;
}void Widget::timerEvent(QTimerEvent *e)
{//如果一个程序中存在多个定时器(startTimer 创建的定时器),此时每个定时器都会触发这个 timerEvent 函数if(e->timerId() == t1){static int n1 = 1;ui->label->setText(QString::number(n1++)); //每个一秒加一次}if(e->timerId() == t2){static int n2 = 1;ui->label_2->setText(QString::number(n2++)); //每隔两秒加一次}
}
使用 timerEvent 比 QTimer 复杂得多,不仅要手动管理 timerId,还需要注意多个定时器同时调用这个函数时的区分问题,所以后续开发中,使用 QTimer 即可,我们之前有介绍:QT跨平台应用程序开发框架(6)—— 常用显示类控件-CSDN博客
1.6 窗口移动和大小改变事件
- moveEvent:窗口移动时触发的事件
- resizeEvent:窗口大小改变时触发的事件
直接重写这两个函数即可:
void Widget::moveEvent(QMoveEvent *event)
{qDebug() << event->pos(); //每次移动都打印窗口的位置,左上角在屏幕上的位置
}void Widget::resizeEvent(QResizeEvent *event)
{qDebug() << event->size(); //每次调整大小都打印目前窗口的大小
}
二,文件操作
C++文件操作:C++——IO流-CSDN博客
C语言文件操作:C语言文件操作-CSDN博客
2.1 文件操作概述
- C语言中,我们通过 fopen 打开文件,通过 fread 和 fwrite 读写文件,fclose 关闭文件
- C++中,我们通过 fstream 打开文件,<< 和 >> 读写文件,close 关闭文件
- Linux中,我们也通过原生 API 的 open 打开文件,read 和 write 读写文件,close 关闭文件
我们在 Qt 中也可以使用上述几种方案来读写文件(Linux 需要在 Linux 系统上),但是 Qt 自己也提供了一套文件操作的 API,因为 Qt 诞生的很早,那时候 C++ 还没有“标准化”的概念
所以下面我们都是使用Qt自己提供的这一套文件操作,因为和 QString 等 Qt 内置类进行很好的兼容和配合
Qt 的文件操作也基本是三个:打开,读写,关闭,都是用的 QFile 类来完成操作,主要继承关系如下图:
- QFile:用于文件操作和文件数据读写的类,使用 QFile 可以读写任意格式的件
- QSaveFile:用于安全保存文件的类。使用 QSaveFile 保存文件时,会先把数据写入一个临时文件,成功提交后才将数据写入最终的文件。如果保存过程中出现错误,临时文件里的数据不会被写入最终文件,这样就能确保最终文件中不会丢失数据或只写入了部分数据。在保存比较大的文件或复杂格式的文件时可以使用这个类,例如从网络上下载文件等
- QTemporaryFile:用于创建临时文件的类。使用函数 QTemporaryFile::open() 就能创建一个文件名唯一的临时文件,在 QTemporaryFile 对象被删除时,临时文件也被动删除
- QTcpSocket 和 QUdpSocket:分别实现了TCP和UDP的类
- QSerialPort:是实现了串口通信的类,通过这个类可以实现计算机与串口设备的通信(窗口是一种比较古老的通信方式,一般是在嵌入式系统上)
- QBluetoothSocket:用于蓝牙通信的类。手机、平板计算机和笔记本电脑等移动设备都有蓝牙通信模块。通过 QBluetoothSocket 类,就可以编写蓝牙通信程
- QProcess:用于启动外部程序,并且可以给程序传递参数
- QBuffer:以一个 QByteArray 对象作为数据缓冲区,将 QByteArray 对象当作一个I/O设备来读写
2.2 QFile 介绍
在 Qt 中,文件的读写主要是通过 QFile 类来实现,Qt 中读写文件的方法有:
- 打开文件:open
- 读文件:read,readLine,readAll 等
- 写文件:write,writeData 等
- 关闭文件:close
更多的操作可以在文档中查询关键词 QFile 了解:
①打开:open
open 有好几个版本,比如下面两个:
一个是 FILE*,一个是文件描述符,用起来比较麻烦,所以我们一般用的都是这个:
构造函数中,只需要指定路径后用 open 直接打开即可,OpenMode 是打开的方式,有读方式,写方式,追加写等方式,要详细了解直接在文档里搜索 OpenMode 即可
②读文件:read,readLine,readAll
在 QIODevice 类里可以找到读文件相关函数的介绍:
③写文件:write
④关闭文件:close
2.3 QFile 使用
我们再次创建 mainwindows 项目,直接通过代码去构造界面
先是 mainwindow.h ,在里面声明函数:
然后是 mainwindow.cpp 的代码:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QPlainTextEdit>
#include <QFileDialog>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);//获取到菜单栏QMenuBar* menuBar = this->menuBar();//添加菜单QMenu* m = new QMenu("文件");menuBar->addMenu(m);//添加菜单项QAction* a1 = new QAction("打开");QAction* a2 = new QAction("保存");m->addAction(a1);m->addAction(a2);//指定一个输入框edit = new QPlainTextEdit();this->setCentralWidget(edit);//把字体放大一些QFont font;font.setPixelSize(20);edit->setFont(font);//连接 QAction 的信号槽connect(a1, &QAction::triggered, this, &MainWindow::handleAction1);connect(a2, &QAction::triggered, this, &MainWindow::handleAction2);
}MainWindow::~MainWindow()
{delete ui;
}void MainWindow::handleAction1()
{//1,先弹出一个“打开文件”的对话框,选择文件QString path = QFileDialog::getOpenFileName(this);//2,将文件名显示到状态栏里(也可以搞一个 label,然后放进去)QStatusBar* s = this->statusBar(); //获取状态栏s->showMessage(path);//3,根据文件路径构建 QFile 对象QFile file(path);bool ret = file.open(QIODevice::ReadOnly); //以只读方式打开文件if(!ret){s->showMessage("路径错误或文件不存在!");return;}//4,读取文件QString text = file.readAll();//可以直接用 QString 来接收,因为Qstring 重载了构造函数//可以用 QByteArray,也就是 readAll 的返回值对象来构造 QString 对象//但是这样需要确保打开的是一个文本文件才行,如果是二进制文件,交给 QString 就不合适了//因为二进制文件没有限制,图片,视频等都可以//文本文件必须得是合法字符(指的是遵循 utf8,gbk等编码方式)//5,关闭文件file.close();//6,将读取的内容显示在输入框中edit->setPlainText(text);
}void MainWindow::handleAction2()
{//1,先弹出“保存文件”对话框QString path = QFileDialog::getOpenFileName(this);//2,再状态栏中显示文件名QStatusBar* s = this->statusBar();s->showMessage(path);//3,根据用户选择的路径,构造 QFile 对象,并打开文件QFile file(path);bool ret = file.open(QFile::WriteOnly); //只写方式if(!ret){s->showMessage("路径错误或文件不存在!");return;}//4,写文件const QString& text = edit->toPlainText();file.write(text.toUtf8());//5,关闭文件file.close();
}
这样就完成了一个简单的针对文本文件的打开修改和保存的窗口了
2.4 QFileInfo 使用
对于文件不仅仅只有读写,还有例如获取属性的一系列操作,如下:
- isDir():检查该文件是否是目录
- isExecutable():检查该文件是否是可执行文件
- fileName():获得文件名
- completeBaseName():获取完整的文件名
- suffix():获取文件后缀名
- completeSuffix():获取完整的文件后缀
- size():获取文件大小
- isFile():判断是否为文件
- fileTime():获取文件创建时间、修改时间、最近访问时间等
我们可以通过 QFileInfo 获取到 Qt 的文件的相关属性,下面我们直接创建一个按钮,要求是点击按钮后,打开文件选择窗口,选择好文件后,打印文件的信息,按钮槽函数如下:
void MainWindow::on_pushButton_clicked()
{//弹出文件对话框,并获取文件属性信息QString path = QFileDialog::getOpenFileName(this);QFileInfo f(path);qDebug() << f.fileName();qDebug() << f.suffix();qDebug() << f.path();qDebug() << f.size();qDebug() << f.isFile();qDebug() << f.isDir();
}
三,Qt多线程
3.1 介绍
Qt 多线程概念和 Linux 本质没有区别,可以参考:
- Linux系统编程——线程基本概念-CSDN博客
- Linux系统编程——线程同步互斥与线程安全-CSDN博客
Linux原生的多线程 API,了解即可,因为使用起来很麻烦,可以看到上面两篇文章里的多线程代码很长也很难理解,所以实际开发中很少使用 原生的线程 API
Qt 中的多线程 API,参考了 Java 中线程库 API 的设计方式
在 Qt 中,多线程的处理一般通过 QThread类 来实现,它代表一个在应用程序中可以独立控制的线程,也可以和进程中的其它线程共享数据
总的来说 QThread对象 用于管理程序中的一个线程,常用 API 如下:
API | 说明 |
---|---|
run() | 线程的入口函数
|
start() | 通过调用 run() 开始执行线程
|
currentThread() | 返回⼀个指向管理当前执行线程的 QThread 的指针 |
isRunning() | 如果线程正在运行则返回 true 否则返回false |
sleep() / msleep() / usleep() | 使线程休眠,单位为秒 / 毫秒 / 微秒 |
wait() | 阻塞线程,直到满足以下任何⼀个条件:
|
terminate() | 终止线程的执行。线程可以立即终止,也可以不立即终止,这取决于操作系统的调度策略 在 terminate ()之后使用QThread::wait()来确保。 |
finished() | 当线程结束时会发出该信号,可以通过该信号来实现线程的清理工作 |
3.2 多线程版倒计时
我们之前使用定时器搞过一个倒计时这样的程序:QT跨平台应用程序开发框架(6)—— 常用显示类控件-CSDN博客
咱们也可以通过线程来完成这样的功能,先创建一个人基于 QWidget 的项目,然后创建QWidget 的子类,在 thread.h 中声明下 run 函数,这时候就可以在 .cpp 里重写 run 函数了 :
然后就是编写线程的run函数逻辑了
注意,此时不能直接在 run 中修改界面内容,前面说过,由于存在线程安全问题,Qt 规定只有主线程才能对界面控件进行修改
如下 thread.cpp 的代码:
#include "thread.h"Thread::Thread()
{}void Thread::run()
{//不能修改界面,但是可以计时,每过一秒钟,通过信号槽,通知主线程去更新页面for(int i = 0; i < 10; i++){sleep(1);//发送一个信号去通知主线emit notify();}
}
创建另一个线程,在新线程中进行计时,每循环一次就是 sleep(1),然后就可以更新界面了
下面是 widget.cpp 的代码:
#include "widget.h"
#include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);connect(&thread, &Thread::notify, this, &Widget::handle); //将自定义函数绑定 widgetthread.start(); //启动线程,start() 其实就是去调用 run()
}Widget::~Widget()
{delete ui;
}void Widget::handle() //记得在 .h 中声明
{//此处修改页面内容即可int value = ui->lcdNumber->intValue();if(value > 0) value--;ui->lcdNumber->display(value);
}
应用场景:
- 我们在 Linux 系统里学习多线程,主要是站在服务器开发角度来看待的:Linux 多线程使用的目的,是为了充分利用多核 CPU 的计算找资源,因为一些高性能服务器可能有2个甚至更多个 CPU
- 但是对于客户端来说,用户的“使用体验”是很重要的,如果“非常快”的代价是“系统很卡”,就会降低使用体验,所以客户端上的程序很少会用多线程把 CPU 资源占完,毕竟用户的个人电脑手机等不仅仅只运行你一个程序
- 所以客户端中的多线程,主要用于执行一个耗时的等待 IO 的操作,避免主线程长时间等待 IO 时卡死,比如客户端 上传/下载 一个很大的文件,需要长时间传输,这时候就可以用线程来执行下载操作而不会导致页面卡死了
3.3 锁
谈到多线,就不得不提到“线程安全”这个大话题,3.1 介绍 的两篇文章已经介绍了 线程安全问题的一系列原因后果和解决方法,这里不再赘述
加锁是解决线程安全的最简单的办法,Qt 同样也提供了对于的锁 QMutex,来针对系统的锁进行封装,主要也提供了两种方法:lock 和 unclock ,负责加锁和解锁,下面来演示一下:
下面是 thread.h 的代码,主要包括:声明run函数,添加静态变量和静态锁:
#ifndef THREAD_H
#define THREAD_H#include <QWidget>
#include <QThread>
#include <QMutex>class Thread : public QThread
{Q_OBJECT
public:Thread();void run(); //重要的是重写父类的run函数//添加一个 static 成员变量,然后让两个线程都去修改这个变量static int num;static QMutex mutex;};#endif // THREAD_H
重写run函数,让其对 num 进行加加操作:
void Thread::run()
{for(int i = 0; i < 50000; i++){mutex.lock();num++;mutex.unlock();}
}
然后我们的 主逻辑如下:
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);Thread t1;Thread t2;t1.start();t2.start();//加上等待,让主线程等待这俩线程执行结束t1.wait();t2.wait();qDebug() << Thread::num;
}
上面只是锁的最简单的用法,锁和动态内存一样,都需要手动释放,但是一旦中间有个 if跳过了释放逻辑,或者直接抛异常了,就会造成死锁问题
动态内存无法释放的问题,我们一般用智能指针来搞:C++——C++11智能指针_智能指针c++11-CSDN博客
- 在C++中,对于锁的释放,C++11 引入了 std::lock_guard,相当于是 std::mutex 的智能指针,借助了 RAII 机制
- Qt 也提供了类似的设计,用到的是 QMutexLocker 类,所以下面我们来调一下 run 函数的代码
void Thread::run()
{for(int i = 0; i < 50000; i++){QMutexLocker locker(&mutex);//mutex.lock();num++;//mutex.unlock();}
}
3.4 条件变量和信号量
关于条件变量,3.1 的第二个链接文章的最后一个标题已经介绍过,而关于信号量:Linux系统编程——进程间通信(管道与共享内存)_共享内存 管道-CSDN博客
多个线程之间的调度是无序的,所以我们为了能够一定程度上干预线程的执行顺序,引入了条件变量,Qt 中的条件变量通过 QWaitCondition 来实现,提供了 wait 和 wake,标识等待和唤醒,还有一个 wakeAll ,唤醒所有线程
注意:只有在 mutex.lock() 后才能 wait 等待,因为 wait 函数会先释放锁,然后再等待,所以要想释放锁,必须先得到锁
这两个和我们 Linux 中的使用方式基本一致,只是 API 不一样,这里就不演示了 :Linux系统编程——线程同步互斥与线程安全-CSDN博客
API 本身的使用并不麻烦,更重要的是 API 背后的运行逻辑等