QT跨平台应用程序开发框架(11)—— Qt系统相关 - 教程
目录
虽然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
#include
Label::Label(QWidget* parent) : QLabel(parent)
{}
void Label::mousePressEvent(QMouseEvent *event)
{
//当前 event 就包含了鼠标的位置
qDebug() x() y(); //原点是控件左上角而不是窗口左上角
qDebug() globalX() globalY(); //这个是相对于 “整个屏幕” 左上角为原点的位置
//这个函数其实按下左键、右键、滚轮都能触发有些鼠标还带有前进后退键,也可以触发
if(event->button() == Qt::LeftButton) qDebug() button() == Qt::RightButton) qDebug() << "按下右键";
else qDebug() << "按下其它键";
}
效果如下:
1.3.2 释放事件
要重写的虚函数为:
文件还是前面的 label.cpp ,重写的事件函数如下:
void Label::mouseReleaseEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton) qDebug() button() == Qt::RightButton) qDebug() << "右键释放";
else qDebug() << "其它键释放";
}
效果和上面类似:
1.3.3 双击事件
要重写的槽函数为:
重写的函数如下:
void Label::mouseDoubleClickEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton) qDebug() 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() pos(); //每次移动都打印窗口的位置,左上角在屏幕上的位置
}
void Widget::resizeEvent(QResizeEvent *event)
{
qDebug() 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
#include
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原生的多线程 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); //将自定义函数绑定 widget
thread.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
#include
#include
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 背后的运行逻辑等