前言

参考正点原子QT开发指南,使用正点原子的RK3568开发板。这里不会完整记录QT的详细知识,只是有一个大概的笔记或流程,细节部分用到的时候再去搜索也不迟。

参考:爱编程的大丙

一、初识 Qt

1.1 Qt 是什么

Qt 是一个跨平台的 C++开发库。主要用来开发图形用户界面(Graphical User Interface,简称 GUI)程序。Qt 虽然经常被当做一个 GUI 库,用来开发图形界面应用程序,但这并不是 Qt 的全部;Qt 除了可以绘制漂亮的界面(包括控件、布局、交互),还包含很多其它功能,比如多线程、访问数据库、图像处理、音频视频处理、网络通信、文件操作等,这些 Qt 都已经内置了。

Qt 支持的操作系统有很多,例如通用操作系统 Windows、 Linux、 Unix,智能手机系统Android、 iOS、 WinPhone, 嵌入式系统 QNX、 VxWorks 等等。“跨平台”代表一份代码可以无需任何修改或者小修改就可以在其他平台上运行。

①Qt 能做什么

Qt 能做什么?理论上来说,您能想到的 App, Qt 它都基本能做。 就是专业做APP的。

②QML(Qt Meta-object Language)

是Qt框架中用于构建用户界面的声明式语言,特别适合创建动态、流畅的触摸屏界面。QML通过声明式语法描述界面元素及其交互,使用JavaScript实现逻辑,支持与C++/Python代码集成扩展功能。它基于Qt Quick模块,提供可视化组件、动画框架和模型视图支持‌。(很空洞,不理解,还不打算学习QML)

1.2 第一个 Qt 程序

①新建一个项目

直接在 Qt Creator 激活状态和英文状态的输入法下使用“Ctrl + N”也可以快速打开新建项目。弹出的新建项目如下图,这里我们可以看到有很多模板(包括项目模板和文件和类模板)。
在这里插入图片描述

作为初学者我们选择第一个Application(Qt)和 Qt Widgets Application, 所谓的模板就是 Qt 为了方便开发程序, 在新建工程
时可以让用户基于一种模板来编写程序,包括 cpp 文件, ui 文件都已经快速的创建,而不用用户手动创建这些文件。

在这里插入图片描述
项目名称为test2.

在这里插入图片描述

默认已经是选择 qmake 编译,主要用 qmake 生成 Makefile 用于项目的编译。

在这里插入图片描述

这里默认选择的基类为 QMainWindow。 在 Base class 一项中我们还可以看到还有 QWidget和 QWialog 这样的基类可以选择。我们创建
的这个项目是基于 QMainWindow 类去开发的。 默认勾选“Generate form”,意思是生成 ui 窗体文件 mainwindow.ui。 为了学习方便,我们统一默认基类为 QMainWindow,但是注意,在嵌入式里一般不需要标题栏,状态栏等,所以常用的是 QWidget 基类。

QMainWindow:主窗口类,主窗口具有主菜单栏、工具栏和状态栏。 类似于一般的应用程序的主窗口。如果您想做个嵌套的窗口程序开发的软件,不妨选择这个 QMainWindow。
QWidget:是可视界面类的基类,也就是说 QMainWindow 类也是由 QWidget 继承封装而来。所以 QWidget 要比 QMainWindow 功能少一些。
QDialog:对话框类,建立一个对话框界面。比较少使用此项作为基类。 一般以 QMainWindow和 QWidget 作为基类的居多。

因为 QWidget 不带窗口标题栏等, 嵌入式里最好 QWidget。

在这里插入图片描述
勾选编译器,这个编译器是我们在安装组件时选择的,使用这个编译器可以编译出 windows版本上跑的可执行程序。假若我们现在有 ARM 平台的 Qt 编译器,那么选择 ARM 平台的 Qt 编译器即可编译出 Qt 在 ARM平台上的可执行文件。

②项目文件介绍

在这里插入图片描述

下面是项目内的文件简介。
test1.pro 是项目管理文件,这个项目管理文件十分重要,当您加入了文件或者删除了文件, Qt Creator 会自动修改这个*.pro 文件。有时候需要打开这个*.pro 文件添加我们的设置项。
Header 分组,这个节点下存放的是项目内所有的头文件*.h。
Source 分组,这个节点下存放的是项目内的所有 C++源码文件*.cpp。
Forms 分组,这个节点下是存放项目内所有界面文件*.ui。 *.ui 文件由 XML 语言描述组成,编译时会生成相应的 cpp 文件,这样交叉编译器就可以编译它了。

③项目文件*.pro

#添加了 Qt 的支持的模块, core 与 gui 库是 Qt 的默认设置。
QT       += core gui

#比较 Qt版本
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

#配置的是使用 c++11
CONFIG += c++11

#添加 QT_DEPRECATED_WARNINGS 定义
DEFINES += QT_DEPRECATED_WARNINGS

