本文最后更新于 2025-02-20,学习久了要注意休息哟

线程是啥、网络是啥、数据库是啥?

​ 你都学到这里了还不知道线程是啥,那你留级吧。。。。

好惨哦,但是关我什么事.jpg

第一章 QT线程编程

多进程

多线程

​ - qt 的多线程 只用于 管理数据 不能对主窗口 进行 读 写

1.1 常用API

1.1.1 公共成员函数

// 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);

1.1.2 信号槽

// 和调用 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();

1.1.3 静态函数

// 返回一个指向管理当前执行线程的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); // 单位: 微秒

1.1.4 任务处理函数

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

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

1.2 操作实例

1.2.1 操作步骤

1、创建线程类的子类

让自定义类继承 Qt 中的线程类 QThread,例:

class MyThread : public QThread
{
    // 自定义线程类
};

2、重写 run() 方法

在子类中重写父类的 run() 方法,用于实现子线程的具体业务逻辑:

class MyThread : public QThread
{
protected:
    void run() override
    {
        // 子线程要处理的具体业务逻辑
    }
};

3、在主线程中创建子线程对象

使用 new 关键字创建线程对象:

MyThread *subThread = new MyThread;

4、启动子线程

调用 start() 方法启动线程,run() 方法会被自动调用:

subThread->start();

注意:不能在类的外部直接调用 run() 方法启动子线程。直接调用 start() 会让 run() 开始运行。

5、父子线程通信

父子线程之间的通信可以通过信号槽机制实现。

[!WARNING]

子线程中不要操作窗口对象

在 Qt 中,子线程不允许直接操作程序的窗口类型对象,否则程序可能会崩溃。

主线程负责窗口操作

只有主线程(默认线程)可以操作窗口对象。用户自行创建的线程属于子线程,不能直接操作窗口对象。

1.2.2 案例:通过多线程实现数数

在没有多线程的情况下,如果程序仅使用一个线程处理数数的任务,数字更新不会实时反映在窗口中,同时拖动窗口可能导致无响应的情况。通过引入多线程,我们可以解决这些问题。

在以下示例中,点击按钮后,通过子线程进行数数任务,并使用信号槽机制将数值传递给主线程,主线程将其更新到窗口中。


1、mythread.h

#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QThread>

class MyThread : public QThread // 注意 需要继承 QThread
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = nullptr);

protected:
    void run() override; // 重写 run() 方法

signals:
    void curNumber(int num); // 自定义信号,用于传递当前数值
};

#endif // MYTHREAD_H

2、mythread.cpp

#include "mythread.h"
#include <QDebug>

MyThread::MyThread(QObject *parent) : QThread(parent)   // 注意 需要初始化 QThread
{
}

void MyThread::run()
{
    qDebug() << "当前线程对象地址: " << QThread::currentThread();

    int num = 0;
    while (true)
    {
        emit curNumber(num++); // 发射信号传递当前数值
        if (num == 10000000) // 数到 10000000 时退出循环
        {
            break;
        }
        QThread::usleep(1); // 模拟耗时任务,降低 CPU 占用率
    }
    qDebug() << "run() 执行完毕,子线程退出...";
}

3、mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "mythread.h"
#include <QDebug>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    qDebug() << "主线程对象地址: " << QThread::currentThread();

    // 创建子线程对象
    MyThread* subThread = new MyThread(this);

    // 连接子线程信号到主线程槽,用于更新 UI
    connect(subThread, &MyThread::curNumber, this, [=](int num)
    {
        ui->label->setNum(num); // 更新窗口中的数值
    });

    // 连接按钮点击信号到子线程启动槽
    connect(ui->startBtn, &QPushButton::clicked, this, [=]()
    {
        subThread->start(); // 启动子线程
    });
}

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

第二章 QT网络编程

2.1 QTcpServer

QTcpServer 类用于监听客户端连接以及与客户端建立连接。

2.1.1 公共成员函数

1. 构造函数

QTcpServer(QObject *parent = Q_NULLPTR);
// 创建 QTcpServer 对象,可以指定父对象。

2. 设置监听

bool listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0);
// 设置监听的地址和端口号。

// 参数:  
// - address:监听的 IP 地址,通过 QHostAddress 类封装,支持 IPv4 和 IPv6;
//            默认值 QHostAddress::Any 表示自动绑定所有可用地址。
// - port:监听的端口号,若为 0 则自动绑定可用端口。  

// 返回值:  
// - true:监听成功。  
// - false:监听失败。

3. 判断是否正在监听

bool isListening() const;
// 判断当前对象是否在监听。

// 返回值:  
// - true:正在监听。  
// - false:未监听。

4. 获取服务器地址

QHostAddress serverAddress() const;
// 如果正在监听,返回服务器的地址;否则返回 QHostAddress::Null。

5. 获取服务器端口

quint16 serverPort() const;
// 如果正在监听,返回服务器的端口;否则返回 0。

6. 获取下一个客户端连接

QTcpSocket* nextPendingConnection();
// 返回和客户端建立连接的 QTcpSocket 对象。

// 说明:  
// - 返回的 QTcpSocket 是 QTcpServer 的子对象,QTcpServer 析构时会自动析构该子对象。  
// - 建议在使用完之后手动析构 QTcpSocket 对象。

7. 阻塞等待新连接

bool waitForNewConnection(int msec = 0, bool *timedOut = Q_NULLPTR);
// 阻塞等待客户端发起连接请求。

// 参数:  
// - msec:最大阻塞时间,单位毫秒,默认值为 0 表示不超时。  
// - timedOut:传出参数,操作超时为 true,未超时为 false。  