#SOURCES 下的是源文件
SOURCES += \
    main.cpp \
    mainwindow.cpp

#HEADERS 下是头文件
HEADERS += \
    mainwindow.h

#FORMS 下是 ui 界面文件
FORMS += \
    mainwindow.ui

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
#qnx:判断是不是 qnx 操作系统,赋值 target.path = /temp/$${TARGET}/bin。
#如果是 unix 系统但不是安卓,赋值 target.path = /opt/$${TARGET}/bin。
#如果 target.path 为空目录, 赋值 INSTALLS += target。

如果需要修改生成目标的可执行程序名字,可赋值 TARGET = xxx。否则 TARGET 将默认取值为项目的名字。

④样式文件*.ui

mainwindow.ui 是一个 xml 类型的文件。 这个文件是生成的不能手动编辑。 只能够通过图形界面修改其属性。双击 mainwindow.ui 后可以跳转到设计界面, 如下图。 下面主要介绍主体部分。可以使用鼠标手动设置。
在这里插入图片描述

  1. ①是控件栏,有各种各样的控件,上方的 Filter 是过滤器,输入首写字母就可以快速定到我们想要找的控件。
  2. ②显示的是我们的窗口程序了,上面已经带有 MainWindow 对象及其几个子对象, 默认MainWindow 就带有菜单栏和状态栏。
  3. ③是对象栏, ②处用到的对象都在③处显示。
  4. ④是属性栏, 点击③处对象栏的某个对象,就可以在④属性栏里编辑它的属性了。 属性项有很多,包括位置,大小,文字,颜色,字体等等。

⑤头文件*.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
private:
    Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H

Qt命名空间声明:QT_BEGIN_NAMESPACE和QT_END_NAMESPACE包裹着前向声明

主窗口类定义:

继承自QMainWindow
包含Q_OBJECT宏,启用Qt的元对象系统
构造函数和析构函数
私有成员ui指针,指向界面对象

这个class MainWindow 就是我们需要使用的窗口类,我需要new一个窗口,其他的控件都是在这个窗口之上。

⑥源文件*.cpp

Sources 下的 mainwindow.cpp:

//mainwindow.h:包含主窗口类的声明,定义了类接口和成员变量
#include "mainwindow.h"
//ui_mainwindow.h:由Qt的uic工具自动生成,包含从.ui文件转换而来的界面定义代码
#include "ui_mainwindow.h"
//构造函数是主窗口初始化的核心部分,
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent) //调用基类构造函数,确保主窗口具备标准Qt窗口功能
    , ui(new Ui::MainWindow)  //创建UI对象实例,该对象负责管理所有界面组件
{
    ui->setupUi(this); #调用setupUi()方法设置UI界面
}
//析构函数
MainWindow::~MainWindow()
{
    delete ui;
}

构造函数定义时函数名后冒号:之后的内容称为‌初始化列表。

Sources 下的 main.cpp:

#include "mainwindow.h"  // 包含自定义主窗口类声明
#include <QApplication>  // 必需的核心GUI头文件,提供QApplication类
int main(int argc, char *argv[])  // Qt标准入口函数格式
{
    QApplication a(argc, argv);  // 创建应用核心管理对象,处理事件循环
    MainWindow w;  // 实例化主窗口(触发构造函数)
    w.show();  // 显示窗口(默认隐藏状态)
    return a.exec();  // 启动事件循环并等待退出信号
}

在每一个使用 Qt 的应用程序中都必须使用一个QApplication 对象。 QApplication 管理了各种各样的应用程序的广泛资源,比如默认的字体和光标。

a.exec()就是 main()把控制转交给 Qt,并且当应用程序退出的时候 exec()就会返回。在 exec()中, Qt 接受并处理用户和系统的事件并且把它们传递给适当的窗口部件。有点类似于进程中的exec 族函数,执行新的程序。

二、Qt 信号与槽

2.1 Qt 信号与槽机制

**信号与槽(Signal & Slot)**是 Qt 编程的基础,因为有了信号与槽的编程机制,在 Qt 中处理界面各个组件的交互操作时变得更加直观和简单。

**信号(Signal)**就是在特定情况下被发射的事件,例如 PushButton 的 clicked() 信号.GUI 程序设计的主要内容就是对界面上各组件的信号的响应,只需要知道什么情况下发射哪些信号,合理地去响应和处理这些信号就可以了。

**槽(Slot)**就是对信号响应的函数。槽就是一个函数,与一般的 C++函数是一样的,可以被直接调用,不同的是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行。当一个信号被发射时,与其关联的槽函数通常被立即执行,就像正常调用一个函数一样。只有当信号关联的所有槽函数执行完毕后,才会执行发射信号处后面的代码。

基本格式是:

QObject::connect(sender, SIGNAL(signal(arg,...)), receiver, SLOT(slot(arg,...)));