// 注意:  
// - 不推荐在单线程程序中使用该方法。  
// - 建议通过信号 newConnection() 处理新连接。

2.1.2 信号

1. 接受连接错误

[signal] void acceptError(QAbstractSocket::SocketError socketError);
// 当接受连接时发生错误,发出此信号。

2. 新连接可用

[signal] void newConnection();
// 每次有新连接可用时发出此信号。

2.2 QTcpSocket

QTcpSocket 是一个套接字通信类,适用于客户端和服务器端的网络通信操作。在 Qt 中,发送和接收数据属于网络 I/O 操作。


2.2.1 公共成员函数

1. 构造函数

QTcpSocket(QObject *parent = Q_NULLPTR);
// 创建 QTcpSocket 对象,可以指定父对象。

2. 连接服务器

[virtual] void connectToHost(const QString &hostName, quint16 port, OpenMode openMode = ReadWrite, NetworkLayerProtocol protocol = AnyIPProtocol);
// 通过主机名和端口号连接到服务器。

// 参数:  
// - hostName:目标服务器的主机名。  
// - port:目标服务器的端口号。  
// - openMode:通信模式,默认值为 ReadWrite(既可读也可写)。  
// - protocol:网络协议类型,默认值为 AnyIPProtocol(支持 IPv4 和 IPv6)。
```cpp
[virtual] void connectToHost(const QHostAddress &address, quint16 port, OpenMode openMode = ReadWrite);
// 通过 IP 地址和端口号连接到服务器。

// 参数:  
// - address:目标服务器的 IP 地址。  
// - port:目标服务器的端口号。  
// - openMode:通信模式,默认值为 ReadWrite(既可读也可写)。

3. 接收数据

qint64 read(char *data, qint64 maxSize);
// 从网络缓冲区读取最大 maxSize 字节的数据到 data 指向的内存中。

// 参数:  
// - data:指向用于存储接收数据的内存地址。  
// - maxSize:指定可接收的最大字节数。

// 返回值:  
// - 实际读取的数据字节数。
```cpp
QByteArray read(qint64 maxSize);
// 从网络缓冲区读取最大 maxSize 字节的数据,返回一个 QByteArray。

// 参数:  
// - maxSize:指定可接收的最大字节数。

// 返回值:  
// - 包含读取数据的 QByteArray。
```cpp
QByteArray readAll();
// 从网络缓冲区读取当前所有可用数据,返回一个 QByteArray。

// 返回值:  
// - 包含所有读取数据的 QByteArray。

4. 发送数据

qint64 write(const char *data, qint64 maxSize);
// 发送 data 指向的内存中前 maxSize 字节的数据。

// 参数:  
// - data:指向需要发送数据的内存地址。  
// - maxSize:指定发送的数据字节数。

// 返回值:  
// - 实际发送的数据字节数。
```cpp
qint64 write(const char *data);
// 发送 data 指向的字符串数据(以 \0 结束)。

// 参数:  
// - data:指向需要发送的字符串。

// 返回值:  
// - 实际发送的数据字节数。
```cpp
qint64 write(const QByteArray &byteArray);
// 发送 QByteArray 类型的数据。

// 参数:  
// - byteArray:需要发送的 QByteArray 数据。

// 返回值:  
// - 实际发送的数据字节数。

2.2.2 信号

1. 数据可用信号

[signal] void readyRead();
// 当接收到数据时发出此信号。
  • 说明: 收到 readyRead 信号后,可以调用 readreadAll 方法获取数据。

2. 连接成功信号

[signal] void connected();
// 调用 connectToHost() 并成功建立连接后发出此信号。

3. 断开连接信号

[signal] void disconnected();
// 当套接字断开连接时发出此信号。

2.3 服务器

服务器搭建流程

1、创建套接字服务器QTcpServer对象

2、通过QTcpServer对象设置监听,即:QTcpServer::listen()

3、基于QTcpServer::newConnection()信号检测是否有新的客户端连接

4、如果有新的客户端连接调用QTcpSocket *QTcpServer::nextPendingConnection()得到通信的套接字对象

5、使用通信的套接字对象QTcpSocket和客户端进行通信

#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    // 定义 server 对象
    my_Server = new QTcpServer(this);

    // 启动服务器监听
    my_Server->listen(QHostAddress::Any , 8899);

    // 检测连接
    connect(my_Server , &QTcpServer::newConnection , this , [=](){
        // 接受套接字
        my_Socket = my_Server->nextPendingConnection();

        qDebug() << "客户端连接成功" ;

        // 读取数据
        connect(my_Socket , &QTcpSocket::readyRead , this , [=](){
            QByteArray data = my_Socket->readAll();
            qDebug() << "接受数据" << data;

            // 回复客户端
            my_Socket->write("我收到啦!!!");
        });
        // 处理客户端断开
        connect(my_Socket , &QTcpSocket::disconnected , this , [=](){
            qDebug() << "客户端断开了" << my_Socket->peerAddress().toString();
            my_Socket->deleteLater();
        });
    });
}

Widget::~Widget()
{
    delete ui;
}
```c++
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTcpServer>
#include <QTcpSocket>
#include <QByteArray>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private:
    Ui::Widget *ui;
    // 创建 套接字 和 服务器 对象
    QTcpServer * my_Server;
    QTcpSocket * my_Socket;


};
#endif // WIDGET_H

2.4 客户端

客户端开发流程

创建通信的套接字类QTcpSocket对象

使用服务器端绑定的IP和端口连接服务器QAbstractSocket::connectToHost()

使用QTcpSocket对象和服务器进行通信