connect() 是 QObject 类的一个静态函数,而 QObject 是所有 Qt 类的基类,在实际调用时可以忽略前面的限定符。signal 和 slot都带"()",也可以带参数,当信号和槽函数带有参数时,在 connect()函数里,要写明参数的类型,但可以不写参数名称。严格的情况下,信号与槽的参数个数和类型需要一致,至少信号的参数不能少于槽的参数。如果不匹配,会出现编译错误或运行错误。

①一个信号可以连接多个槽

例如:

connect(pushButton, SIGNAL(clicked()), this, SLOT(hide());
connect(pushButton, SIGNAL(clicked()), this, SLOT(close());

当一个信号与多个槽函数关联时,槽函数按照建立连接时的顺序依次执行。即先执行hide()函数,再执行close()函数。

②多个信号可以连接同一个槽

connect(pushButton,SIGNAL(clicked()),this,SLOT(close()));
connect(pushButton_2,SIGNAL(clicked()),this,SLOT(close()));
connect(pushButton_3,SIGNAL(clicked()),this,SLOT(close()));

当任何一个 pushButton 被单击时,都会执行 close()函数,进而关闭或者退出程序。

③信号连接信号

一个信号可以连接另外一个信号(说明了 connect 万物皆可连,非常好用!),例如:

connect(pushButton, SIGNAL(objectNameChanged(QString)),this, SIGNAL(windowTitelChanged(QString)));

这样,当一个信号发射时,也会发射另外一个信号,实现某些特殊的功能。

④disconnect()

disconnect(),这个方法重载了好几个函数,解开格式如下。

bool QObject::disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *method)

1、断开一切与 myObject 连接的信号或槽。
disconnect(myObject, 0, 0, 0);
相当于非静态重载函数:
myObject->disconnect();

2、断开所有连接到特定信号的东西。
disconnect(myObject, SIGNAL(mySignal()), 0, 0);
相当于非静态重载函数:
myObject->disconnect(SIGNAL(mySignal()));
3、与指定的接收者断开连接。
disconnect(myObject, 0, myReceiver, 0);
相当于非静态重载函数:
myObject->disconnect(myReceiver);

信号与槽机制是 Qt GUI 编程的基础,使用信号与槽机制可以比较容易地将信号与响应代码关联起来。

2.2 创建信号

信号(Signals)‌
定义‌:

signals :
	void signalName(arg ......);

信号是 ‌特殊的成员函数‌,由 signals: 关键字声明。
‌没有返回值‌(void),可以有参数。
‌不需要实现‌(Qt 的 moc 会自动生成代码)。
信号通常用于 ‌事件通知‌

信号只需声明, 无需定义,所以我们只需要在 mianwindow.h 里声明信号即可( mianwindow是主窗口,其他控件都是在这个窗口之上建立的)。这里创建一个 void pushButtonTextChanged() 信号,定义信号最好是贴合信号本身的含义。 笔者定义这个信号的意思是按钮的文本发生改变后的 signal。

#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
private:
    Ui::MainWindow *ui;
signals:
    void pushButtonTextChanged();    
};
#endif // MAINWINDOW_H

2.3 创建槽

创建槽的方法也很简单, 也是直接在 mianwindow.h 里直接声明槽,在 mianwindow.cpp 里实现槽的定义, 声明槽必须实现槽的定义(定义指函数体的实现),否则编译器编译时将会报错。槽有以下特点:

  1. 槽可以是任何成员函数、普通全局函数、静态函数
  2. 槽函数和信号的参数和返回值要一致

定义:

public/private slots:
	返回值类型 functionName(arg ......;

例如我们创建了信号: void pushButtonTextChanged(); 所以我们声明的槽函数必须是无返回值类型 void 和无需参数。为了实现功能,我们还声明一个 QPushButton 对象 pushButton,和一个按钮点击的槽函数pushButtonClicked()。按理说,点击Button后,直接发送void pushButtonTextChanged() 信号,执行相应的槽函数changeButtonText()即可,为啥还要槽函数pushButtonClicked() 呢?

因为PushButton这个控件本身没有pushButtonTextChanged()这个信号,这两者无法直接关联在一起,需要一个中间商,槽函数pushButtonClicked() 就是这个中间商,PushButton有clicked()信号,该信号可以连接槽函数pushButtonClicked(),在该函数中emit pushButtonTextChanged()信号,让后再去执行槽函数changeButtonText()。

#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include<QPushButton>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
private:
    Ui::MainWindow *ui;
    QPushButton *pushButton;/* 声明一个对象 pushButton */
signals:
    void pushButtonTextChanged();
public slots:
    void changeButtonText();/* 声明一个槽函数 */
    void pushButtonClicked();/* 声明按钮点击的槽函数 */
};
#endif // MAINWINDOW_H

在 mainwindow.cpp 里 实 现 声 明 的 槽 函 数 void changeButtonText(); 和 void pushButtonClicked();。 同时还实例化了 pushButton 对象。

#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    /* 设置窗体的宽为 800,高为 480 */
    this->resize(800,480);

    /* 实例化 pushButton 对象 */
    pushButton = new QPushButton(this);
   
    /* 调用 setText()方法设定按钮的文本 */
    pushButton->setText("我是一个按钮");
}

MainWindow::~MainWindow()
{
    delete ui;
}

/* 实现按钮点击槽函数 */
void MainWindow::pushButtonClicked()
{
    /* 使用 emit 发送信号 */
    emit pushButtonTextChanged();
}

/* 实现按钮文本改变的槽函数 */
void MainWindow::changeButtonText()
{
    /* 在槽函数里改变按钮的文本 */
    pushButton->setText("被点击了! ");
}

实例化 pushButton 对象,在堆中实例化,并指定父对象为 this, 即该主窗口。

2.4 连接信号与槽

信号与槽连接的代码如下。

connect(pushButton, SIGNAL(clicked()), this, SLOT(pushButtonClicked()));
connect(this, SIGNAL(pushButtonTextChanged()), this, SLOT(changeButtonText()));

注意,发送信号的对象,和接收的信号的对象。 因为我们 pushButtonClicked()是本类里定义的槽,所以用 this 来接收。同理, pushButtonTextChanged()也是本类定义的信号。所以发送者写成 this。 changeButtonText()也是本类的槽函数,所以接收槽的对象也是 this。

连接信号与槽,整个流程就是当点击了按钮,然后触发了 pushButtonClicked(), pushButtonClicked()槽里发送 pushButtonTextChanged()信号, changeButtonText()槽响应 pushButtonTextChanged()信号,我们在 changeButtonText()槽实现响应的动作(事件)。最终的实现效果是按钮的文本由“我是一个按钮” 被点击时变成“被点击了!”。

三、多线程

参考爱编程的大丙
在qt中使用了多线程,有些事项是需要额外注意的:
1、 默认的线程在Qt中称之为窗口线程,也叫主线程,负责窗口事件处理或者窗口控件数据的更新。
2、子线程负责后台的业务逻辑处理,子线程中不能对窗口对象做任何操作,这些事情需要交给窗口线程处理。
3、主线程和子线程之间如果要进行数据的传递,需要使用Qt中的信号槽机制。

3.1 线程类 QThread

Qt中提供了一个线程类,通过这个类就可以创建子线程了,Qt中一共提供了两种创建子线程的方式,后边会依次介绍其使用方式。先来看一下这个类中提供的一些常用API函数:

① 常用共用成员函数

// QThread 类常用 API
// 构造函数
QThread::QThread(QObject *parent = Q_NULLPTR);
// 判断线程中的任务是不是处理完毕了
bool QThread::isFinished() const;
// 判断子线程是不是在执行任务
bool QThread::isRunning() const;

// Qt中的线程可以设置优先级
// 得到当前线程的优先级
Priority QThread::priority() const;
void QThread::setPriority(Priority priority);
优先级:
    QThread::IdlePriority         --> 最低的优先级
    QThread::LowestPriority
    QThread::LowPriority
    QThread::NormalPriority
    QThread::HighPriority
    QThread::HighestPriority
    QThread::TimeCriticalPriority --> 最高的优先级
    QThread::InheritPriority      --> 子线程和其父线程的优先级相同, 默认是这个
// 退出线程, 停止底层的事件循环
// 退出线程的工作函数
void QThread::exit(int returnCode = 0);
// 调用线程退出函数之后, 线程不会马上退出因为当前任务有可能还没有完成, 调回用这个函数是
// 等待任务完成, 然后退出线程, 一般情况下会在 exit() 后边调用这个函数
bool QThread::wait(unsigned long time = ULONG_MAX);

②信号槽

// 和调用 exit() 效果是一样的
// 调用这个函数之后, 再调用 wait() 函数
[slot] void QThread::quit();
// 启动子线程
[slot] void QThread::start(Priority priority = InheritPriority);
// 线程退出, 可能是会马上终止线程, 一般情况下不使用这个函数
[slot] void QThread::terminate();

// 线程中执行的任务完成了, 发出该信号
// 任务函数中的处理逻辑执行完毕了
[signal] void QThread::finished();
// 开始工作之前发出这个信号, 一般不使用
[signal] void QThread::started();

③静态函数

// 返回一个指向管理当前执行线程的QThread的指针
[static] QThread *QThread::currentThread();
// 返回可以在系统上运行的理想线程数 == 和当前电脑的 CPU 核心数相同
[static] int QThread::idealThreadCount();
// 线程休眠函数
[static] void QThread::msleep(unsigned long msecs);	// 单位: 毫秒
[static] void QThread::sleep(unsigned long secs);	// 单位: 秒
[static] void QThread::usleep(unsigned long usecs);	// 单位: 微秒