#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    Tcp_sok = new QTcpSocket(this);



    // 检测服务器回复
    connect(Tcp_sok , &QTcpSocket::readyRead , this , [=](){
        QByteArray recv_Msg = Tcp_sok->readAll();
        qDebug() << QString::fromUtf8(recv_Msg);
    });

    // 检测是否和服务器是否连接成功
    connect(Tcp_sok , &QTcpSocket::connected , this , [=](){
        qDebug() << "连接成功" ;
    });
    // 检测服务器是否和客户端断开连接
    connect(Tcp_sok , &QTcpSocket::disconnected , this , [=](){
        qDebug() << "断开连接";
    });

}

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


void Widget::on_pushButton_clicked()
{
    // 设定服务器地址
    Tcp_sok->connectToHost("127.0.0.1" , 8899);
    qDebug() << "启动连接";
}
```c++
#ifndef WIDGET_H
#define WIDGET_H

#include <QDebug>

#include <QWidget>
#include <QTcpSocket>
#include <QByteArray>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    void on_pushButton_clicked();

private:
    Ui::Widget *ui;
    QTcpSocket * Tcp_sok;

};
#endif // WIDGET_H

第三章 QT数据库编程

3.1 前提条件

在进行操作数据库之前,我们需要做一些准备工作

1、包含模块配置

QT += core gui sql

2、包含头文件

#include <QSqlDatabase>  // 用于数据库连接
#include <QSqlQuery>     // 用于执行 SQL 查询
#include <QSqlError>     // 用于处理数据库操作错误
#include <QVariant>      // 用于处理查询结果
#include <QDebug>        // 用于调试信息输出

3.2 常用 API

QSqlDatabase:负责连接数据库。

QSqlQuery:通过连接执行 SQL 查询。

QSqlError:捕获操作中的错误。

QVariant:保存和转换查询的结果。

3.2.1 QSqlDatabase

常用API

// 添加 SQLite 数据库驱动并创建数据库连接
QSqlDatabase::addDatabase(const QString &driver, const QString &connectionName = QString());

// 设置数据库文件路径
void QSqlDatabase::setDatabaseName(const QString &name);

// 打开数据库连接
bool QSqlDatabase::open();

// 关闭数据库连接
void QSqlDatabase::close();

// 检查是否连接成功
bool QSqlDatabase::isOpen();

示例代码

QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); // 添加 SQLite 驱动
db.setDatabaseName("example.db"); // 设置数据库文件路径
if (!db.open()) {
    qDebug() << "数据库连接失败:" << db.lastError().text(); // 输出错误信息
} else {
    qDebug() << "数据库连接成功!";
}

3.2.2 QSqlQuery

用于执行 SQL 查询和命令。

常用API

// 构造函数,绑定数据库连接
QSqlQuery::QSqlQuery(const QSqlDatabase &db = QSqlDatabase());

// 执行 SQL 语句
bool QSqlQuery::exec(const QString &query);

// 绑定参数(用于占位符语句)
void QSqlQuery::bindValue(const QString &placeholder, const QVariant &val);

// 遍历查询结果
bool QSqlQuery::next(); // 指向下一条记录,返回 false 表示到达末尾
QVariant QSqlQuery::value(int index) const; // 按列索引获取数据
QVariant QSqlQuery::value(const QString &name) const; // 按列名获取数据

示例代码

QSqlQuery query;

// 创建表
if (!query.exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")) {
    qDebug() << "创建表失败:" << query.lastError().text();
}

// 插入数据
query.prepare("INSERT INTO users (name, age) VALUES (:name, :age)");
query.bindValue(":name", "张三");
query.bindValue(":age", 25);
if (!query.exec()) {
    qDebug() << "插入数据失败:" << query.lastError().text();
}

// 查询数据
if (query.exec("SELECT * FROM users")) {
    while (query.next()) {
        int id = query.value("id").toInt();
        QString name = query.value("name").toString();
        int age = query.value("age").toInt();
        qDebug() << "ID:" << id << ", Name:" << name << ", Age:" << age;
    }
}

3.2.3 QSqlError

用于捕获和处理数据库操作中的错误。

常用API

// 获取错误信息
QString QSqlError::text() const;

// 判断错误类型
QSqlError::ErrorType QSqlError::type() const;

示例程序

QSqlError error = db.lastError();
if (error.isValid()) {
    qDebug() << "错误信息:" << error.text();
}

3.2.4 QVariant

用于保存和转换查询的结果。

常用API

// 转换为常见类型
int QVariant::toInt(bool *ok = nullptr) const;
QString QVariant::toString() const;

示例程序

if (query.exec("SELECT age FROM users WHERE name = '张三'")) {
    if (query.next()) {
        int age = query.value(0).toInt();
        qDebug() << "张三的年龄是:" << age;
    }
}

3.3 操作步骤

#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
#include <QVariant>
#include <QDebug>

int main() {
    // 1. 连接数据库
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    db.setDatabaseName("example.db");
    if (!db.open()) {
        qDebug() << "数据库连接失败:" << db.lastError().text();
        return -1;
    }
    qDebug() << "数据库连接成功!";

    // 2. 创建数据表
    QSqlQuery query;
    if (!query.exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")) {
        qDebug() << "创建表失败:" << query.lastError().text();
        return -1;
    }

    // 3. 插入数据
    query.prepare("INSERT INTO users (name, age) VALUES (:name, :age)");
    query.bindValue(":name", "张三");
    query.bindValue(":age", 25);
    if (!query.exec()) {
        qDebug() << "插入数据失败:" << query.lastError().text();
        return -1;
    }

    // 4. 查询数据
    if (query.exec("SELECT * FROM users")) {
        while (query.next()) {
            int id = query.value("id").toInt();
            QString name = query.value("name").toString();
            int age = query.value("age").toInt();
            qDebug() << "ID:" << id << ", Name:" << name << ", Age:" << age;
        }
    }

    // 5. 关闭数据库
    db.close();
    QSqlDatabase::removeDatabase(QSqlDatabase::defaultConnection);
    return 0;
}

3.4 示例程序

#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    // 1、连接数据库
    db = QSqlDatabase::addDatabase("QSQLITE"); // 使用 SQLite 驱动
    db.setDatabaseName("hqyj.db");            // 设置数据库文件路径
    if (!db.open()) {
        qDebug() << "数据库打开失败!" << db.lastError().text();
        return;
    }
    qDebug() << "连接成功!";

    // 初始化查询对象
    query = QSqlQuery(db);

    // 2、创建数据表
    if (!query.exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)")) {
        qDebug() << "创建表格失败" << query.lastError().text();
        return;
    }
    qDebug() << "数据表创建成功!";
}

Widget::~Widget()
{
    db.close(); // 关闭数据库连接
    delete ui;
}

// 插入数据
void Widget::on_Insert_Button_clicked()
{
    // 获取用户输入
    QString name = ui->lineEdit_name->text();
    int age = ui->lineEdit_age->text().toInt();

    // 准备 SQL 插入语句
    query.prepare("INSERT INTO users (name, age) VALUES (:name, :age)");
    query.bindValue(":name", name);
    query.bindValue(":age", age);

    // 执行插入操作
    if (!query.exec()) {
        qDebug() << "插入失败!" << query.lastError().text();
        return;
    }
    qDebug() << "数据插入成功!";
}

// 删除数据
void Widget::on_Delete_Button_clicked()
{
    // 获取用户输入
    QString name = ui->lineEdit_name->text();

    // 准备 SQL 删除语句
    query.prepare("DELETE FROM users WHERE name = :name");
    query.bindValue(":name", name);

    // 执行删除操作
    if (!query.exec()) {
        qDebug() << "删除失败!" << query.lastError().text();
        return;
    }

    // 检查受影响的行数
    if (query.numRowsAffected() > 0) {
        qDebug() << "删除成功!";
    } else {
        qDebug() << "没有找到匹配的记录,删除失败!";
    }
}

// 更新数据
void Widget::on_Update_Button_clicked()
{
    // 获取用户输入
    QString name = ui->lineEdit_name->text();
    int newAge = ui->lineEdit_age->text().toInt();

    // 准备 SQL 更新语句
    query.prepare("UPDATE users SET age = :age WHERE name = :name");
    query.bindValue(":age", newAge);
    query.bindValue(":name", name);

    // 执行更新操作
    if (!query.exec()) {
        qDebug() << "更新失败!" << query.lastError().text();
        return;
    }

    // 检查受影响的行数
    if (query.numRowsAffected() > 0) {
        qDebug() << "更新成功!";
    } else {
        qDebug() << "没有找到匹配的记录,更新失败!";
    }
}

// 查询数据
void Widget::on_fund_Button_clicked()
{
    // 执行查询
    if (!query.exec("SELECT * FROM users")) {
        qDebug() << "查询失败!" << query.lastError().text();
        return;
    }

    // 遍历查询结果
    while (query.next()) {
        int id = query.value("id").toInt();
        QString name = query.value("name").toString();
        int age = query.value("age").toInt();
        qDebug() << "ID:" << id << ", Name:" << name << ", Age:" << age;
    }
}
```c++
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
#include <QVariant>
#include <QDebug>

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr); // 构造函数
    ~Widget(); // 析构函数