④任务处理函数

// 子线程要处理什么任务, 需要写到 run() 中
[virtual protected] void QThread::run();

这个run()是一个虚函数,如果想让创建的子线程执行某个任务,需要写一个子类让其继承QThread,并且在子类中重写父类的run()方法,函数体就是对应的任务处理流程。另外,这个函数是一个受保护的成员函数,不能够在类的外部调用,如果想要让线程执行这个函数中的业务流程,需要通过当前线程对象调用槽函数start()启动子线程,当子线程被启动,这个run()函数也就在线程内部被调用了。

3.2 方法一:继承 QThread 的线程

Qt中提供的多线程的第一种使用方式的特点是: 简单。操作步骤如下:

  //要创建一个线程类的子类,让其继承QT中的线程类 QThread,比如:
class MyThread:public QThread
{
    ......
}

重写父类的 run() 方法,在该函数内部编写子线程要处理的具体的业务流程,可以在.h头文件在声明,在.c文件中定义具体内容。

class MyThread:public QThread
{
    ......
 protected:
    void run() override
    {
        ........
    }
 protected:
    void run() override;
}

在主线程中创建子线程对象,new 一个就可以了

MyThread * subThread = new MyThread;

启动子线程, 调用 start() 方法

if (!workerThread->isRunning())
	subThread->start();

不能在类的外部调用run() 方法启动子线程,在外部调用start()相当于让run()开始运行。

当子线程别创建出来之后,父子线程之间的通信可以通过信号槽的方式,注意事项:

在Qt中在子线程中不要操作程序中的窗口类型对象, 不允许, 如果操作了程序就挂了。
只有主线程才能操作程序中的窗口对象, 默认的线程就是主线程, 自己创建的就是子线程。

这种方法有个明显的缺陷,就是run函数无法传参,只能通过信号与槽的机制进行传参。

3.3 方法二:继承 QObject 的线程

Qt提供的第二种线程的创建方式弥补了第一种方式的缺点,用起来更加灵活,但是这种方式写起来会相对复杂一些,它通过 QObject::moveToThread()方法,将一个 QObeject的类转移到一个线程里执行,就是说,开辟一个新线程,至少需要一个 QObeject类和一个QThread类。其具体操作步骤如下:

创建一个新的类,让这个类从QObject派生

class MyWork:public QObject
{
    .......
}

在这个类中添加公共的成员函数,函数体就是我们要子线程中执行的业务逻辑

class MyWork:public QObject
{
public:
    .......
    // 函数名自己指定, 叫什么都可以, 参数可以根据实际需求添加
    void working(arg ......);
    void working2(arg ......);
    void working3(arg ......);
}

在主线程中创建一个QThread对象, 这就是子线程的对象

QThread* sub = new QThread;

在主线程中创建工作的类对象(千万不要指定给创建的对象指定父对象)

MyWork* work = new MyWork(this);    // error
MyWork* work = new MyWork;          // ok

将MyWork对象移动到创建的子线程对象中, 需要调用QObject类提供的moveToThread()方法,可以自行安排那个函数到那个线程中,更具有灵活性。

// void QObject::moveToThread(QThread *targetThread);
// 如果给work指定了父对象, 这个函数调用就失败了
// 提示: QObject::moveToThread: Cannot move objects with a parent
work->moveToThread(sub);	// 移动到子线程中工作

启动子线程,调用 start(), 这时候线程启动了, 但是移动到线程中的对象并没有工作,这点不同于继承QThread的类,继承QThread的类需要重写run函数,因为其调用 start()后,默认就是启动run函数,而继承QObject的类没有指定默认运行的函数。

只有主动调用MyWork类对象的工作函数,让这个函数开始执行,这时候才在移动到的那个子线程中运行。
主动调用,不是在主线程函数在直接使用MyWork类对象的工作函数,不是MyWork::working(arg ......),这样就是在主线程中执行该函数了,不是多线程了,需要使用connect函数进行信号与槽(即MyWork类对象的工作函数)的连接。且connect函数的槽函数就是指定在新线程中运行的函数,所以说,继承QObject的类可以有多个run()函数(相对于继承QThread),可以传参,可以指定运行那个run函数。

// 启动线程
sub->start();
// 让工作的对象开始工作, 点击开始按钮, 开始工作
connect(ui->startBtn, &QPushButton::clicked, work, &MyWork::working);

3.4 线程资源释放

可以在主函数,也就是mainwindow.c的析构函数中进行资源的释放。如果已经指定了父类,可以不需要下面的显示清除。

对于继承QThread的线程:

workerThread->quit();
/* 阻塞等待 2000ms 检查一次进程是否已经退出 */
if (workerThread->wait(2000)) 
	qDebug()<<"线程已经结束! "<<endl;
workerThread->deleteLater();

对于继承QObject的线程:

/* 打断线程再退出 */
worker->stopWork(); 
worker->deleteLater();
workerThread.quit();
/* 阻塞线程 2000ms,判断线程是否结束 */
if (workerThread.wait(2000)) 
	qDebug()<<"线程结束"<<endl;
workerThread->deleteLater();

四、网络编程

想要在程序中使用 Qt 网络模块,我们需要在 pro 项目配置文件里增加下面的一条语句:QT += network

4.1 获取本机网络信息

在网络应用中,经常需要用到本机的主机名、 IP 地址、 MAC 地址等网络信息,Qt 提供了 QHostInfo 和 QNetworkInterface 类可以用于此类信息查询。

 /* 通过QHostInfo的localHostName函数获取主机名称 */
 QString str = "主机名称:" + QHostInfo::localHostName() + "\n";

/* 获取所有的网络接口,
  * QNetworkInterface类提供主机的IP地址和网络接口的列表 */
 QList<QNetworkInterface> list
         = QNetworkInterface::allInterfaces();

 /* 遍历list */
 foreach (QNetworkInterface interface, list) {
     str+= "网卡设备:" + interface.name() + "\n";
     str+= "MAC地址:" + interface.hardwareAddress() + "\n";

     /* QNetworkAddressEntry类存储IP地址子网掩码和广播地址 */
     QList<QNetworkAddressEntry> entryList
             = interface.addressEntries();

     /* 遍历entryList */
     foreach (QNetworkAddressEntry entry, entryList) {
         /* 过滤IPv6地址,只留下IPv4 */
         if (entry.ip().protocol() ==
                 QAbstractSocket::IPv4Protocol) {
             str+= "IP 地址:" + entry.ip().toString() + "\n";
             str+= "子网掩码:" + entry.netmask().toString() + "\n";
             str+= "广播地址:" + entry.broadcast().toString() + "\n\n";
         }
     }
 }

QHostInfo 的 localHostName 函数获取主机名称。
QNetworkInterface::allInterfaces()获取网络接口列表 list 类存储 IP 地址子网掩码和广播地址。

4.2 TCP通信

TCP 协议(Transmission Control Protocol)全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。
TCP 通信必须先建立 TCP 连接,通信端分为客户端和服务端。服务端通过监听某个端口来监听是否有客户端连接到来,如果有连接到来,则建立新的 socket 连接;客户端通过 ip 和port 连接服务端,当成功建立连接之后,就可进行数据的收发了。

①QTcpServer

实例化 tcp 服务器与 tcp 套接字。tcpServer是服务端,可以接受客户端的内容,使用套接字tcpSocket 接收,tcpSocket 用于获取客户端的套接字。

tcpServer = new QTcpServer(this);
tcpSocket = new QTcpSocket(this);

客户端连接,获取客户端信息。nextPendingConnection()阻塞等待客户端发起的连接请求,不推荐在单线程程序中使用,建议使用非阻塞方式处理新连接,即使用信号 newConnection() ,即使用该信号间接连接nextPendingConnection()函数。

/* 获取客户端的套接字 */
tcpSocket = tcpServer->nextPendingConnection();
/* 客户端的 ip 信息 */
QString ip = tcpSocket->peerAddress().toString();
/* 客户端的端口信息 */
quint16 port = tcpSocket->peerPort();

相关监听。

bool QTcpServer::listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0);
参数:
    address:通过类QHostAddress可以封装IPv4、IPv6格式的IP地址,QHostAddress::Any表示自动绑定,
    			常用 QHostAddress::Any 表示监听所有可用 IPv4 地址‌。
    port:如果指定为0表示随机绑定一个可用端口。
返回值:绑定成功返回true,失败返回false

// 判断当前对象是否在监听, 是返回true,没有监听返回false
bool QTcpServer::isListening() const;
// 如果当前对象正在监听返回监听的服务器地址信息, 否则返回 QHostAddress::Null
QHostAddress QTcpServer::serverAddress() const;
// 如果服务器正在侦听连接,则返回服务器的端口; 否则返回0
quint16 QTcpServer::serverPort() const

②QTcpSocket

QTcpSocket是一个套接字通信类,不管是客户端还是服务器端都需要使用。在Qt中发送和接收数据也属于IO操作(网络IO),先来看一下这个类的继承关系:
在这里插入图片描述

连接服务器,需要指定服务器端绑定的IP和端口信息。

[virtual] void QAbstractSocket::connectToHost(const QString &hostName, quint16 port,
				OpenMode openMode = ReadWrite, NetworkLayerProtocol protocol = AnyIPProtocol);

[virtual] void QAbstractSocket::connectToHost(const QHostAddress &address, quint16 port, 
				OpenMode openMode = ReadWrite);

在Qt中不管调用读操作函数接收数据,还是调用写函数发送数据,操作的对象都是本地的由Qt框架维护的一块内存。因此,调用了发送函数数据不一定会马上被发送到网络中,调用了接收函数也不是直接从网络中接收数据,关于底层的相关操作是不需要使用者来维护的。