private slots:
    void on_Insert_Button_clicked();  // 插入数据
    void on_Delete_Button_clicked(); // 删除数据
    void on_Update_Button_clicked(); // 更新数据
    void on_fund_Button_clicked();   // 查询数据

private:
    Ui::Widget *ui;
    QSqlDatabase db;  // 数据库连接
    QSqlQuery query;  // 查询对象
};

#endif // WIDGET_H

第四章 QT-Json

4.1 Json 概述

4.1.1 JSON 的引入

在讲解 JSON 之前, 首先想这么一种场景:

通常,客户端和服务端要进行通信,那么通信采用什么数据格式呢?

比如 C++ 写的服务端,创建了一个 Person 对象:

class Person {
    string name;
    string gender;
    int age;
};

怎么将服务端创建的 Person 对象,传递到客户端呢?

直接传 Person 对象肯定是不合适的,因为客户端可能甚至不是 C++ 写的,可能是 Java 写的,Java 不认识 C++ 中的对象。

更有甚者,客户端是一个单片机的设备,是用 C 语言写的,C 语言是面向过程的,压根就没有类和对象的概念

此时就需要一种通用的数据格式,JSON 就应运而生了。

JSON(JavaScript Object Notation),中文名 JS对象表示法,因为它和 JS 中的对象的写法很类似

通常说的 JSON,其实就是 JSON 字符串,本质上是一种特殊格式的字符串。

JSON 是一种轻量级的数据交换格式,客户端和服务端数据交互,基本都是 JSON 格式的。

4.1.2 JSON 的特点

相对于其他数据传输格式,JSON 有以下特点

(1)便于阅读和书写

除了 JSON 格式,还有一种数据传输格式 XML,相对于 XMLJSON更加便于阅读和书写

(2)独立于编程语言

JSON 完全独立于编程语言,并不是说名字里有 JavaScript,就只能在 JavaScript 中使用,不是这样的

JSONJavaScript 的关系,就类似于:雷锋和雷峰塔、周杰和周杰伦、范伟和范玮琪,可以说没啥关系

几乎在所有的编程语言和开发环境中,都有解析和生成 JSON 字符串的库,比如:

C 语言

​ Jansson、cJSON

✅ C++

​ jsonCpp、JSON for Modern C++

✅ Java

​ json-lib、org-json

✅ Android

​ GSON、FastJson

✅ QT

​ QJsonXxx

(3)网络传输的标准数据格式

基于以上的特点,JSON 成为网络传输使用率最高的数据格式

4.2 JSON 的数据格式

json 有两种数据格式

  • json对象
  • json数组

规则:被大括号包裹的是 JSON 对象; 被中括号包裹的是 JSON 数组

4.1.2 Json数组

[元素1, 元素2, 元素3, … 元素n]

类似于 C/C++ 中的数组,元素之间以逗号分隔

不同的是,JSON 数组中的元素可以是不同的数据类型,包括:整形、 浮点、 字符串、 布尔类型、 JSON 数组、 JSON 对象、空值

1、JSON数组中的元素是同一类型

// 元素类型都是数字
[1, 2, 3, 4]

// 元素类型都是字符串
["Spring", "Summer", "Autumn", "Winter"]

2、 JSON数组中的元素是不同类型

// 元素类型分别是:整型、浮点、字符串、布尔、空值
[1, 2.5, "hello", true, false, null]

3、 Json数组嵌套

// 数组中嵌套数组
[
    [1, 2, 3, 4],
    ["Spring", "Summer", "Autumn", "Winter"],
    [1, 2.5, "hello", true, false, null]
]

4、JSON 数组嵌套 JSON对象

// 数组中嵌套对象
[
    {
        "name": "Tom", 
        "age": 18, 
        "gender": "male"
    }, 
    {
        "name": "Tom", 
        "age": 18, 
        "gender": "male"
    }
]

4.1.3 Json对象

{ 
    "key1" : value1, 
    "key2" : value2,
    "key3" : value3
}

JSON 对象内部使用键值对的方式来组织;

键和值之间使用冒号分隔,多个键值之间使用逗号分隔;

键是字符串类型,值的类型可以是:整形、 浮点、 字符串、 布尔类型、 JSON 数组、 JSON 对象、 空值

1、 最简单的 JSON 对象

{
    "name": "Tom", 
    "age": 18, 
    "gender": "male"
}

2、 JSON 对象和 JSON 数组嵌套

JSON 对象中,还可以嵌套 JSON 对象和 JSON 数组

{
    "name": "China",
    "info": {
        "capital": "beijing",
        "asian": true,
        "founded": 1949
    },
    "provinces": [{
        "name": "shandong",
        "capital": "jinan"
    }, {
        "name": "zhejiang",
        "capital": "hangzhou"
    }]
}

4.3 Json 在线解析

JSON 本质就是一种特殊格式的字符串

实际工作中,这个 JSON 字符串可能是自己手写的,也可能是来自网络接收的数据

如下是一段 JSON 字符串,它可能是我们自己写的,也可能是服务端返回的

它是压缩格式,也就是没有换行和缩进,不方便判断格式是否正确

{"name":"China","info":{"capital":"beijing","asian":true,"founded":1949},"provinces":[{"name":"shandong","capital":"jinan"},{"name":"zhejiang","capital":"hangzhou"}]}

在线解析网站 :https://www.json.cn/#gsc.tab=0

当然自己找一个也是没有问题的

解析的效果如下

image-20241121003040506

4.3 Json Qt

我们在进行 Json 操作中 常用的类有以下几个

QJsonObject

QJsonArray

QJsonValue

QJsonDocument

4.3.1 QJsonObject

QJsonObject 封装了 JSON 中的对象,可以存储多个键值对。

其中,键为字符串类型,值为 QJsonValue 类型。

常用api介绍

// 构造函数
QJsonObject::QJsonObject();

// 将键值对添加到 QJsonObject 对象中
QJsonObject::iterator insert(const QString &key, const QJsonValue &value);

// 获取 QJsonObject 对象中键值对的个数
int QJsonObject::count() const;
int QJsonObject::size() const;
int QJsonObject::length() const;

// 通过 key 得到 value
QJsonValue QJsonObject::value(const QString &key) const;
QJsonValue QJsonObject::operator[](const QString &key) const;

// 检查 key 是否存在
iterator QJsonObject::find(const QString &key);
bool QJsonObject::contains(const QString &key) const;

// 遍历 key
QStringList QJsonObject::keys() const;

4.3.2 QJsonArray

QJsonArray 封装了 Json 中的数组。

数组中元素的类型统一为 QJsonValue 类型

构造函数

QJsonArray::QJsonArray();

添加数组元素

// 添加到头部和尾部
void QJsonArray::append(const QJsonValue &value);
void QJsonArray::prepend(const QJsonValue &value);

// 插入到 i 的位置之前
void QJsonArray::insert(int i, const QJsonValue &value); 

// 添加到头部和尾部
void QJsonArray::push_back(const QJsonValue &value);
void QJsonArray::push_front(const QJsonValue &value);

获取 QJsonArray 中元素个数

int QJsonArray::count() const;
int QJsonArray::size() const;

获取元素的值

// 获取头部和尾部
QJsonValue QJsonArray::first() const;
QJsonValue QJsonArray::last() const;

// 获取指定位置
QJsonValue QJsonArray::at(int i) const;
QJsonValueRef QJsonArray::operator[](int i);

删除元素

// 删除头部和尾部
void QJsonArray::pop_back();
void QJsonArray::pop_front();

void QJsonArray::removeFirst();
void QJsonArray::removeLast();

// 删除指定位置
void QJsonArray::removeAt(int i);
QJsonValue QJsonArray::takeAt(int i);

4.3.3 QJsonValue