接收数据

// 指定可接收的最大字节数 maxSize 的数据到指针 data 指向的内存中
qint64 QIODevice::read(char *data, qint64 maxSize);
// 指定可接收的最大字节数 maxSize,返回接收的字符串
QByteArray QIODevice::read(qint64 maxSize);
// 将当前可用操作数据全部读出,通过返回值返回读出的字符串
QByteArray QIODevice::readAll();

发送数据

// 发送指针 data 指向的内存中的 maxSize 个字节的数据
qint64 QIODevice::write(const char *data, qint64 maxSize);
// 发送指针 data 指向的内存中的数据,字符串以 \0 作为结束标记
qint64 QIODevice::write(const char *data);
// 发送参数指定的字符串
qint64 QIODevice::write(const QByteArray &byteArray);

③信号

当接受新连接导致错误时,将发射如下信号。socketError参数描述了发生的错误相关的信息。

[signal] void QTcpServer::acceptError(QAbstractSocket::SocketError socketError);

每次有新连接可用时都会发出 newConnection() 信号。

[signal] void QTcpServer::newConnection();

在使用QTcpSocket进行套接字通信的过程中,如果该类对象发射出readyRead()信号,说明对端发送的数据达到了,之后就可以调用 read 函数接收数据了。

[signal] void QIODevice::readyRead();

调用connectToHost()函数并成功建立连接之后发出connected()信号。

[signal] void QAbstractSocket::connected();

在套接字断开连接时发出disconnected()信号。

[signal] void QAbstractSocket::disconnected();

④服务器端通信流程

创建套接字服务器QTcpServer对象
通过QTcpServer对象设置监听,即:QTcpServer::listen()
基于QTcpServer::newConnection()信号检测是否有新的客户端连接
如果有新的客户端连接调用QTcpSocket *QTcpServer::nextPendingConnection()得到通信的套接字对象
使用通信的套接字对象QTcpSocket和客户端进行通信

⑤客户端通信流程

创建通信的套接字类QTcpSocket对象
使用服务器端绑定的IP和端口连接服务器QAbstractSocket::connectToHost()
使用QTcpSocket对象和服务器进行通信

4.3 UDP通信

Qt中的UDP通信主要通过QUdpSocket类实现,具有无连接、不可靠但高效的特点,适用于实时性要求高的场景。以下是核心要点:

①基础使用步骤‌

创建Socket对象‌

QUdpSocket *udpSocket = new QUdpSocket(this);

‌绑定端口(接收端)‌

udpSocket->bind(QHostAddress::Any, 12345);  // 监听所有IPv4地址的12345端口

失败时可调用errorString()获取错误信息。

‌发送数据‌
使用writeDatagram()发送数据报,需指定目标地址和端口:

udpSocket->writeDatagram(data, QHostAddress("192.168.1.100"), 54321);

‌接收数据‌
通过readyRead信号触发读取:

connect(udpSocket, &QUdpSocket::readyRead, this, &MyClass::readData);

在槽函数中解析数据:

QByteArray datagram; /* udpSocket 发送的数据报是 QByteArray 类型的字节数组 */
datagram.resize(udpSocket->pendingDatagramSize());
QHostAddress sender; //用于获取发送者的 IP
quint16 senderPort;  // 用于获取发送者的端口
udpSocket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);

②通信模式‌

‌单播(Unicast)‌
一对一通信,connect()函数需明确目标地址和端口。

‌广播(Broadcast)‌
发送到同一子网所有主机,connect()的目标地址设为QHostAddress::Broadcast(通常为255.255.255.255)。

‌组播(Multicast)‌
一个 D 类 IP 地址的第一个字节必须以“1110”开始, D 类 IP 地址不分网络地址和主机地址,是一个专门保留的地址,其地址范围为 224.0.0.0~239.255.255.255。 D 类 IP 地址主要用于多点广播(Multicast,也称为多播(组播))之中作为多播组 IP 地址。

每一个多播组ip地址都是一个小组,类似于qq群的群地址,如果一个主机想要获取多播组中的信息,那么他就需要加入这个多播组,即UDP套接字绑定多播组ip地址和端口号。

加入组播组后收发数据:

udpSocket->bind(QHostAddress::AnyIPv4, port, QUdpSocket::ShareAddress)  //加入组播前必须先绑定端口
udpSocket->joinMulticastGroup(QHostAddress("224.0.0.100"));  // 加入组播组

六、智能家居

功能:
① Mqtt通信:继承QThread的线程,led主题(开关状态)、video主题(控制udp图传开关)、beep警报主题(开关状态)
点击mqtt连接按钮后:
更新控件,进行mqtt连接,设置断开连接时的回调函数 cl 、 接收消息的回调函数 ma、 发布消息的回调函数 dc(没实现)。
在接收消息的回调函数 中,判断消息主题和相应的信息,进而进行设备的操作。