它封装了 JSON 支持的六种数据类型,分别为:

| 数据类型 | Qt 类 |
| ——————– | —————— |
| 布尔类型 | QJsonValue::Bool |
| 浮点类型(包括整形) | QJsonValue::Double |
| 字符串类型 | QJsonValue::String |
| 数组类型 | QJsonValue::Array |
| 对象类型 | QJsonValue::Object |
| 空值类型 | QJsonValue::Null |

构造函数

// 字符串
QJsonValue(const char *s);
QJsonValue(QLatin1String s);
QJsonValue(const QString &s);

// 整形 and 浮点型
QJsonValue(qint64 v);
QJsonValue(int v);
QJsonValue(double v);

// 布尔类型
QJsonValue(bool b);

// Json对象
QJsonValue(const QJsonObject &o);

// Json数组
QJsonValue(const QJsonArray &a);

// 空值类型
QJsonValue(QJsonValue::Type type = Null);

判断类型

// 是否是字符串类型
bool isString() const;

// 是否是浮点类型(整形也是通过该函数判断)
bool isDouble() const;

// 是否是布尔类型
bool isBool() const;

// 是否是Json对象
bool isObject() const;

// 是否是Json数组
bool isArray() const;

// 是否是未定义类型(无法识别的类型)
bool isUndefined() const;

// 是否是空值类型
bool isNull() const;

通过以上判断函数,获取到其内部数据的实际类型之后,如果有需求就可以再次将其转换为对应的基础数据类型,对应的 API 函数如下:

// 转换为字符串类型
QString toString() const;
QString toString(const QString &defaultValue) const;

// 转换为浮点类型
double toDouble(double defaultValue = 0) const;
// 转换为整形
int toInt(int defaultValue = 0) const;

// 转换为布尔类型
bool toBool(bool defaultValue = false) const;

// 转换为Json对象
QJsonObject toObject(const QJsonObject &defaultValue) const;
QJsonObject toObject() const;

// 转换为Json数组
QJsonArray toArray(const QJsonArray &defaultValue) const;
QJsonArray toArray() const;

4.3.4 QJsonDocument

它封装了一个完整的 JSON 文档。

它可以从 UTF-8 编码的基于文本的表示,以及 Qt 本身的二进制格式,读取和写入该文档。

QJsonObjectQJsonArray 这两个对象是不能直接转换为字符串类型的,需要通过 QJsonDocument 类来完成二者的转换

下面介绍转换的步骤

QJsonObject / QJsonArray => 字符串

// 1. 创建 QJsonDocument 对象
// 以 QJsonObject 或者 QJsonArray 为参数来创建 QJsonDocument 对象
QJsonDocument::QJsonDocument(const QJsonObject &object);
QJsonDocument::QJsonDocument(const QJsonArray &array);

// 2. 将 QJsonDocument 对象中的数据进行序列化
// 通过调用 toXXX() 函数就可以得到文本格式或者二进制格式的 Json 字符串了。
QByteArray QJsonDocument::toBinaryData() const;                            // 二进制格式的json字符串
QByteArray QJsonDocument::toJson(JsonFormat format = Indented) const;      // 文本格式

// 3. 使用得到的字符串进行数据传输,或者保存到文件

字符串 => QJsonObject / QJsonArray

通常,通过网络接收或者读取磁盘文件,会得到一个 JSON 格式的字符串,之后可以按照如下步骤,解析出 JSON 字符串中的一个个字段

// 1. 将 JSON 字符串转换为 QJsonDocument 对象
[static] QJsonDocument QJsonDocument::fromBinaryData(const QByteArray &data, DataValidation validation = Validate);
[static] QJsonDocument QJsonDocument::fromJson(const QByteArray &json, QJsonParseError *error = Q_NULLPTR);

// 2. 将文档对象转换为 json 数组 / 对象

// 2.1 判断文档对象中存储的数据,是 JSON 数组还是 JSON 对象
bool QJsonDocument::isArray() const;
bool QJsonDocument::isObject() const

// 2.2 之后,就可以转换为 JSON 数组或 JSON 对象
QJsonObject QJsonDocument::object() const;
QJsonArray QJsonDocument::array() const;

// 3. 调用 QJsonArray / QJsonObject 类提供的 API 获取存储在其中的数据

4.4 构建JSON字符串

在网络传输时,通常是传输的 JSON 字符串

传输之前,首先要生成 JSON 字符串,接下来使用 Qt 提供的工具类,来生成如下格式的 JSON 字符串