后续的设备端mqtt主题发送,是由QFileSystemWatcher进行监测和发送。

connect(startMqttButton, SIGNAL(clicked(bool)), this, SLOT(startMqttButtonClicked(bool)));//主线程更新控件

connect(startMqttButton, SIGNAL(clicked(bool)), mqttthread, SLOT(mqttControl(bool)));//主线程mqtt连接、订阅主题,调用一次mqttthread->start()

mqttthread->start() //新线程mqttthread发送led、beep、video信息

mqttThread::msgarrvd(arg ……)// 接收消息函数,通过三个主题的信息,控制led、video、beep的开关、状态。

② V4l2图像采集:继承QThread的线程,将内存映射的图片通过信号signals:void imageReady(QImage); 发送给主线程的QLabel *videoLabel; 进行显示。

在重写的run函数中,开启v4l2,在while循环中不断发送imageReady(QImage)信号,也可以通过按键选择是否开启udp本地广播和http的图片分块传输。

connect(videoButton, SIGNAL(clicked()), this, SLOT(openVideoWindow()));//打开video视图
connect(videoButton, SIGNAL(clicked()), captureThread, SLOT(setThreadStart()));//启动新线程v4l2

connect(captureThread, SIGNAL(imageReady(QImage)), this, SLOT(showImage(QImage)));//新线程发送图片,主线程显示图片
connect(closeButton, SIGNAL(clicked()), this, SLOT(closeVideoWindow())); //关闭video视图
connect(closeButton, SIGNAL(clicked()), captureThread, SLOT(setThreadStop()));//停止新线程v4l2

③ Udp通信:UDP 协议快速传输视频画面。在v4l2的captureThread, SLOT(setThreadStart()中实现。

connect(checkBox1, SIGNAL(clicked(bool)), captureThread, SLOT(setBroadcast(bool)));//判断新线程v4l2是否进行udp图像广播。

④ Linux驱动开发(感应led):继承QObject的线程,I2C驱动三合一环境传感器AP3216C。当距离传感器太近时,自动开启led。
程序运行后:
配置好IIC传感器的驱动,在构造、析构函数进行驱动的加载、卸载。
开启后:autoledRun函数运行
使用fd(/dev/ap3216c)读取数据,应用层read对应驱动中的file_operations的read。
根据所设定的阈值判断灯的开关,可以延时1s,避免不断监测,消耗cpu资源。

 //autoled新线程
    QThread* sub = new QThread;
    Mywork* work = new Mywork;
    work->moveToThread(sub);
class Mywork : public QObject
{
    Q_OBJECT
public:
    Mywork();//单片机加载驱动模块
    ~Mywork();//卸载驱动模块

public slots:
    void autoledRun();//控制自动感应灯光的开启和关闭

private:
    //自动感应灯相关
    unsigned short databuf[3];//读取3种数据
    unsigned short ir, als, ps;
    int fd;//open函数返回值,文件标识符
    const char * limit_ps ="10";//感应距离
};

⑤ Watcher监视文件变化,主动发布mqtt信息,更改控件。
linux下皆文件,通过监视文件是否改动,进行相应的控件更新和mqtt消息发布。

watcher=new QFileSystemWatcher(this);
watcher->addPath(led_path);
watcher->addPath(beep_path);
watcher->addPath(video_path);
//led、beep、video文件状态改变触发,更新状态,判断是否发送mqtt
connect(watcher, SIGNAL(fileChanged(QString)), this, SLOT(onFileChanged(QString)));
void MainWindow::onFileChanged(const QString &path)
{
    Q_UNUSED(path)
    updateledState();
    updatebeepState();
    if(mqttFlag){ //mqtt connected
        mqttthread->start();//mqtt发送开发板led、beep状态。
        videoState();
    }
}

⑥ 基于QObject的自动感应灯
程序运行后:
配置好IIC传感器的驱动,在构造、析构函数进行驱动的加载、卸载。
开启后:autoledRun函数运行
使用fd(/dev/ap3216c)读取数据,应用层read对应驱动中的file_operations的read。
根据所设定的阈值判断灯的开关,可以延时1s,避免不断监测,消耗cpu资源。

 //autoled新线程
    QThread* sub = new QThread;
    Mywork* work = new Mywork;
    work->moveToThread(sub);
class Mywork : public QObject
{
    Q_OBJECT
public:
    Mywork();//单片机加载驱动模块
    ~Mywork();//卸载驱动模块

public slots:
    void autoledRun();//控制自动感应灯光的开启和关闭

private:
    //自动感应灯相关
    unsigned short databuf[3];//读取3种数据
    unsigned short ir, als, ps;
    int fd;//open函数返回值,文件标识符
    const char * limit_ps ="10";//感应距离
};
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