{
    "name": "China",
    "info": {
        "capital": "beijing",
        "asian": true,
        "founded": 1949
    },
    "provinces": [{
        "name": "shandong",
        "capital": "jinan"
    }, {
        "name": "zhejiang",
        "capital": "hangzhou"
    }]
}
```c++
void writeJson() {
    QJsonObject rootObj;

    // 1. 插入 name 字段
    rootObj.insert("name", "China");

    // 2. 插入 info 字段
    QJsonObject infoObj;
    infoObj.insert("capital", "beijing");
    infoObj.insert("asian", true);
    infoObj.insert("founded", 1949);
    rootObj.insert("info", infoObj);

    // 3. 插入 prvince 字段
    QJsonArray provinceArray;

    QJsonObject provinceSdObj;
    provinceSdObj.insert("name", "shandong");
    provinceSdObj.insert("capital", "jinan");

    QJsonObject provinceZjObj;
    provinceZjObj.insert("name", "zhejiang");
    provinceZjObj.insert("capital", "hangzhou");

    provinceArray.append(provinceSdObj);
    provinceArray.append(provinceZjObj);

    rootObj.insert("provinces", provinceArray);

    // 4.转换为 json 字符串
    QJsonDocument doc(rootObj);
    QByteArray json = doc.toJson();

    // 5.1 打印json字符串
    qDebug() << QString(json).toUtf8().data();

    // 5.2 将json字符串写到文件
    QFile file("d:\\china.json");
    file.open(QFile::WriteOnly);
    file.write(json);
    file.close();
}

4.5 解析JSON字符串

void fromJson() {
    QFile file("d:\\china.json");
    file.open(QFile::ReadOnly);
    QByteArray json = file.readAll();
    file.close();

    QJsonDocument doc = QJsonDocument::fromJson(json);
    if (!doc.isObject()) {
        qDebug() << "not an object";
        return;
    }

    QJsonObject obj = doc.object();
    QStringList keys = obj.keys();
    for (int i = 0; i < keys.size(); ++i) {
        // 获取key-value
        QString key = keys[i];
        QJsonValue value = obj.value(key);

        if (value.isBool()) {
            qDebug() << key << ":" << value.toBool();
        } else if (value.isString()) {
            qDebug() << key << ":" << value.toString();
        } else if (value.isDouble()) {
            qDebug() << key << ":" << value.toInt();
        } else if (value.isObject()) {
            qDebug() << key << ":";

            QJsonObject infoObj = value.toObject();
#if 0
            // 简便起见,直接使用键来取值
            QString capital = infoObj["capital"].toString();
            bool asian = infoObj["asian"].toBool();
            int founded = infoObj["founded"].toInt();

            qDebug() << "    "
                     << "capital"
                     << ":" << capital;
            qDebug() << "    "
                     << "asian"
                     << ":" << asian;
            qDebug() << "    "
                     << "founded"
                     << ":" << founded;
#else
            QStringList infoKeys = infoObj.keys();
            for (int i = 0; i < infoKeys.size(); i++) {
                QJsonValue v = infoObj.value(infoKeys[i]);
                if (v.isBool()) {
                    qDebug() << "    " << infoKeys[i] << ":" << v.toBool();
                } else if (v.isDouble()) {
                    qDebug() << "    " << infoKeys[i] << ":" << v.toInt();
                } else if (v.isString()) {
                    qDebug() << "    " << infoKeys[i] << ":" << v.toString();
                }
            }
#endif
        } else if (value.isArray()) {
            qDebug() << key;

            QJsonArray provinceArray = value.toArray();
#if 1
            // 简便起见,直接使用键来取值
            for (int i = 0; i < provinceArray.size(); i++) {
                QJsonObject provinceObj = provinceArray[i].toObject();

                QString name = provinceObj["name"].toString();
                QString capital = provinceObj["capital"].toString();

                qDebug() << "    "
                         << "name"
                         << ":" << name << ", capital"
                         << ":" << capital;
            }
#else
            for (int i = 0; i < provinceArray.size(); i++) {
                QJsonObject provinceObj = provinceArray[i].toObject();
                QStringList provinceKeys = provinceObj.keys();
                for (int j = 0; j < provinceKeys.size(); j++) {
                    // 简便起见,不再进行类型判断
                    QJsonValue infoValue = provinceObj.value(provinceKeys[j]);
                    qDebug() << "   " << provinceKeys[j] << ":" << infoValue.toString();
                }
            }
#endif
        }
    }
}

第五章 QT-HTTP

5.1 HTTP基础

5.1.1 HTTP 必知必会

既然是实现 HTTP 协议的天气预报,那么 HTTP 相关的知识,必须掌握

HTTP:超文本传输协议(HyperText Transfer Protocol)

HTTP 是浏览器端 Web 通信的基础。

关于 HTTP 的介绍,不论是书本还是网上的博客,介绍的过于学术化,这里以实战出发,介绍几个关键的点:

5.1.2 两种架构

B/S 架构

  • Browser/Server,浏览器/服务器架构
  • B:浏览器,比如 FirefoxInternet ExplorerGoogle ChromeSafariOpera
  • S:服务器,比如 Apachenginx

C/S 架构

  • Client/Server,客户端/服务器架构

B/S 架构相对于 C/S 架构,客户机上无需安装任何软件,使用浏览器即可访问服务器

因此,越来越多的 C/S 架构正被 B/S 架构所替代

5.1.3 基于请求响应的模式

HTTP 协议永远都是客户端发起请求,服务器做出响应

也就是说,请求必定是先从客户端发起的,服务器端在没有接收到请求之前不会发送任何响应

这就无法实现这样一种场景:服务端主动推送消息给客户端

image-20241121012335576

5.1.4 无状态

当浏览器第一次发送请求给服务器时,服务器做出了响应;

当浏览器第二次发送请求给服务器时,服务器同样可以做出响应,但服务器并不知道第二次的请求和第一次来自同一个浏览器

也就是说,服务器是不会记住你是谁的,所以是无状态的。

而如果要使 HTTP 协议有状态,就可以使浏览器访问服务器时,加入 cookie

这样,只要你在请求时有了这个 cookie,服务器就能够通过 cookie 知道,你就是之前那个浏览器

这样的话,HTTP 协议就有状态了。

5.1.4 请求报文

请求报文由四部分组成:

请求行 + 请求头(请求首部字段) + 空行 + 实体

1、请求行

请求行里面有:

  • 请求方法:比如 GETPOST
  • 资源对象(URI
  • 协议名称和版本号(HTTP/1.1
POST /custom/a2873925c34ecbd2/web/cstm?stm=1649149234039 HTTP/1.1

POST                                                    即请求方法
/custom/a2873925c34ecbd2/web/cstm?stm=1649149234039     即 URL
HTTP/1.1                                                即协议和版本

2、请求头

请求首部字段,请求头由于告诉服务器该请求的一些信息,起到传递额外信息的目的

Host: api.growingio.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Content-Length: 264
Origin: https://leetcode-cn.com
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: cross-site
Referer: https://leetcode-cn.com/

3、空行

用来区分开请求头和请求实体。

4、请求实体

请求实体即真正所需要传输的数据

5.1.5 响应报文

响应报文同样是由四部分组成:

状态行 + 响应头 + 空行 + 消息体

1、状态行

状态行主要由三部分组成:

  • HTTP 版本
  • 状态码(表示相应的结果)
  • 原因短语(解释)
HTTP/2 200 OK

HTTP/2         协议和版本
200            状态码 - 200代表OK,表示请求成功,404代表NOT FOUND,表示请求失败,所请求资源未在服务器上发现。
OK             原因短语

2、响应头

响应报文首部,和请求报文首部一样,响应报文首部同样是为了传递额外信息,例如:

date: Tue, 05 Apr 2024 10:48:17 GMT              // 响应时间
content-type: application/json                   // 响应格式
content-length: 127                              // 长度
strict-transport-security: max-age=31536000
X-Firefox-Spdy: h2

3、空行

用来区分开响应实体和响应首部

4、响应实体

真正存储响应信息的部分

5.1.6 请求方式

HTTP 常用的请求方式有很多中,最常用的是 GETPOST

二者最主要的区别就是:

GET 请求的参数位于 URL 中,会显示在地址栏上

POST 请求的参数位于 request body 请求体中。

因此,GET 请求的安全性不如 POST 请求,并且 GET 请求的参数有长度限制,而 POST 没有

5.2 Postman

HTTP 包含客户端和服务端,在讲解 Postman 之前,试想下面的两种情况

  • 服务端开发完毕,而客户端还未完成开发。此时服务端开发人员能否对自己写的服务端程序进行测试呢?
  • 客户端开发人员访问服务端出错,比如无法访问服务端,有没有第三方的测试工具做进一步验证呢?

此时,就用到 Postman

Postman 是一个接口测试工具,使用 HTTP 开发的人几乎是无人不知的

它主要是用来模拟各种 HTTP 请求的(比如 GET 请求、POST 请求等)

在做接口测试的时候,Postman 相当于客户端,它可模拟用户发起的各类 HTTP 请求,将请求数据发送至服务端,并获取对应的响应结果

接下来,以获取天气北京的天气为例,来介绍 Postman 的基本使用

5.2.1 安装

首先要安装 Postman

登录 www.postman.com 根据自己的系统,下载对应的版本,并一步步傻瓜式安装即可

5.1.2 发送请求

安装成功后,就可以使用它来发送请求了

这里以获取北京的天气为例

获取北京天气的 URL 为:http://t.weather.itboy.net/api/weather/city/101010100

其中,101010100 是北京的城市编码,是 9 位的

image-20241121013505715

上图的序号就是操作的具体步骤:

① 点击 “+”,即可打开一个用于发送请求的标签页

② 选择请求的方法(GET/POST),并输入请求的 URL

③ 点击【Params】,可以添加参数

④ 点击【Headers】,可以添加请求头

⑤ 点击【Send】,发送该请求到服务器

Body,响应体,也就是服务器返回的实际数据。响应体中选择右侧的【JSON】格式,并选择【Pretty】,可以对 JSON 数据做美化显示

Headers,响应头,也就是服务器返回的响应头数据

5.3 QT 获取 HTTP

5.3.1 常用类

QNetworkAccessManager

QNetworkRequest

QNetworkReply

QUrl

QUrlQuery

5.3.2 操作方法

1、创建 “网络访问管理” 对象

首先需要创建一个 QNetworkAccessManager 对象,这是 Qt 中进行 HTTP 请求的开端

mNetAccessManager = new QNetworkAccessManager(this);

2、连接对象

在发送 HTTP 请求之前,先关联信号槽

// 获取到数据之后
connect(mNetAccessManager, &QNetworkAccessManager::finished, this, &MainWindow::onReplied);

当请求结束,获取到服务器的数据时,mNetAccessManager 会发射一个 finished 信号,该信号携带一个 QNetworkReply 的参数

服务器返回的所有数据就封装在其中,通过 QNetworkReply 类提供的各种函数,就可以获取响应头,响应体等各种数据。

3、发送请求

接下来就可以发送 HTTP 请求了

QUrl url("http://t.weather.itboy.net/api/weather/city/101010100");
mNetAccessManager->get(QNetworkRequest(url));

根据请求的地址构建出一个 QUrl 对象,然后直接调用 QNetworkAccessManagerget 函数,即可发送一个 GET 请求

5、接收数据

由于上面绑定了信号槽,服务器返回数据后,自动调用我们自定义的槽函数 onReplied

如下是 onReplied 函数的标准写法:

void MainWindow::onReplied(QNetworkReply* reply)
{
    // 响应的状态码为200, 表示请求成功
    int status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();

    qDebug() << "operation:" << reply->operation();         // 请求方式
    qDebug() << "status code:" << status_code;              // 状态码
    qDebug() << "url:" << reply->url();                     // url
    qDebug() << "raw header:" << reply->rawHeaderList();    // header

    if ( reply->error() != QNetworkReply::NoError || status_code != 200 ) {
        QMessageBox::warning(this, "提示", "请求数据失败!", QMessageBox::Ok);
    } else {
        //获取响应信息
        QByteArray reply_data = reply->readAll();
        QByteArray byteArray  = QString(reply_data).toUtf8();
        qDebug() << "read all:" << byteArray.data();

        // parseJson()
    }

    reply->deleteLater();
}