07-多线程网络多媒体与数据库.md 91 KB


title: 多线程、网络、多媒体与数据库 tags: [Qt, QThread, moveToThread, QMutex, QTcpServer, QTcpSocket, QUdpSocket, 多播, QHostInfo, QNetworkAccessManager, QMediaPlayer, QSound, QAudioRecorder, QtSQL, SQLite, QSqlTableModel, IMX6ULL, 嵌入式Qt] created: 2026-09-18 updated: 2026-09-18

pdf_ref: "《I.MX6U嵌入式Qt开发指南V1.2》第十章 多线程、第十一章 网络编程、第十二章 多媒体、第十三章 数据库"

多线程、网络、多媒体与数据库

💡 关联知识:[[05-Qt应用开发/06-文本绘图与图表]]、[[05-Qt应用开发/08-嵌入式Qt交叉编译与项目实战]]、[[05-Qt应用开发/07-多线程网络多媒体与数据库]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/15. TCP编程]]

本篇是 PDF 第二篇提高篇的最后一组内容,把四块「让界面能用起来」的能力串在一起:

  1. 多线程——程序默认只有一个主线程,耗时操作会冻结界面;用 QThread 把耗时操作放到子线程;
  2. 网络编程——用 QTcpSocket/QTcpServer/QUdpSocket 做 TCP/UDP 通信,用 QHostInfo 查本机信息,用 QNetworkAccessManager 下载文件;
  3. 多媒体——用 QSound 播音效、QMediaPlayer 播音乐和视频、QAudioRecorder 录音;
  4. 数据库——用 Qt SQL 模块连接 SQLite,通过 QSqlQuery 执行 SQL、QSqlTableModel/QSqlQueryModel 把表映射到界面。

对应例程:Qt/02/05_qthread_example1 ~ Qt/02/18_sqlite_table,共 14 个。桌面端与 I.MX6ULL 开发板的界面代码完全一致,差异只在交叉编译环境与可用的多媒体插件/解码器。


1. 多线程

1.1 为什么需要多线程

应用程序跑起来后一般情况下只有一个线程,就是主线程,线程内的操作都是顺序执行的。假设界面上某个操作比较耗时,例如在嵌入式 CPU 上拍照保存照片(嵌入式 CPU 主频通常远低于 PC,处理速度也慢):

  • 界面点击后,耗时操作没完成前,界面会冻结、无法响应;
  • 直到操作完成才恢复正常。

用户体验很差。解决办法是创建一个单独的线程来执行耗时操作,主线程继续负责界面刷新。

官方文档:QThread 类提供了一种独立于平台的方法来管理线程。QThread 对象在程序中管理一个控制线程,线程在 run() 中开始执行。默认情况下 run() 通过调用 exec() 启动事件循环,并在线程中运行 Qt 事件循环。也可以通过 QObject::moveToThread() 把 worker 对象移动到线程中使用。

1.2 两种多线程方法

QThread 是 Qt 实现多线程的核心类。Qt 有两种多线程写法:

方法 核心思想 说明
继承 QThread,重写 run() 子类中只有 run() 在新线程执行 Qt 4.8 之前的主流写法
继承 QObjectmoveToThread() 整个 QObject 对象移到线程里执行 Qt 4.8 之后官方推荐,更灵活

Qt 官方从 4.8 之后建议使用第二种方法。两种方法区别不大,但继承 QObject 的方法更灵活,所以 Qt 帮助文档里先给继承 QObject 的写法,再给继承 QThread 的写法。

关键区别:继承 QThread 时,只有 run() 在子线程里,类内其他方法仍在主线程;继承 QObject 并用 moveToThread() 后,该对象的所有槽函数都在子线程里执行,因此可以有 doWork1()doWork2() 等多份耗时工作。

flowchart LR
    subgraph 继承QThread["方法一:继承 QThread"]
        A1["主线程<br/>fun1() fun2() ..."] --- A2["子线程<br/>仅 run()"]
    end
    subgraph 继承QObject["方法二:继承 QObject + moveToThread"]
        B1["主线程<br/>fun1() fun2() ..."] -->|"发出信号触发槽"| B2["QThread 线程<br/>doWork1() doWork2() ...<br/>(整个对象移入)"]
    end

另外 Qt 提供了 QMutexQMutexLockerQReadLockerQWriteLocker 等类用于线程之间的同步。

1.3 继承 QThread 的线程

例程 05_qthread_example1。通过点击按钮开启线程,线程执行完成时发送 resultReady(const QString &s) 信号给主线程。

1.3.1 mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QThread>
#include <QDebug>
#include <QPushButton>

/* 使用下面声明的 WorkerThread 线程类 */
class WorkerThread;

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

private:
    /* 在 MainWindow 类里声明对象 */
    WorkerThread *workerThread;

    /* 声明一个按钮,使用此按钮点击后开启线程 */
    QPushButton *pushButton;

private slots:
    /* 槽函数,用于接收线程发送的信号 */
    void handleResults(const QString &result);

    /* 点击按钮开启线程 */
    void pushButtonClicked();
};

/* 新建一个 WorkerThread 类继承于 QThread */
class WorkerThread : public QThread
{
    /* 用到信号槽即需要此宏定义 */
    Q_OBJECT

public:
    WorkerThread(QWidget *parent = nullptr) {
        Q_UNUSED(parent);
    }

    /* 重写 run 方法,继承 QThread 的类,只有 run 方法是在新的线程里 */
    void run() override {
        QString result = "线程开启成功";

        /* 这里写上比较耗时的操作 */
        // ...
        // 延时 2s,把延时 2s 当作耗时操作
        sleep(2);

        /* 发送结果准备好的信号 */
        emit resultReady(result);
    }

signals:
    /* 声明一个信号,译结果准确好的信号 */
    void resultReady(const QString &s);
};

#endif // MAINWINDOW_H

1.3.2 mainwindow.cpp

#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    /* 设置位置与大小 */
    this->setGeometry(0, 0, 800, 480);

    /* 对象实例化 */
    pushButton = new QPushButton(this);
    workerThread = new WorkerThread(this);

    /* 按钮设置大小与文本 */
    pushButton->resize(100, 40);
    pushButton->setText("开启线程");

    /* 信号槽连接 */
    connect(workerThread, SIGNAL(resultReady(QString)),
            this, SLOT(handleResults(QString)));
    connect(pushButton, SIGNAL(clicked()),
            this, SLOT(pushButtonClicked()));
}

MainWindow::~MainWindow()
{
    /* 进程退出,注意本例 run() 方法没写循环,此方法需要有循环才生效 */
    workerThread->quit();

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

void MainWindow::handleResults(const QString &result)
{
    /* 打印出线程发送过来的结果 */
    qDebug() << result << endl;
}

void MainWindow::pushButtonClicked()
{
    /* 检查线程是否在运行,如果没有则开始运行 */
    if (!workerThread->isRunning())
        workerThread->start();
}

1.3.3 逐段解释

  • QThread 对象管理线程:Qt 使用 C++ 基本是对象编程,线程也不例外,用对象来管理线程。
  • run() override:把耗时操作写在这里。本例相当于一个继承 QThread 线程的模板。注意只有 run() 在新线程里。
  • sleep(2)QThread 提供的静态保护方法,此处用 2s 延时模拟耗时操作。
  • emit resultReady(result):在子线程内发射信号,主线程接收后执行 handleResults()。信号槽是线程安全的跨线程通信方式。
  • workerThread->start()start() 会调用重写的 run(),启动新线程。用 isRunning() 判断,避免 2s 内重复点击重复启动。
  • 析构里 quit() + wait(2000)quit() 让线程事件循环退出;wait() 阻塞等待线程结束,返回 true 表示正常结束。本例 run() 没有循环,所以 quit() 对其影响有限,主要是演示退出流程。

1.3.4 运行效果

点击「开启线程」按钮后延时 2s,应用程序输出窗口打印「线程开启成功」。2s 内多次点击不会重复开启线程(线程还在运行),且点击按钮没有卡顿——延时操作在子线程里,按钮在主线程里。

关闭程序后,子线程在主线程析构函数里退出,等待 2s 后打印「线程已经结束」。

1.4 继承 QObject 的线程(moveToThread)

例程 06_qthread_example2。点击按钮开启线程,另一个按钮点击关闭线程,并通过加锁安全地终止一个耗时循环。

为什么需要加锁来终止线程? quit()exit() 都不会中途终止线程;要马上终止可以用 terminate(),但该函数非常不安全,Qt 官方文档不推荐。因此通常加一个 bool 变量,由主线程修改它来通知子线程退出循环。由于这个 bool 变量会被两个线程访问,可能引起访问冲突,所以需要加锁。加锁会消耗一定性能、增加耗时,但保证了安全。

flowchart TB
    M["主线程 MainWindow"] -->|"startWork(QString) 信号"| W
    subgraph T["workerThread 线程"]
        W["worker 对象(继承 QObject,已 moveToThread)"]
        W --> D1["耗时的 doWork1()"]
        W --> D2["doWork2() ..."]
    end
    W -->|"resultReady(QString) 信号"| M
    M -->|"worker->stopWork() 直接调用,加锁修改标志位"| W

1.4.1 mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QThread>
#include <QDebug>
#include <QPushButton>
#include <QMutexLocker>
#include <QMutex>

/* 工人类 */
class Worker;

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

private:
    /* 开始线程按钮 */
    QPushButton *pushButton1;

    /* 打断线程按钮 */
    QPushButton *pushButton2;

    /* 全局线程 */
    QThread workerThread;

    /* 工人类 */
    Worker *worker;

private slots:
    /* 按钮1 点击开启线程 */
    void pushButton1Clicked();

    /* 按钮2 点击打断线程 */
    void pushButton2Clicked();

    /* 用于接收工人是否在工作的信号 */
    void handleResults(const QString &);

signals:
    /* 工人开始工作(做些耗时的操作 ) */
    void startWork(const QString &);
};

/* Worker 类,这个类声明了 doWork1 函数,将整个 Worker 类移至线程 workerThread */
class Worker : public QObject
{
    Q_OBJECT

private:
    /* 互斥锁 */
    QMutex lock;

    /* 标志位 */
    bool isCanRun;

public slots:
    /* 耗时的工作都放在槽函数下,工人可以有多份不同的工作,但是每次只能去做一份 */
    void doWork1(const QString &parameter) {

        /* 标志位为真 */
        isCanRun = true;

        /* 死循环 */
        while (1) {
            /* 此 {} 作用是 QMutexLocker 与 lock 的作用范围,获取锁后,
             * 运行完成后即解锁 */
            {
                QMutexLocker locker(&lock);
                /* 如果标志位不为真 */
                if (!isCanRun) {
                    /* 跳出循环 */
                    break;
                }
            }
            /* 使用 QThread 里的延时函数,当作一个普通延时 */
            QThread::sleep(2);

            emit resultReady(parameter + "doWork1 函数");
        }
        /* doWork1 运行完成,发送信号 */
        emit resultReady("打断doWork1 函数");
    }

    // void doWork2();...

public:
    /* 打断线程(注意此方法不能放在槽函数下) */
    void stopWork() {
        qDebug() << "打断线程" << endl;

        /* 获取锁后,运行完成后即解锁 */
        QMutexLocker locker(&lock);
        isCanRun = false;
    }

signals:
    /* 工人工作函数状态的信号 */
    void resultReady(const QString &result);
};
#endif // MAINWINDOW_H

1.4.2 mainwindow.cpp

#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    /* 设置显示位置与大小 */
    this->setGeometry(0, 0, 800, 480);
    pushButton1 = new QPushButton(this);
    pushButton2 = new QPushButton(this);

    /* 设置按钮的位置大小 */
    pushButton1->setGeometry(300, 200, 80, 40);
    pushButton2->setGeometry(400, 200, 80, 40);

    /* 设置两个按钮的文本 */
    pushButton1->setText("开启线程");
    pushButton2->setText("打断线程");

    /* 工人类实例化 */
    worker = new Worker;

    /* 将 worker 类移至线程 workerThread */
    worker->moveToThread(&workerThread);

    /* 信号槽连接 */

    /* 线程完成销毁对象 */
    connect(&workerThread, SIGNAL(finished()),
            worker, SLOT(deleteLater()));
    connect(&workerThread, SIGNAL(finished()),
            &workerThread, SLOT(deleteLater()));

    /* 发送开始工作的信号,开始工作 */
    connect(this, SIGNAL(startWork(QString)),
            worker, SLOT(doWork1(QString)));

    /* 接收到 worker 发送过来的信号 */
    connect(worker, SIGNAL(resultReady(QString)),
            this, SLOT(handleResults(QString)));

    /* 点击按钮开始线程 */
    connect(pushButton1, SIGNAL(clicked()),
            this, SLOT(pushButton1Clicked()));

    /* 点击按钮打断线程 */
    connect(pushButton2, SIGNAL(clicked()),
            this, SLOT(pushButton2Clicked()));
}

MainWindow::~MainWindow()
{
    /* 打断线程再退出 */
    worker->stopWork();
    workerThread.quit();

    /* 阻塞线程 2000ms,判断线程是否结束 */
    if (workerThread.wait(2000)) {
        qDebug() << "线程结束" << endl;
    }
}

void MainWindow::pushButton1Clicked()
{
    /* 字符串常量 */
    const QString str = "正在运行";

    /* 判断线程是否在运行 */
    if (!workerThread.isRunning()) {
        /* 开启线程 */
        workerThread.start();
    }

    /* 发送正在运行的信号,线程收到信号后执行后返回线程耗时函数 + 此字符串 */
    emit this->startWork(str);
}

void MainWindow::pushButton2Clicked()
{
    /* 如果线程在运行 */
    if (workerThread.isRunning()) {

        /* 停止耗时工作,跳出耗时工作的循环 */
        worker->stopWork();
    }
}

void MainWindow::handleResults(const QString &results)
{
    /* 打印线程的状态 */
    qDebug() << "线程的状态:" << results << endl;
}

1.4.3 逐段解释

  • 工人类实例化不能指定父对象:继承 QObject 的多线程类不能用 new Worker(this),因为它的「父」是主线程对象,指定父对象会破坏 moveToThread 的前提(不能把有父对象的对象移到别的线程)。
  • worker->moveToThread(&workerThread):把 worker 对象整体移到 workerThread 线程里。此后 worker 的槽函数都在该线程执行。
  • connect(&workerThread, SIGNAL(finished()), worker, SLOT(deleteLater())):线程结束后用 deleteLater() 销毁 worker 对象与 workerThread 对象分配的内存。deleteLater() 会确认消息循环中没有这两个对象后再销毁,避免内存泄漏。
  • connect(this, SIGNAL(startWork(QString)), worker, SLOT(doWork1(QString))):主线程发射 startWork,worker 的 doWork1() 在子线程执行。这是跨线程调用槽函数的标准方式。
  • doWork1() 里的 while 循环 + 锁
    • 进入循环后用 QMutexLocker locker(&lock) 加锁,判断 isCanRun,为假就 break
    • {} 限定锁的作用范围,离开代码块自动解锁,QThread::sleep(2) 不会持锁;
    • 每 2s 发射一次 resultReady
  • stopWork() 不能放在槽函数下:它是被主线程直接调用的(不是信号触发),所以是普通 public 成员函数,不是槽。里面同样用 QMutexLocker 加锁后把 isCanRun = false,通知循环跳出。
  • QMutexLocker 的价值:在构造时加锁、析构时解锁,即使中途 return/异常也能保证解锁,避免忘记 unlock() 造成死锁。
  • doWork1() 结束不等于线程结束:因为 worker 被移到了线程里,线程会一直存在,直到对象被销毁,或调用 quit()/exit() 退出线程事件循环。
  • 析构顺序:先 stopWork() 让循环跳出,再 workerThread.quit(),最后 wait(2000) 确认退出。

1.4.4 运行效果

点击「开启线程」后,输出窗口每隔 2 秒打印「正在运行 doWork1 函数」;点击「打断线程」后打印「打断 doWork1 函数」,循环结束。再次点击「开启线程」可再次运行 doWork1()

1.5 线程同步类

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。原书只提到 Qt 提供 QMutexQMutexLockerQReadLockerQWriteLocker 等类用于线程同步,此处的对照表为 Qt 官方文档标准内容,补充以便实际开发查阅。

| 类 | 作用 | 典型用法 | | -- | ---- | -------- | | QMutex | 互斥锁,保护临界区 | lock.lock(); ...; lock.unlock(); | | QMutexLocker | RAII 方式安全使用 QMutex | QMutexLocker locker(&lock); 作用域结束自动解锁 | | QReadLocker / QWriteLocker | 配合 QReadWriteLock 的读/写锁 | 读多写少场景提升并发 | | QSemaphore | 信号量,控制同时访问资源的线程数 | 生产者/消费者缓冲 | | QWaitCondition | 条件变量,等待某个条件成立 | 线程间通知 |

⚠️ 易错点:跨线程访问共享变量(如本例的 isCanRun)必须加锁,否则可能出现读到旧值、编译器重排等问题。锁的粒度要尽量小,不要在处理耗时操作时持锁。

1.6 跨线程信号与槽

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。原书示例通过信号把子线程结果传回主线程,但未展开连接类型,以下为 Qt 官方文档中信号槽跨线程的标准规则。

Qt 的信号槽天生支持跨线程:

  • 默认连接方式 Qt::AutoConnection:若信号发射者与接收者不在同一线程,自动使用 Qt::QueuedConnection(把调用排入接收者线程的事件循环,异步执行);在同一线程则用 Qt::DirectConnection(直接调用)。
  • Qt::QueuedConnection 要求参数类型可被元对象系统复制,因此自定义类型需要 qRegisterMetaType() 注册。
  • 子线程不能直接操作界面控件:所有界面更新都应通过信号回到主线程执行,否则行为未定义。这也是本例中 worker 只发 resultReady 信号、由主线程打印的原因。

2. Qt 网络编程

Qt 网络模块提供了编写 TCP/IP 客户端和服务器的类:

  • 低级类QTcpSocketQTcpServerQUdpSocket,代表低层网络概念;
  • 高级类QNetworkRequestQNetworkReplyQNetworkAccessManager,用通用协议执行网络操作;
  • 还提供 QNetworkConfigurationQNetworkConfigurationManagerQNetworkSession 等类实现承载管理。

在项目中使用 Qt 网络模块,需要在 .pro 文件里增加:

QT += network

2.1 获取本机网络信息

为什么先获取本机网络信息? 建立网络通信前至少要获取对方的 IP 地址。网络应用中经常需要本机的主机名、IP 地址、MAC 地址等信息,Qt 提供了 QHostInfoQNetworkInterface 类用于此类信息查询。

例程 07_networkhostinfo。点击「获取本机信息」按钮后延时 1s 刷新并显示网络接口信息,点击「清空文本信息」清空显示。

2.1.1 核心代码:getHostInfo()

#include <QNetworkInterface>
#include <QHostInfo>

QString MainWindow::getHostInfo()
{
    /* 通过 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";
            }
        }
    }

    /* 返回网络信息 */
    return str;
}

槽函数与定时器:

void MainWindow::timerStart()
{
    /* 清空文本 */
    textBrowser->clear();

    /* 定时 1s */
    timer->start(1000);
}

void MainWindow::timerTimeOut()
{
    /* 显示本机信息 */
    showHostInfo();
    /* 停止定时器 */
    timer->stop();
}

void MainWindow::showHostInfo()
{
    /* 获取本机信息后显示到 textBrowser */
    textBrowser->insertPlainText(getHostInfo());
}

void MainWindow::clearHostInfo()
{
    if (!textBrowser->toPlainText().isEmpty())
        textBrowser->clear();
}

2.1.2 逐段解释

  • QHostInfo::localHostName():获取主机名称。
  • QNetworkInterface::allInterfaces():获取所有网络接口列表。直接打印 list 可以看到所有网络信息,但要从接口里提取细节需要 QNetworkAddressEntry
  • interface.addressEntries():从接口获取所有地址条目。
  • entry.ip().protocol() == QAbstractSocket::IPv4Protocol:一个接口下可能有 IPv4 和 IPv6 两个地址,用协议类型过滤只保留 IPv4。

2.1.3 运行效果

点击「获取本机信息」,文本浏览框打印主机名、网卡名、MAC 地址、IPv4 地址、子网掩码、广播地址。lo 是本地回环网卡,ens33 是 VMware 虚拟出来的网卡(Ubuntu 虚拟机环境)。点击「清空文本信息」清空内容。

2.2 TCP 通信

2.2.1 TCP 简介

TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 通信必须先建立连接,通信端分为客户端和服务端:

  • 服务端通过监听某个端口来等待客户端连接,有连接到来则建立新的 socket 连接;
  • 客户端通过 IP 和 port 连接服务端,连接成功后即可收发数据。

注意:在 Qt 中,Qt 把 socket 当成输入输出流来对待,数据收发通过 read()write() 进行,需要与常见 C socket 的 send() / recv() 区分。

sequenceDiagram
    participant C as TCP 客户端 (QTcpSocket)
    participant S as TCP 服务端 (QTcpServer + QTcpSocket)
    S->>S: listen(IP, port)
    C->>S: connectToHost(IP, port)
    S-->>S: 发射 newConnection()
    S->>S: nextPendingConnection() 获取通信 socket
    S-->>C: 连接建立
    C->>S: write(data)
    S-->>C: readyRead() → readAll()
    S->>C: write(data)
    C-->>S: readyRead() → readAll()
    C->>S: disconnectFromHost()

2.2.2 TCP 服务端(08_tcpserver)

流程:获取本地 IP → 创建 tcpServertcpSocket → 点击监听即绑定本地 IP 和端口等待连接 → 客户端连上后收发消息。

核心代码:

/* 新的客户端连接 */
void MainWindow::clientConnected()
{
    /* 获取客户端的套接字 */
    tcpSocket = tcpServer->nextPendingConnection();
    /* 客户端的 ip 信息 */
    QString ip = tcpSocket->peerAddress().toString();
    /* 客户端的端口信息 */
    quint16 port = tcpSocket->peerPort();
    /* 在文本浏览框里显示出客户端的连接信息 */
    textBrowser->append("客户端已连接");
    textBrowser->append("客户端ip 地址:" + ip);
    textBrowser->append("客户端端口:" + QString::number(port));

    connect(tcpSocket, SIGNAL(readyRead()),
            this, SLOT(receiveMessages()));
    connect(tcpSocket,
            SIGNAL(stateChanged(QAbstractSocket::SocketState)),
            this,
            SLOT(socketStateChange(QAbstractSocket::SocketState)));
}

/* 获取本地 IP */
void MainWindow::getLocalHostIP()
{
    QList<QNetworkInterface> list
            = QNetworkInterface::allInterfaces();
    foreach (QNetworkInterface interface, list) {
        QList<QNetworkAddressEntry> entryList
                = interface.addressEntries();
        foreach (QNetworkAddressEntry entry, entryList) {
            if (entry.ip().protocol() ==
                    QAbstractSocket::IPv4Protocol) {
                comboBox->addItem(entry.ip().toString());
                /* 添加到 IP 列表中 */
                IPlist << entry.ip();
            }
        }
    }
}

/* 开始监听 */
void MainWindow::startListen()
{
    /* 需要判断当前主机是否有 IP 项 */
    if (comboBox->currentIndex() != -1) {
        qDebug() << "start listen" << endl;
        tcpServer->listen(IPlist[comboBox->currentIndex()],
                spinBox->value());

        /* 设置按钮与下拉列表框的状态 */
        pushButton[0]->setEnabled(false);
        pushButton[1]->setEnabled(true);
        comboBox->setEnabled(false);
        spinBox->setEnabled(false);

        textBrowser->append("服务器IP 地址:" + comboBox->currentText());
        textBrowser->append("正在监听端口:" + spinBox->text());
    }
}

/* 停止监听 */
void MainWindow::stopListen()
{
    qDebug() << "stop listen" << endl;
    /* 停止监听 */
    tcpServer->close();

    /* 如果是连接上了也应该断开,如果不断开客户端还能继续发送信息,
     * 因为 socket 未断开,还在监听上一次端口 */
    if (tcpSocket->state() == tcpSocket->ConnectedState)
        tcpSocket->disconnectFromHost();

    /* 设置按钮与下拉列表框的状态 */
    pushButton[1]->setEnabled(false);
    pushButton[0]->setEnabled(true);
    comboBox->setEnabled(true);
    spinBox->setEnabled(true);

    textBrowser->append("已停止监听端口:" + spinBox->text());
}

/* 服务端接收消息 */
void MainWindow::receiveMessages()
{
    /* 读取接收到的消息 */
    QString messages = "客户端:" + tcpSocket->readAll();
    textBrowser->append(messages);
}

/* 服务端发送消息 */
void MainWindow::sendMessages()
{
    if (NULL == tcpSocket)
        return;

    /* 如果已经连接 */
    if (tcpSocket->state() == tcpSocket->ConnectedState) {
        /* 发送消息 */
        tcpSocket->write(lineEdit->text().toUtf8().data());
        /* 在服务端插入发送的消息 */
        textBrowser->append("服务端:" + lineEdit->text());
    }
}

在构造函数里需要连接 newConnection 信号:

tcpServer = new QTcpServer(this);
tcpSocket = new QTcpSocket(this);
/* ... 界面代码 ... */
connect(tcpServer, SIGNAL(newConnection()),
        this, SLOT(clientConnected()));

逐段解释:

  • tcpServer->listen(QHostAddress, quint16):绑定 IP 与端口并开始监听。端口范围设为 10000~99999,避免与系统已用端口冲突。
  • newConnection() 信号:有客户端连接时发出,槽函数里用 nextPendingConnection() 取出该连接的 QTcpSocket
  • peerAddress() / peerPort():获取对端(客户端)的 IP 与端口。
  • readyRead() 信号:有新数据可读时发出,槽函数里 readAll() 读出全部内容。
  • stateChanged(QAbstractSocket::SocketState):socket 状态变化(未连接、已连接、连接中、主机查找、关闭中、监听中、已绑定)时发出,用于界面提示。
  • tcpSocket->write(...):发送数据,参数为字节数组。这里用 .toUtf8().data()QString 转成 UTF-8 字节。
  • stopListen() 里先 tcpServer->close() 再判断断连:只关服务器不断开已建立的 socket 的话,客户端仍能继续发数据。

2.2.3 TCP 客户端(09_tcpclient)

流程:获取本地 IP → 创建 tcpSocket → 用 connectToHost() 连接服务端 IP 和端口 → 收发数据。

核心代码:

void MainWindow::toConnect()
{
    /* 如果连接状态还没有连接 */
    if (tcpSocket->state() != tcpSocket->ConnectedState) {
        /* 指定 IP 地址和端口连接 */
        tcpSocket->connectToHost(IPlist[comboBox->currentIndex()],
                spinBox->value());
    }
}

void MainWindow::toDisConnect()
{
    /* 断开连接 */
    tcpSocket->disconnectFromHost();
    /* 关闭 socket */
    tcpSocket->close();
}

void MainWindow::connected()
{
    /* 显示已经连接 */
    textBrowser->append("已经连上服务端");

    /* 设置按钮与下拉列表框的状态 */
    pushButton[0]->setEnabled(false);
    pushButton[1]->setEnabled(true);
    comboBox->setEnabled(false);
    spinBox->setEnabled(false);
}

void MainWindow::disconnected()
{
    /* 显示已经断开连接 */
    textBrowser->append("已经断开服务端");

    pushButton[1]->setEnabled(false);
    pushButton[0]->setEnabled(true);
    comboBox->setEnabled(true);
    spinBox->setEnabled(true);
}

/* 客户端接收消息 */
void MainWindow::receiveMessages()
{
    QString messages = tcpSocket->readAll();
    textBrowser->append("服务端:" + messages);
}

/* 客户端发送消息 */
void MainWindow::sendMessages()
{
    if (NULL == tcpSocket)
        return;

    if (tcpSocket->state() == tcpSocket->ConnectedState) {
        textBrowser->append("客户端:" + lineEdit->text());
        tcpSocket->write(lineEdit->text().toUtf8().data());
    }
}

构造函数里的信号槽:

connect(tcpSocket, SIGNAL(connected()),    this, SLOT(connected()));
connect(tcpSocket, SIGNAL(disconnected()), this, SLOT(disconnected()));
connect(tcpSocket, SIGNAL(readyRead()),    this, SLOT(receiveMessages()));
connect(tcpSocket,
        SIGNAL(stateChanged(QAbstractSocket::SocketState)),
        this,
        SLOT(socketStateChange(QAbstractSocket::SocketState)));

逐段解释:

  • connectToHost(IP, port):客户端主动连接服务端。连接是异步的,成功后会发出 connected() 信号。
  • connected() / disconnected() 信号:分别在连接建立、断开时发出,用于更新界面按钮状态与提示。
  • 发送与接收都用同一个 tcpSocket:Qt 把 socket 当流处理,write() 发送、readAll() 接收。
  • disconnectFromHost() + close():主动断开并释放 socket。

2.2.4 运行效果

两个程序可运行在同一台机器(服务端监听本机 IP,客户端连接同一 IP)或分别运行在开发板与 Ubuntu 上。服务端先点「开始监听」,客户端点「连接服务端」,双方即可互发消息;服务端会显示客户端 IP 与端口,客户端会显示连接状态。停止监听或断开连接时,双方界面上的按钮状态自动切换。

2.3 UDP 通信

2.3.1 UDP 简介

UDP 是无连接的传输层协议,不需要事先建立连接,直接把数据报(datagram)发出去,速度快但不保证可靠。UDP 支持三种发送方式:

方式 目标地址 说明
单播 某个具体 IP 一对一,点对点发送
广播 QHostAddress::Broadcast(255.255.255.255) 同一网段内监听目标端口的程序都会收到
组播(多播) D 类地址,如 239.255.255.1 加入同一组播组的主机都能收到
flowchart TB
    A["UDP 数据报"] --> B{"目标地址类型"}
    B -->|"具体IP"| C["单播:只有该主机收到"]
    B -->|"255.255.255.255"| D["广播:同网段所有主机收到"]
    B -->|"D 类地址 239.x.x.x"| E["组播:加入该组的主机收到"]

2.3.2 UDP 单播与广播(10_udp_unicast_broadcast)

流程:绑定本地端口 → 接收数据 → 向目标 IP 单播或向广播地址广播。

核心代码:

/* 绑定端口 */
void MainWindow::bindPort()
{
    quint16 port = spinBox[0]->value();

    /* 绑定端口需要在 socket 的状态为 UnconnectedState */
    if (udpSocket->state() != QAbstractSocket::UnconnectedState)
        udpSocket->close();

    if (udpSocket->bind(port)) {
        textBrowser->append("已经成功绑定端口:"
                            + QString::number(port));

        pushButton[0]->setEnabled(false);
        pushButton[1]->setEnabled(true);
        spinBox[1]->setEnabled(false);
    }
}

/* 解绑端口 */
void MainWindow::unbindPort()
{
    /* 解绑,不再监听 */
    udpSocket->abort();

    pushButton[0]->setEnabled(true);
    pushButton[1]->setEnabled(false);
    spinBox[1]->setEnabled(true);
}

/* 接收消息 */
void MainWindow::receiveMessages()
{
    /* 局部变量,用于获取发送者的 IP 和端口 */
    QHostAddress peerAddr;
    quint16 peerPort;

    /* 如果有数据已经准备好 */
    while (udpSocket->hasPendingDatagrams()) {
        /* udpSocket 发送的数据报是 QByteArray 类型的字节数组 */
        QByteArray datagram;

        /* 重新定义数组的大小 */
        datagram.resize(udpSocket->pendingDatagramSize());

        /* 读取数据,并获取发送方的 IP 地址和端口 */
        udpSocket->readDatagram(datagram.data(),
                                datagram.size(),
                                &peerAddr,
                                &peerPort);
        /* 转为字符串 */
        QString str = datagram.data();

        /* 显示信息到文本浏览框窗口 */
        textBrowser->append("接收来自"
                            + peerAddr.toString()
                            + ":"
                            + QString::number(peerPort)
                            + str);
    }
}

/* 单播发送消息 */
void MainWindow::sendMessages()
{
    /* 文本浏览框显示发送的信息 */
    textBrowser->append("发送:" + lineEdit->text());

    /* 要发送的信息,转为 QByteArray 类型字节数组,数据一般少于 512 个字节 */
    QByteArray data = lineEdit->text().toUtf8();

    /* 要发送的目标 Ip 地址 */
    QHostAddress peerAddr = IPlist[comboBox->currentIndex()];

    /* 要发送的目标端口号 */
    quint16 peerPort = spinBox[1]->value();

    /* 发送消息 */
    udpSocket->writeDatagram(data, peerAddr, peerPort);
}

/* 广播发送消息 */
void MainWindow::sendBroadcastMessages()
{
    textBrowser->append("发送:" + lineEdit->text());

    QByteArray data = lineEdit->text().toUtf8();

    /* 广播地址,一般为 255.255.255.255,
     * 同一网段内监听目标端口的程序都会接收到消息 */
    QHostAddress peerAddr = QHostAddress::Broadcast;

    /* 要发送的目标端口号 */
    quint16 peerPort = spinBox[1]->text().toInt();

    /* 发送消息 */
    udpSocket->writeDatagram(data, peerAddr, peerPort);
}

逐段解释:

  • udpSocket->bind(port):UDP 接收前必须绑定端口。绑定要求 socket 处于 UnconnectedState,否则先 close()
  • hasPendingDatagrams() + pendingDatagramSize():循环判断是否有待读数据报,并获取其大小。
  • readDatagram(data, size, &addr, &port):读取数据报,同时可获取发送方 IP 与端口(这是 UDP 与 TCP 的重要区别)。
  • writeDatagram(data, addr, port):发送数据报。单播传具体 IP,广播传 QHostAddress::Broadcast
  • abort():立即关闭 socket 并丢弃待处理数据,用于解绑。

2.3.3 UDP 组播(11_udp_multicast)

组播的关键是先绑定端口,再加入组播组,发送时以组播地址和端口为目标。

核心代码:

/* 构造函数中设置组播 TTL */
udpSocket = new QUdpSocket(this);
/* 设置 IP_MULTICAST_TTL 套接字选项,限制数据包在 Internet 中的生存时间,
 * 防止其无限期循环;数据报跨一个路由会减一,默认值为 1,表示多播仅适用于本地子网 */
udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption, 1);

/* 加入组播 */
void MainWindow::joinGroup()
{
    /* 获取端口 */
    quint16 port = spinBox->value();
    /* 获取组播地址 */
    QHostAddress groupAddr = QHostAddress(comboBox[1]->currentText());

    /* 绑定端口需要在 socket 的状态为 UnconnectedState */
    if (udpSocket->state() != QAbstractSocket::UnconnectedState)
        udpSocket->close();

    /* 加入组播前必须先绑定端口 */
    if (udpSocket->bind(QHostAddress::AnyIPv4,
                        port, QUdpSocket::ShareAddress)) {

        /* 加入组播组,返回结果给 ok 变量 */
        bool ok = udpSocket->joinMulticastGroup(groupAddr);

        textBrowser->append(ok ? "加入组播成功" : "加入组播失败");
        textBrowser->append("组播地址IP:" + comboBox[1]->currentText());
        textBrowser->append("绑定端口:" + QString::number(port));

        pushButton[0]->setEnabled(false);
        pushButton[1]->setEnabled(true);
        comboBox[1]->setEnabled(false);
        spinBox->setEnabled(false);
    }
}

/* 退出组播 */
void MainWindow::leaveGroup()
{
    QHostAddress groupAddr = QHostAddress(comboBox[1]->currentText());

    /* 退出组播 */
    udpSocket->leaveMulticastGroup(groupAddr);

    /* 解绑,不再监听 */
    udpSocket->abort();

    pushButton[0]->setEnabled(true);
    pushButton[1]->setEnabled(false);
    comboBox[1]->setEnabled(true);
    spinBox->setEnabled(true);
}

/* 组播发送消息 */
void MainWindow::sendMessages()
{
    textBrowser->append("发送:" + lineEdit->text());

    QByteArray data = lineEdit->text().toUtf8();

    /* 要发送的目标 Ip 地址(组播地址) */
    QHostAddress groupAddr = QHostAddress(comboBox[1]->currentText());

    /* 要发送的目标端口号 */
    quint16 groupPort = spinBox->value();

    /* 发送消息 */
    udpSocket->writeDatagram(data, groupAddr, groupPort);
}

逐段解释:

  • setSocketOption(QAbstractSocket::MulticastTtlOption, 1):设置组播 TTL。默认 1 表示只在本地子网内多播。
  • bind(QHostAddress::AnyIPv4, port, QUdpSocket::ShareAddress):绑定 AnyIPv4 表示监听所有 IPv4 网卡的该端口;ShareAddress 允许多个 socket 绑定同一地址端口(组播常用)。
  • joinMulticastGroup(groupAddr):加入组播组,返回 bool。必须先 bind 再 join
  • leaveMulticastGroup(groupAddr):退出组播组,随后 abort() 解绑。
  • isLoopback() 过滤:组播例程在枚举本地 IP 时会过滤掉环回地址,保证显示的是实际网卡地址。

2.3.4 运行效果

UDP 单播:服务端绑定端口,发送端填写目标 IP 和端口即可互发。广播:发送时选择广播,同一网段所有监听该端口的程序都能收到。组播:双方都加入同一组播地址(如 239.255.255.1)并绑定同一端口,即可互相收发。

2.4 网络下载(12_imagedownload)

例程使用 QNetworkAccessManager + QNetworkRequest + QNetworkReply 通过 HTTP 下载一张图片,显示进度并保存到本地。

核心代码:

#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QMessageBox>
#include <QCoreApplication>

void MainWindow::startDownload()
{
    /* 获取 URL 链接 */
    QUrl newUrl(QUrl(lineEdit->text()));

    /* 如果下载链接无效,则直接返回 */
    if (!newUrl.isValid()) {
        QMessageBox::information(this, "error", "invalid url");
        return;
    }

    /* 网络请求 */
    QNetworkRequest networkRequest;

    /* 设置下载的地址 */
    networkRequest.setUrl(newUrl);

    /* 网络响应 */
    QNetworkReply *newReply =
            networkAccessManager->get(networkRequest);

    /* 信号槽连接 */
    connect(newReply, SIGNAL(finished()),
            this, SLOT(replyFinished()));
    connect(newReply, SIGNAL(downloadProgress(qint64, qint64)),
            this, SLOT(imageDownloadProgress(qint64, qint64)));
    connect(newReply,
            SIGNAL(error(QNetworkReply::NetworkError)),
            this,
            SLOT(networkReplyError(QNetworkReply::NetworkError)));
}

void MainWindow::replyFinished()
{
    /* 获取信号发送者 */
    QNetworkReply *reply = (QNetworkReply *)sender();

    /* 防止内存泄漏 */
    reply->deleteLater();

    /* 立刻读取数据 */
    readyReadData(reply);

    /* 判断当前执行程序下的图像是否下载完成 */
    QFile imageFile(QCoreApplication::applicationDirPath()
                    + "/下载图.jpg");
    if (imageFile.exists()) {
        /* 显示下载的图片 */
        label[0]->setPixmap(QPixmap(imageFile.fileName()));
        qDebug() << "已经成功下载,文件路径为:"
                 << imageFile.fileName() << endl;
    } else
        /* 清空显示 */
        label[0]->clear();

    /* 设置按钮可用 */
    pushButton->setEnabled(true);
}

void MainWindow::readyReadData(QNetworkReply *reply)
{
    /* 设置按钮不可用,防止未完成,再次点击 */
    pushButton->setEnabled(false);

    QFile imageFile;
    /* 保存到当前路径,名称为 "下载图.jpg" */
    imageFile.setFileName(QCoreApplication::applicationDirPath()
                          + "/下载图.jpg");

    /* 如果此图片已经存在,则删除 */
    if (imageFile.exists())
        imageFile.remove();

    /* 读取数据 */
    QByteArray data = reply->readAll();
    if (data.isEmpty()) {
        qDebug() << "data is null, please try it again!" << endl;
        return;
    }

    /* 判断是不是 JPG 格式的图片,如果不是则返回,如果不需要判断 JPG 可以删除这部分代码 */
    if (!(data[0] == (char)0xff
          && data[1] == (char)0xd8
          && data[data.size() - 2] == (char)0xff
          && data[data.size() - 1] == (char)0xd9)) {
        qDebug() << "not JPG data, please try it again!" << endl;
        return;
    }

    /* 转为 QPixmap */
    QPixmap pixmap;
    pixmap.loadFromData(data);
    pixmap.save(imageFile.fileName());
}

void MainWindow::imageDownloadProgress(qint64 bytes,
                                       qint64 totalBytes)
{
    /* 设置进度条的最大值 */
    progressBar->setMaximum(totalBytes);
    /* 设置当前值 */
    progressBar->setValue(bytes);
}

/* 网络响应处理函数 */
void MainWindow::networkReplyError(QNetworkReply::NetworkError error)
{
    switch (error) {
    case QNetworkReply::ConnectionRefusedError:
        qDebug() << "远程服务器拒绝连接" << endl;
        break;
    case QNetworkReply::HostNotFoundError:
        qDebug() << "找不到远程主机名" << endl;
        break;
    case QNetworkReply::TimeoutError:
        qDebug() << "与远程服务器连接超时" << endl;
        break;
    default:
        break;
    }
}

逐段解释:

  • QNetworkAccessManager::get(request):发起 HTTP GET 请求,返回一个 QNetworkReply 对象。请求是异步的,结果通过信号返回。
  • finished() 信号:请求完成(无论成功失败)后发出。
  • downloadProgress(qint64, qint64):下载过程中持续发出,参数为已下载字节数和总字节数,用于更新进度条。
  • error(QNetworkReply::NetworkError):出错时发出,可据此提示连接被拒、主机找不到、超时等。
  • reply->deleteLater():响应对象需要手动释放,避免内存泄漏。
  • readAll() 读取响应体:请求完成后一次性读出全部数据;随后用 loadFromData() 转为 QPixmap
  • JPG 头尾校验:JPG 文件以 FF D8 开头、FF D9 结尾,用于判断下载到的确实是图片而不是错误页面。
  • 保存路径用 QCoreApplication::applicationDirPath():可执行文件所在目录,避免相对路径歧义。

运行效果:输入图片 URL(浏览器右键复制图片地址),点击「下载」,进度条随下载推进,完成后图片显示在窗口上方,并保存为可执行文件目录下的「下载图.jpg」。


3. 多媒体

3.1 Qt 多媒体模块简介

Qt 从 4.4 版本开始提供多媒体框架。Qt 4.6 中实现多媒体播放图形界面主要依赖 phonon 框架(源自 KDE,应用不用关心底层是 gstreamer 还是 xine,中间通过 backend 中转)。从 Qt5 开始,Qt 弃用了 phonon,直接使用 Qt Multimedia 模块,同样通过底层多媒体插件实现,但不再需要中转。

Qt 多媒体模块的主要类:

| 类 | 用途 | | -- | ---- | | QSound | 播放本地 WAV 音效(异步方式) | | QSoundEffect | 播放音效,支持本地与网络文件(用 QUrl) | | QMediaPlayer | 播放音乐/视频 | | QMediaPlaylist | 播放列表,管理多个媒体文件 | | QVideoWidget | 视频显示窗口 | | QAudioRecorder | 录音 | | QAudioProbe | 探测音频缓冲区(用于显示音量 level) | | QAudioOutput / QAudioInput | 音频输出/输入 |

.pro 里添加:

QT += multimedia

注意:Qt 的音乐/视频播放器需要在 Ubuntu 里安装媒体解码器才能播放。Ubuntu16/18 安装命令:

> sudo apt-get install gstreamer1.0-plugins-base gstreamer1.0-plugins-bad \
>   gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly \
>   gstreamer1.0-pulseaudio gstreamer1.0-libav
> ```
>
> Windows 需要安装 LAVFilters 解码器。视频播放器还需要在 `.pro` 里额外加 `multimediawidgets`。

### 3.2 音效文件播放(13_button_sound)

播放简短提示音(按键音等)可以用 `QSoundEffect` 和 `QSound`。两者主要区别:

- `QSound`:**异步方式播放,只能播放本地 WAV 音效文件**。WAV 文件尺寸较大,适合存储简短声音片段,具有低延时、不失真的特点。
- `QSoundEffect`:不仅可播放本地音效,也可以播放网络文件(一般用 `QUrl` 链接)。

例程 `13_button_sound`:点击按钮播放按键音效。

cpp /* mainwindow.h */ #include #include #include

class MainWindow : public QMainWindow {

Q_OBJECT

public:

MainWindow(QWidget *parent = nullptr);
~MainWindow();

private:

QPushButton *pushButton;

private slots:

void pushButtonClicked();

};


cpp /* mainwindow.cpp */ #include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent)

: QMainWindow(parent)

{

/* 设置主窗体的位置与大小 */
this->setGeometry(0, 0, 800, 480);

/* 实例化按钮 */
pushButton = new QPushButton(this);
pushButton->setText("按钮音效测试");
pushButton->setGeometry(340, 220, 120, 40);

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

}

void MainWindow::pushButtonClicked() {

/* 异步的方式播放 */
QSound::play(":/audio/bell.wav");

}


资源文件 `src.qrc`:

xml

<qresource prefix="/">
    <file>audio/bell.wav</file>
</qresource>


**逐段解释**:

- `QSound::play(fileName)` 是静态函数,直接播放指定音效,返回后立即继续执行(异步),不会阻塞界面。
- 播放路径使用资源系统中的 `:/audio/bell.wav`,音效随可执行文件打包,开发板上无需额外拷贝文件。

### 3.3 音乐播放器(14_musicplayer)

例程 `14_musicplayer`:扫描 `myMusic` 目录下的 mp3,用 `QMediaPlayer` + `QMediaPlaylist` 播放,支持上一曲/下一曲、进度拖动、列表点击。

**媒体核心结构(mainwindow.h):**

cpp #include #include

/* 媒体信息结构体 */ struct MediaObjectInfo {

/* 用于保存歌曲文件名 */
QString fileName;
/* 用于保存歌曲文件路径 */
QString filePath;

};

class MainWindow : public QMainWindow {

Q_OBJECT

public:

MainWindow(QWidget *parent = nullptr);
~MainWindow();

private:

/* 媒体播放器,用于播放音乐 */
QMediaPlayer *musicPlayer;
/* 媒体列表 */
QMediaPlaylist *mediaPlaylist;
/* 音乐列表 */
QListWidget *listWidget;
/* 播放进度条 */
QSlider *durationSlider;
QPushButton *pushButton[7];
QLabel *label[4];
QWidget *listMask;
QVector<MediaObjectInfo> mediaObjectInfo;

/* 音乐布局函数(界面构建,代码较长,见例程) */
void musicLayout();
/* 主窗体大小重设大小函数重写 */
void resizeEvent(QResizeEvent *event);
/* 扫描歌曲 */
void scanSongs();
/* 媒体播放器类初始化 */
void mediaPlayerInit();

private slots:

void btn_play_clicked();
void btn_next_clicked();
void btn_previous_clicked();
void mediaPlayerStateChanged(QMediaPlayer::State);
void listWidgetCliked(QListWidgetItem*);
void mediaPlaylistCurrentIndexChanged(int);
void musicPlayerDurationChanged(qint64);
void mediaPlayerPositionChanged(qint64);
void durationSliderReleased();

};


**播放器初始化与扫描歌曲:**

cpp void MainWindow::mediaPlayerInit() {

musicPlayer = new QMediaPlayer(this);
mediaPlaylist = new QMediaPlaylist(this);
/* 确保列表是空的 */
mediaPlaylist->clear();
/* 设置音乐播放器的列表为 mediaPlaylist */
musicPlayer->setPlaylist(mediaPlaylist);
/* 设置播放模式,Loop 是列表循环 */
mediaPlaylist->setPlaybackMode(QMediaPlaylist::Loop);

}

void MainWindow::scanSongs() {

QDir dir(QCoreApplication::applicationDirPath() + "/myMusic");
QDir dirbsolutePath(dir.absolutePath());
/* 如果目录存在 */
if (dirbsolutePath.exists()) {
    /* 定义过滤器 */
    QStringList filter;
    /* 包含所有 mp3 后缀的文件 */
    filter << "*.mp3";
    /* 获取该目录下的所有文件 */
    QFileInfoList files =
            dirbsolutePath.entryInfoList(filter, QDir::Files);
    /* 遍历 */
    for (int i = 0; i < files.count(); i++) {
        MediaObjectInfo info;
        /* 使用 utf-8 编码 */
        QString fileName = QString::fromUtf8(files.at(i)
                                             .fileName()
                                             .replace(".mp3", "")
                                             .toUtf8()
                                             .data());
        info.fileName = fileName + "\n"
                + fileName.split("-").at(1);
        info.filePath = QString::fromUtf8(files.at(i)
                                          .filePath()
                                          .toUtf8()
                                          .data());
        /* 媒体列表添加歌曲 */
        if (mediaPlaylist->addMedia(
                    QUrl::fromLocalFile(info.filePath))) {
            mediaObjectInfo.append(info);
            listWidget->addItem(info.fileName);
        } else {
            qDebug() << mediaPlaylist->errorString().toUtf8().data();
            qDebug() << "  Error number:" << mediaPlaylist->error();
        }
    }
}

}


**播放控制:**

cpp void MainWindow::btn_play_clicked() {

int state = musicPlayer->state();
switch (state) {
case QMediaPlayer::StoppedState:
    musicPlayer->play();
    break;
case QMediaPlayer::PlayingState:
    musicPlayer->pause();
    break;
case QMediaPlayer::PausedState:
    musicPlayer->play();
    break;
}

}

void MainWindow::btn_next_clicked() {

musicPlayer->stop();
int count = mediaPlaylist->mediaCount();
if (0 == count)
    return;
/* 列表下一曲 */
mediaPlaylist->next();
musicPlayer->play();

}

void MainWindow::btn_previous_clicked() {

musicPlayer->stop();
int count = mediaPlaylist->mediaCount();
if (0 == count)
    return;
/* 列表上一曲 */
mediaPlaylist->previous();
musicPlayer->play();

}

void MainWindow::listWidgetCliked(QListWidgetItem *item) {

musicPlayer->stop();
mediaPlaylist->setCurrentIndex(listWidget->row(item));
musicPlayer->play();

}

void MainWindow::durationSliderReleased() {

/* 设置媒体播放的位置 */
musicPlayer->setPosition(durationSlider->value() * 1000);

}


**时长与进度显示:**

cpp void MainWindow::musicPlayerDurationChanged(qint64 duration) {

durationSlider->setRange(0, duration / 1000);
int second = duration / 1000;
int minute = second / 60;
second %= 60;

QString mediaDuration;
if (minute >= 10)
    mediaDuration = QString::number(minute, 10);
else
    mediaDuration = "0" + QString::number(minute, 10);

if (second >= 10)
    mediaDuration = mediaDuration + ":" + QString::number(second, 10);
else
    mediaDuration = mediaDuration + ":0" + QString::number(second, 10);

/* 显示媒体总长度时间 */
label[3]->setText(mediaDuration);

}

void MainWindow::mediaPlayerPositionChanged(qint64 position) {

if (!durationSlider->isSliderDown())
    durationSlider->setValue(position / 1000);

int second = position / 1000;
int minute = second / 60;
second %= 60;

QString mediaPosition;
if (minute >= 10)
    mediaPosition = QString::number(minute, 10);
else
    mediaPosition = "0" + QString::number(minute, 10);

if (second >= 10)
    mediaPosition = mediaPosition + ":" + QString::number(second, 10);
else
    mediaPosition = mediaPosition + ":0" + QString::number(second, 10);

/* 显示现在播放的时间 */
label[2]->setText(mediaPosition);

}


**信号槽连接(构造函数中):**

cpp connect(pushButton[0], SIGNAL(clicked()), this, SLOT(btn_previous_clicked())); connect(pushButton[1], SIGNAL(clicked()), this, SLOT(btn_play_clicked())); connect(pushButton[2], SIGNAL(clicked()), this, SLOT(btn_next_clicked()));

connect(musicPlayer, SIGNAL(stateChanged(QMediaPlayer::State)),

    this, SLOT(mediaPlayerStateChanged(QMediaPlayer::State)));

connect(mediaPlaylist, SIGNAL(currentIndexChanged(int)),

    this, SLOT(mediaPlaylistCurrentIndexChanged(int)));

connect(musicPlayer, SIGNAL(durationChanged(qint64)),

    this, SLOT(musicPlayerDurationChanged(qint64)));

connect(musicPlayer, SIGNAL(positionChanged(qint64)),

    this, SLOT(mediaPlayerPositionChanged(qint64)));

connect(listWidget, SIGNAL(itemClicked(QListWidgetItem*)),

    this, SLOT(listWidgetCliked(QListWidgetItem*)));

connect(durationSlider, SIGNAL(sliderReleased()),

    this, SLOT(durationSliderReleased()));

**逐段解释**:

- `QMediaPlayer` 负责播放,`QMediaPlaylist` 负责曲目列表与切换。`musicPlayer->setPlaylist(mediaPlaylist)` 把它们绑定起来。
- `setPlaybackMode(QMediaPlaylist::Loop)` 设置循环播放模式。
- `addMedia(QUrl::fromLocalFile(path))` 添加本地媒体,返回是否成功。
- 播放状态由 `stateChanged` 信号报告,界面用 `pushButton[1]->setChecked()` 同步按钮的按下/弹起外观。
- 播放进度:`positionChanged` 持续报告当前播放位置,用于更新时间和滑块;`durationChanged` 报告总时长。拖动滑块时先 `isSliderDown()` 判断,避免自动更新与用户拖动冲突;松开后 `setPosition()` 跳转。
- 界面布局函数 `musicLayout()` 负责按钮、列表、CD 图片、进度条等的摆放,代码较长,直接参考例程即可。

**运行效果**:程序目录下的 `myMusic` 文件夹中的 mp3 显示在左侧列表;点击列表项或播放/上一曲/下一曲按钮进行播放,底部显示当前时间/总时长,拖动进度条可跳转。

### 3.4 视频播放器(15_videoplayer)

例程 `15_videoplayer`:用 `QMediaPlayer` + `QMediaPlaylist` + `QVideoWidget` 播放 `myVideo` 目录下的视频,支持播放/暂停、下一集、音量加减、全屏。

关键区别:视频需要设置视频输出窗口,并在 `.pro` 里额外加 `multimediawidgets`。

pro QT += core gui multimedia multimediawidgets


**媒体初始化(与音乐播放器的关键差异):**

cpp #include

void MainWindow::mediaPlayerInit() {

videoPlayer = new QMediaPlayer(this);
mediaPlaylist = new QMediaPlaylist(this);
/* 确保列表是空的 */
mediaPlaylist->clear();
/* 设置视频播放器的列表为 mediaPlaylist */
videoPlayer->setPlaylist(mediaPlaylist);
/* 设置视频输出窗口——这是视频播放与音频播放的关键差别 */
videoPlayer->setVideoOutput(videoWidget);
/* 设置播放模式,Loop 是列表循环 */
mediaPlaylist->setPlaybackMode(QMediaPlaylist::Loop);
/* 设置默认软件音量 50% */
videoPlayer->setVolume(50);

}


**扫描视频文件:**

cpp void MainWindow::scanVideoFiles() {

QDir dir(QCoreApplication::applicationDirPath() + "/myVideo");
QDir dirbsolutePath(dir.absolutePath());
if (dirbsolutePath.exists()) {
    QStringList filter;
    /* 包含所有常见视频后缀的文件 */
    filter << "*.mp4" << "*.mkv" << "*.wmv" << "*.avi";
    QFileInfoList files =
            dirbsolutePath.entryInfoList(filter, QDir::Files);
    for (int i = 0; i < files.count(); i++) {
        MediaObjectInfo info;
        info.fileName = QString::fromUtf8(files.at(i)
                                          .fileName().toUtf8().data());
        info.filePath = QString::fromUtf8(files.at(i)
                                          .filePath().toUtf8().data());
        if (mediaPlaylist->addMedia(
                    QUrl::fromLocalFile(info.filePath))) {
            mediaObjectInfo.append(info);
            listWidget->addItem(info.fileName);
        } else {
            qDebug() << mediaPlaylist->errorString().toUtf8().data();
            qDebug() << "  Error number:" << mediaPlaylist->error();
        }
    }
}

}


**播放与音量控制:**

cpp void MainWindow::btn_play_clicked() {

int state = videoPlayer->state();
switch (state) {
case QMediaPlayer::StoppedState:
    videoPlayer->play();
    break;
case QMediaPlayer::PlayingState:
    videoPlayer->pause();
    break;
case QMediaPlayer::PausedState:
    videoPlayer->play();
    break;
}

}

void MainWindow::btn_next_clicked() {

videoPlayer->stop();
int count = mediaPlaylist->mediaCount();
if (0 == count)
    return;
mediaPlaylist->next();
videoPlayer->play();

}

void MainWindow::btn_volmeup_clicked() {

/* 点击每次音量 +5 */
volumeSlider->setValue(volumeSlider->value() + 5);
videoPlayer->setVolume(volumeSlider->value());

}

void MainWindow::btn_volmedown_clicked() {

/* 点击每次音量 -5 */
volumeSlider->setValue(volumeSlider->value() - 5);
videoPlayer->setVolume(volumeSlider->value());

}

void MainWindow::btn_fullscreen_clicked() {

/* 全屏/非全屏操作,隐藏/显示左侧列表容器 */
vWidget[0]->setVisible(!pushButton[4]->isChecked());

}

void MainWindow::volumeSliderReleased() {

videoPlayer->setVolume(volumeSlider->value());

}


**逐段解释**:

- `videoPlayer->setVideoOutput(videoWidget)` 把视频画面输出到 `QVideoWidget`,这是视频与音乐播放器最本质的区别。
- `setVolume(0~100)` 设置音量,配合 `QSlider` 实现音量条;按钮点击时 ±5。
- 全屏通过隐藏左侧列表容器实现,`resizeEvent()` 重写保证底部控制条随窗口大小重新布局。
- 进度条拖动逻辑与音乐播放器相同(`durationChanged` / `positionChanged` / `sliderReleased`)。

**运行效果**:左侧显示视频列表,右侧 `QVideoWidget` 播放视频,底部有进度条与播放、下一集、音量、全屏按钮;音量条拖动实时生效,全屏按钮隐藏列表。

### 3.5 录音(16_audiorecorder)

例程 `16_audiorecorder`:用 `QAudioRecorder` 录音为 mp3,用 `QAudioProbe` 实时探测音频缓冲区显示音量 level,并支持回放、上一首/下一首、删除录音。

**录音数据结构(audiorecorder.h 节选):**

cpp #include #include #include #include #include #include

class AudioRecorder : public QMainWindow {

Q_OBJECT

public:

AudioRecorder(QWidget *parent = nullptr);
~AudioRecorder();

private:

void layoutInit();
QAudioRecorder *m_audioRecorder = nullptr;
QAudioProbe *m_probe = nullptr;
QMediaPlayer *recorderPlayer;
QMediaPlaylist *mediaPlaylist;
QProgressBar *progressBar[4];
QVector<MediaObjectInfo> mediaObjectInfo;

/* 录音设置容器,保存录音设备的可用信息 */
QList<QVariant> devicesVar, codecsVar, containersVar,
                sampleRateVar, channelsVar, qualityVar, bitratesVar;

void scanRecordFiles();
void clearAudioLevels();

private slots:

void recorderBtClicked();
void processBuffer(const QAudioBuffer&);
void updateProgress(qint64);
// ... 播放相关槽 ...

};


**录音初始化(构造函数节选):**

cpp /* 录制音频的类 */ m_audioRecorder = new QAudioRecorder(this);

/* 用于探测缓冲区的数据 */ m_probe = new QAudioProbe(this);

/* 信号槽连接,更新录音 level 显示 */ connect(m_probe, &QAudioProbe::audioBufferProbed,

    this, &AudioRecorder::processBuffer);

/* 设置探测的对象 */ m_probe->setSource(m_audioRecorder);

/* 播放器与播放列表 */ recorderPlayer = new QMediaPlayer(this); mediaPlaylist = new QMediaPlaylist(this); recorderPlayer->setPlaylist(mediaPlaylist); mediaPlaylist->setPlaybackMode(QMediaPlaylist::CurrentItemOnce);

/* 扫描本地声卡设备,保存到 devicesVar */ devicesVar.append(QVariant(QString())); for (auto &device : m_audioRecorder->audioInputs())

devicesVar.append(QVariant(device));

/* 支持的音频编码 */ for (auto &codecName : m_audioRecorder->supportedAudioCodecs())

codecsVar.append(QVariant(codecName));

/* 支持的容器格式 */ for (auto &containerName : m_audioRecorder->supportedContainers())

containersVar.append(QVariant(containerName));

/* 支持的采样率 */ for (int sampleRate : m_audioRecorder->supportedAudioSampleRates())

sampleRateVar.append(QVariant(sampleRate));

/* 录音时长变化 */ connect(m_audioRecorder, &QAudioRecorder::durationChanged,

    this, &AudioRecorder::updateProgress);

**开始/停止录音:**

cpp void AudioRecorder::recorderBtClicked() {

/* 录音前停止正在播放的媒体 */
if (recorderPlayer->state() != QMediaPlayer::StoppedState)
    recorderPlayer->stop();

/* 如果录音已经停止,则开始录音 */
if (m_audioRecorder->state() == QMediaRecorder::StoppedState) {
    /* 设置默认的录音设备 */
    m_audioRecorder->setAudioInput(devicesVar.at(0).toString());

    /* 下面的是录音设置,都是选择默认,可根据录音可用项自行修改 */
    QAudioEncoderSettings settings;
    settings.setCodec(codecsVar.at(0).toString());
    settings.setSampleRate(sampleRateVar[0].toInt());
    settings.setBitRate(bitratesVar[0].toInt());
    settings.setChannelCount(channelsVar[0].toInt());
    settings.setQuality(QMultimedia::EncodingQuality(
                            qualityVar[0].toInt()));
    /* 以恒定的质量录制,可选恒定的比特率 */
    settings.setEncodingMode(QMultimedia::ConstantQualityEncoding);
    QString container = containersVar.at(0).toString();
    m_audioRecorder->setEncodingSettings(settings,
                                         QVideoEncoderSettings(),
                                         container);
    m_audioRecorder->setOutputLocation(
        QUrl::fromLocalFile(
            QCoreApplication::applicationDirPath()
            + tr("/Sounds/%1.mp3")
              .arg(QDateTime::currentDateTime().toString())));
    /* 开始录音 */
    m_audioRecorder->record();
    countLabel->clear();
    centerWidget->show();
} else {
    /* 停止录音 */
    m_audioRecorder->stop();
    clearAudioLevels();
    centerWidget->hide();
    /* 重新扫描录音文件 */
    scanRecordFiles();
}

}

/* 录音时长更新 */ void AudioRecorder::updateProgress(qint64 duration) {

if (m_audioRecorder->error() != QMediaRecorder::NoError)
    return;
/* 显示录制时长 */
countLabel->setText(tr("已录制 %1 s").arg(duration / 1000));

}


**扫描录音文件(与音乐播放器类似):**

cpp void AudioRecorder::scanRecordFiles() {

mediaPlaylist->clear();
listWidget->clear();
mediaObjectInfo.clear();

/* 录音文件保存在当前 Sounds 文件夹下 */
QDir dir(QCoreApplication::applicationDirPath() + "/Sounds");
QDir dirbsolutePath(dir.absolutePath());

/* 如果文件夹不存在,则创建一个 */
if (!dirbsolutePath.exists())
    dirbsolutePath.mkdir(dirbsolutePath.absolutePath());

QStringList filter;
filter << "*.mp3";
QFileInfoList files =
        dirbsolutePath.entryInfoList(filter, QDir::Files);
for (int i = 0; i < files.count(); i++) {
    MediaObjectInfo info;
    info.fileName = QString::fromUtf8(
                files.at(i).fileName().toUtf8().data());
    info.filePath = QString::fromUtf8(
                files.at(i).filePath().toUtf8().data());
    if (mediaPlaylist->addMedia(
                QUrl::fromLocalFile(info.filePath))) {
        mediaObjectInfo.append(info);
        listWidget->addItem(new QListWidgetItem(
                                QIcon(":/icons/play.png"),
                                info.fileName));
    } else {
        qDebug() << mediaPlaylist->errorString().toUtf8().data();
    }
}

}


**音量 level 探测(QAudioProbe → QAudioBuffer → 进度条):**

cpp void AudioRecorder::processBuffer(const QAudioBuffer &buffer) {

/* 根据通道数目需要显示 count 个 level */
int count = buffer.format().channelCount();
for (int i = 0; i < 4; i++) {
    if (i < count)
        progressBar[i]->setVisible(true);
    else
        progressBar[i]->setVisible(false);
}

/* 设置 level 的值 */
QVector<qreal> levels = getBufferLevels(buffer);
for (int i = 0; i < levels.count(); ++i)
    progressBar[i]->setValue(levels.at(i) * 100);

}


`getBufferLevels()` 会判断采样类型(`Float`/`SignedInt`/`UnSignedInt`)、采样位宽(8/16/32 位),用 `getPeakValue()` 求出该格式的最大采样值,再计算各通道的峰值并归一化到 0~1,最后乘以 100 作为进度条的值。

**逐段解释**:

- `QAudioRecorder` 是录音核心,负责选择输入设备、编码设置和输出文件。
- `QAudioEncoderSettings` 设置编码器、采样率、比特率、通道数、质量与编码模式;`QVideoEncoderSettings()`(空)与 `container` 一起传给 `setEncodingSettings()`。
- `setOutputLocation(QUrl::fromLocalFile(...))` 指定输出文件,这里用当前日期时间命名,保存到 `Sounds` 目录。
- `QAudioProbe::audioBufferProbed` 信号在每次音频缓冲区就绪时发出,槽函数 `processBuffer()` 计算音量并更新 level 进度条,实现「录音时音量条跳动」的效果。
- `state() == QMediaRecorder::StoppedState` 用来区分「开始录音」和「停止录音」两种点击。

**运行效果**:点击录音按钮开始录音,窗口中央显示已录制时长与跳动的音量条;再次点击停止并刷新列表。点击列表项播放录音,可上一首/下一首/删除(删除会同时删除文件与列表项)。首次运行会自动创建 `Sounds` 目录。

---

## 4. 数据库

### 4.1 Qt SQL 模块简介

**数据库是什么?** 简单说就是保存数据的文件,可以存储大量数据,支持插入、更新、删除等操作。专业上说,数据库是「按照数据结构来组织、存储和管理数据的仓库」。

**什么时候需要数据库?** 在嵌入式里,需要存储大量数据或记录数据时就需要。例如手机的闹钟:设置的闹钟数据保存到数据库,程序重新运行时会从数据库读回上次保存的数据;如果没有数据库,关机后数据丢失,明显不合理。

Qt SQL 模块为数据库提供编程支持,支持 MySQL、Oracle、MS SQL Server、SQLite 等。它包含连接数据库、执行 SQL、获取数据与界面显示等类,一般数据与界面之间采用 **Model/View 架构**。

**嵌入式里一般用 SQLite3**:非常小、轻量级,完全配置时小于 400KiB,省略可选功能时小于 250KiB。它是一个进程内的库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。SQLite 引擎不是独立进程,可按应用需求静态或动态连接,直接访问其存储文件。

在 `.pro` 里添加:

pro QT += core gui sql


### 4.2 Qt 数据库操作流程

Qt 对数据库的基本操作流程:

1. `QSqlDatabase` 加载某一类型的数据库驱动后,用 `open()` 打开(连接)数据库;
2. 用 `QSqlQuery` 创建数据库表,用 `exec()` 执行增删查改 SQL;
3. 通过 `QTableView` 显示数据库内容:设置 Model 为 `QSqlTableModel`(或 `QSqlQueryModel`);
4. 直接编辑 `QTableView` 里的内容也可以修改数据库表格内容。

mermaid flowchart LR

A["QSqlDatabase<br/>加载驱动 + open()"] --> B["QSqlQuery<br/>exec() 建表/增删查改"]
B --> C["QSqlTableModel / QSqlQueryModel<br/>读取表格模型"]
C --> D["QTableView / QListWidget<br/>显示与编辑"]

### 4.3 Qt SQL 核心类与 API

| 类 | 作用 | 常用 API |
| -- | ---- | -------- |
| `QSqlDatabase` | 数据库连接 | `addDatabase(driver)`、`setDatabaseName(name)`、`open()`、`close()`、`lastError()`、`drivers()` |
| `QSqlQuery` | 执行 SQL 语句 | `exec(sql)`、`prepare(sql)`、`bindValue(placeholder, value)`、`first()`、`next()`、`value(field)` |
| `QSqlTableModel` | 单表模型,可编辑 | `setTable()`、`setEditStrategy()`、`select()`、`insertRow()`、`setData()`、`data()`、`submit()`、`submitAll()`、`removeRow()`、`isDirty()`、`rowCount()` |
| `QSqlQueryModel` | 查询结果模型,只读 | `setQuery()`、`record()`、`setHeaderData()`、`lastError()` |
| `QDataWidgetMapper` | 模型字段映射到控件 | `setModel()`、`setSubmitPolicy()`、`addMapping()`、`setCurrentModelIndex()` |
| `QItemSelectionModel` | 选择模型 | `currentRowChanged` 信号、`currentIndex()` |
| `QTableView` | 表格视图 | `setModel()`、`setSelectionModel()`、`hideColumn()`、`horizontalHeader()` |

`QSqlTableModel::EditStrategy` 三种编辑策略:

| 策略 | 说明 |
| ---- | ---- |
| `OnFieldChange` | 字段改变即同步到数据库 |
| `OnRowChange` | 行改变(切换到别的行)时同步 |
| `OnManualSubmit` | 手动调用 `submit()` / `submitAll()` 才提交 |

> **注意**:`setEditStrategy(OnFieldChange)` 这类规则需要与 `QTableView` 这样的控件配合才生效,因为 `QTableView` 可以直接编辑表里的内容。

### 4.4 实用闹钟(17_sqlite_alarm)

例程 `17_sqlite_alarm`:结合数据库开发一个闹钟,记录新建的闹钟数据,支持增、删、改操作,但不使用 `QTableView` 显示(用 `QListWidget` + 自定义开关),界面与手机闹钟类似。本例篇幅很长,只关注 `mainwindow.h` 和 `mainwindow.cpp` 的数据库操作;`numberpicker.*`(数字选择器)和 `switchbutton.*`(闹钟开关)是为界面美观自行设计的自定义控件,可直接当普通控件使用。

**数据库初始化与建表(构造函数节选):**

cpp #include #include #include #include

/* 查看本机可用的数据库驱动 */ QStringList drivers = QSqlDatabase::drivers(); foreach (QString driver, drivers) {

qDebug() << driver;

}

/* 以 QSQLITE 驱动方式打开或者创建数据库 / sqlDatabase = QSqlDatabase::addDatabase("QSQLITE"); sqlDatabase.setDatabaseName("alarm.db"); / 以 open 的方式打开 alarm.db 数据库,则会创建一个 alarm.db */ if (!sqlDatabase.open())

qDebug() << "连接数据库错误" << sqlDatabase.lastError() << endl;

else

qDebug() << "连接数据库成功" << endl;

QSqlQuery query(sqlDatabase); /* 使用指令式创建表 */ query.exec("create table alarm (id int primary key, time vchar(15), flag vchar(5))");

model = new QSqlTableModel(this, sqlDatabase);

/* 模型设置表的名字,需要与数据库的表的名字相同 */ model->setTable("alarm");

/* 如果有修改则同步修改到数据库,

  • 注意这个规则需要与 tableView 这样的控件才生效 */ model->setEditStrategy(QSqlTableModel::OnFieldChange);

/* 成功则返回 true,查看数据库里是否有 alarm 这个表格 */ model->select();

/* 如果数据表数据为空,则添加两个闹钟 */ if (model->rowCount() == 0) {

/* 插入一行 */
model->insertRow(model->rowCount());
/* 在该行插入数据 */
model->setData(model->index(0, 0), 1);
model->setData(model->index(0, 1), "06:00");
model->setData(model->index(0, 2), "false");
/* 插入数据后记得提交 */
model->submit();

/* 再插入一行 */
model->insertRow(model->rowCount());
model->setData(model->index(1, 0), 2);
model->setData(model->index(1, 1), "18:00");
model->setData(model->index(1, 2), "true");
model->submit();

}


**读数据到列表(构造函数节选):**

cpp /* 打印出闹钟数据库里的信息并添加到列表 */ for (int i = 0; i < model->rowCount(); i++) {

for (int j = 0; j < 3; j++) {
    QModelIndex qindex = model->index(i, j);
    switch (j) {
    case 0:
        qDebug() << "第" << model->data(qindex).toInt() << "行数据";
        break;
    case 1:
        listWidget->addItem(model->data(qindex).toString());
        qDebug() << "闹钟时间为:" << model->data(qindex).toString();
        break;
    case 2:
        qDebug() << "闹钟状态为:" << model->data(qindex).toString();
        if (model->data(qindex).toString() != "true")
            listWidget->item(i)->setTextColor(QColor(22, 22, 22, 60));
        else
            listWidget->item(i)->setTextColor(QColor(22, 22, 22, 225));
        break;
    }
}

}

/* 在列表里添加闹钟开关 */ for (int i = 0; i < model->rowCount(); i++) {

ItemObjectInfo info;
info.widget = new QWidget();
info.switchButton = new SwitchButton();
info.hBoxLayout = new QHBoxLayout();
info.switchButton->setMaximumSize(55, 30);
info.switchButton->setMinimumSize(55, 30);
info.hBoxLayout->setContentsMargins(0, 0, 0, 0);
info.hBoxLayout->setAlignment(Qt::AlignRight);
info.hBoxLayout->addWidget(info.switchButton);
info.widget->setLayout(info.hBoxLayout);
listWidget->setItemWidget(listWidget->item(i), info.widget);
itemObjectInfo.append(info);

connect(info.switchButton, SIGNAL(toggled(bool)),
        this, SLOT(switchButtonClicked(bool)));

/* 获取数据库里的闹钟开关状态 */
QModelIndex qindex = model->index(i, 2);
if (model->data(qindex).toBool())
    info.switchButton->setToggle(true);

}


**新增/修改闹钟(确认按钮):**

cpp void MainWindow::yesButtonClicked() {

/* 获取数值选择值的数据,转为字符串 */
QString hour;
QString minute;

if (hourPicker->readValue() < 10)
    hour = "0" + QString::number(hourPicker->readValue()) + ":";
else
    hour = QString::number(hourPicker->readValue()) + ":";

if (minutePicker->readValue() < 10)
    minute = "0" + QString::number(minutePicker->readValue());
else
    minute = QString::number(minutePicker->readValue());

/* 如果不是选中闹钟列表的数据 */
if (listWidget->currentRow() == -1) {
    /* 插入一行数据,闹钟时间为选择的闹钟时间 */
    int row = model->rowCount();

    /* 插入数据到数据库 */
    model->insertRow(row);
    model->setData(model->index(row, 0), row + 1);
    model->setData(model->index(row, 1), hour + minute);
    model->setData(model->index(row, 2), "true");
    model->submit();

    /* 添加闹钟到列表(含开关控件,略) */
    listWidget->addItem(hour + minute);
    // ... 创建 SwitchButton 并 setItemWidget ...
} else {
    /* 修改数据(更新闹钟数据) */
    int row = listWidget->currentRow();
    model->setData(model->index(row, 0), row + 1);
    model->setData(model->index(row, 1), hour + minute);
    model->setData(model->index(row, 2), "true");
    model->submit();

    /* 设置当前项的闹钟文本 */
    listWidget->currentItem()->setText(hour + minute);
}

/* 再确保提交 */
if (model->isDirty())
    model->submitAll();

/* 关闭对话框 */
alarmDialog->close();

}


**删除闹钟(取消按钮在"删除"模式):**

cpp void MainWindow::cancelButtonClicked() {

if (cancelButton->text() == "删除") {
    /* 删除数据库整一行数据 */
    model->removeRow(listWidget->currentRow());
    model->submit();
    /* 重新查询刷新模型 */
    model->select();
    itemObjectInfo.remove(listWidget->currentRow());
    listWidget->takeItem(listWidget->currentRow());
}

if (model->isDirty())
    model->submitAll();

alarmDialog->close();

}


**开关状态同步到数据库:**

cpp void MainWindow::switchButtonClicked(bool checked) {

listWidget->clearSelection();

SwitchButton *button = (SwitchButton *)sender();
for (int i = 0; i < itemObjectInfo.count(); i++) {
    if (button == itemObjectInfo.at(i).switchButton) {
        if (checked) {
            model->setData(model->index(i, 2), "true");
            listWidget->item(i)->setTextColor(QColor(22, 22, 22, 225));
        } else {
            model->setData(model->index(i, 2), "false");
            listWidget->item(i)->setTextColor(QColor(22, 22, 22, 60));
        }
        model->submit();
        break;
    }
}

}


**逐段解释**:

- `QSqlDatabase::addDatabase("QSQLITE")` 加载 SQLite 驱动,`setDatabaseName("alarm.db")` 指定数据库文件名,`open()` 打开——文件不存在会自动创建。
- `QSqlQuery::exec()` 直接执行 SQL 建表语句;字段 `flag` 保存闹钟开关状态(`"true"` / `"false"`)。
- `QSqlTableModel` 是单表模型,`setTable("alarm")` 指定表名,`select()` 查询;`rowCount()`、`data()`、`setData()` 操作模型数据。
- **`setData()` 后要 `submit()`**:`setData()` 只改模型,`submit()` 才写回数据库(`OnFieldChange` 策略下部分场景自动提交,但显式提交更可靠)。
- `insertRow(row)` 插入空行后再 `setData()` 填充;`removeRow(row)` 删除行。
- `isDirty()` 判断模型是否有未提交改动,`submitAll()` 一次性提交。
- 开关按钮 `SwitchButton` 发射 `toggled(bool)` 信号,主线程用 `sender()` 判断是哪一个开关,从而更新对应行的 `flag` 字段,实现"界面状态 ↔ 数据库"双向同步。

**运行效果**:程序启动显示闹钟列表,每条有开关;点击开关变灰/变亮并同步数据库;点「+」添加闹钟(弹窗选时分),点已有闹钟可修改或删除;重启程序后数据仍在(保存在 `alarm.db`)。

### 4.5 数据库表格显示(18_sqlite_table)

例程 `18_sqlite_table`:用 `QTableView` + `QSqlQueryModel` 显示员工表,用 `QDataWidgetMapper` 把选中行的字段映射到底部编辑控件,并读取数据库中的照片路径显示照片。

**数据库初始化与建表:**

cpp #include #include #include #include

/* 查看本机可用的数据库驱动 */ QStringList drivers = QSqlDatabase::drivers(); foreach (QString driver, drivers) {

qDebug() << driver;

}

/* 以 QSQLITE 驱动方式打开或者创建数据库 / sqlDatabase = QSqlDatabase::addDatabase("QSQLITE"); sqlDatabase.setDatabaseName("employee.db"); / 以 open 的方式打开 employee.db 数据库,则会创建一个 employee.db */ if (!sqlDatabase.open())

qDebug() << "连接数据库错误" << sqlDatabase.lastError() << endl;

else

qDebug() << "连接数据库成功" << endl;

QSqlQuery query(sqlDatabase); /* 使用指令式创建表 */ query.exec("create table employee (id int primary key, name vchar(10), "

       "age int, sex vchar(3), photo text)");

QStringList photoPath; /* 当前可执行程序的路径 */ QString path(QApplication::applicationDirPath()); photoPath << path + "/photos/啊万.jpg" << path + "/photos/啊棠.jpg";

/* 以指令的方式插入数据,如果数据已经存在则不会成功不能插入 */ query.exec(tr("insert into employee values(1, '啊万', 27, '男', '%1')")

       .arg(photoPath[0]));

query.exec(tr("insert into employee values(2, '啊棠', 28, '女', '%1')")

       .arg(photoPath[1]));

**只读查询模型 + QTableView:**

cpp /* QSqlQueryModel 适合用于查询数据,不能修改数据 */ sqlQueryModel = new QSqlQueryModel(this);

/* 选择编号、姓名、年龄和性别的内容显示到 tableView 上,

  • 图片最后通过数据选择再读取到 Label 上 */ sqlQueryModel->setQuery("select id, name, age, sex from employee");

if (sqlQueryModel->lastError().isValid())

qDebug() << "选择数据失败!" << endl;

sqlQueryModel->setHeaderData(0, Qt::Horizontal, "编号"); sqlQueryModel->setHeaderData(1, Qt::Horizontal, "姓名"); sqlQueryModel->setHeaderData(2, Qt::Horizontal, "年龄"); sqlQueryModel->setHeaderData(3, Qt::Horizontal, "性别");

tableView = new QTableView(); tableView->setModel(sqlQueryModel);

/* 设置显示平均分列 */ tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);


**选中行 → 读取照片:**

cpp void MainWindow::on_currentRowChanged(const QModelIndex &current,

                                  const QModelIndex &previous)

{

Q_UNUSED(previous)
/* 更新数据映射行号,初始化时映射到第 0 行 */
dataWidgetMapper->setCurrentModelIndex(current);

/* 获取当前行号 */
int row = itemSelectionModel->currentIndex().row();
/* 获取当前模型记录 */
QSqlRecord record = sqlQueryModel->record(row);
/* 获取 id 信息 */
int id = record.value("id").toInt();

QSqlQuery query;
/* 使用 bindValue 绑定 prepare 里语句的值,需要使用 ":","是占位符" */
query.prepare("select photo from employee where id = :ID");
query.bindValue(":ID", id);
query.exec();
/* 返回到选择的第一条记录,因为 id 是唯一的,也只有一条记录 */
query.first();

/* 获取字段为 photo 的值,也就是存储照片的路径 */
QVariant temp = query.value("photo");
if (!temp.isValid()) {
    qDebug() << "数据无效!" << endl;
    return;
}

imageLabel->clear();
QImage image(temp.toString());
if (image.isNull()) {
    qDebug() << "未找到" << temp.toString() << endl;
    return;
}
/* 显示照片 */
imageLabel->setPixmap(QPixmap::fromImage(image));

}


**数据映射(把模型字段映射到底部控件):**

cpp itemSelectionModel = new QItemSelectionModel(sqlQueryModel); tableView->setSelectionModel(itemSelectionModel);

/* 信号槽连接,表示表中行数据变化时,触发槽函数 */ connect(itemSelectionModel,

    SIGNAL(currentRowChanged(QModelIndex, QModelIndex)),
    this,
    SLOT(on_currentRowChanged(QModelIndex, QModelIndex)));

dataWidgetMapper = new QDataWidgetMapper(this); /* 设置为自动提交 / dataWidgetMapper->setSubmitPolicy(QDataWidgetMapper::AutoSubmit); dataWidgetMapper->setModel(sqlQueryModel); / 创建数据映射,将前面的数据库内容映射到控件上 / dataWidgetMapper->addMapping(lineEdit, 1); / 姓名 → 单行输入框 / dataWidgetMapper->addMapping(spinBox[0], 0); / 编号 → 数值框 / dataWidgetMapper->addMapping(spinBox[1], 2); / 年龄 → 数值框 / dataWidgetMapper->addMapping(comboBox, 3); / 性别 → 下拉框 */


**另一种写法:可编辑的 QSqlTableModel(注释中给出):**

cpp QSqlTableModel model = new QSqlTableModel(this, sqlDatabase); model->setTable("employee"); model->setEditStrategy(QSqlTableModel::OnFieldChange); model->select(); / 设置表格的头信息,若不设置则显示数据库里的英文字段头信息 */ model->setHeaderData(model->fieldIndex("id"), Qt::Horizontal, tr("编号")); model->setHeaderData(model->fieldIndex("name"), Qt::Horizontal, tr("姓名")); model->setHeaderData(model->fieldIndex("age"), Qt::Horizontal, tr("年龄")); model->setHeaderData(model->fieldIndex("sex"), Qt::Horizontal, tr("性别"));

QTableView view = new QTableView; view->setModel(model); view->hideColumn(4); / 不显示图片路径信息列 */ setCentralWidget(view);


**逐段解释**:

- `QSqlQueryModel` 与 `QSqlTableModel` 的区别:前者适合**只读查询**,`setQuery()` 传入任意 SQL;后者针对**单表**且可编辑,适合 `QTableView` 直接改数据。
- `setHeaderData()` 设置表头显示的中文名,不设置则显示数据库英文字段。
- `QItemSelectionModel` 发出 `currentRowChanged` 信号,实现"选中表格某行 → 更新下方详情"。
- `QDataWidgetMapper` 把模型的列映射到具体控件(输入框、数值框、下拉框),随选中行自动更新控件内容;`AutoSubmit` 表示控件修改自动提交回模型。
- `QSqlQuery::prepare()` + `bindValue(":ID", id)` 使用带占位符的预处理语句,避免 SQL 注入,也便于复用。
- 照片路径存在数据库的 `photo` 文本字段中,查询出来后用 `QImage`/`QPixmap` 加载显示。

**运行效果**:窗口上方是表格(只显示编号/姓名/年龄/性别),选中一行后下方对应控件显示该行数据,右侧显示该员工的照片;编辑图片路径外的字段会更新到模型。

---

## 5. 构建与运行

### 5.1 各例程所需模块

| 例程 | `.pro` 额外模块 |
| ---- | --------------- |
| 05、06 多线程 | 无(`core gui` 即可) |
| 07、08、09、10、11、12 网络 | `QT += network` |
| 13 按钮音效 | `QT += multimedia` |
| 14 音乐播放器 | `QT += multimedia` |
| 15 视频播放器 | `QT += multimedia multimediawidgets` |
| 16 录音 | `QT += multimedia` |
| 17、18 数据库 | `QT += core gui sql` |

### 5.2 Ubuntu 上编译运行

bash

用 Qt Creator 打开对应 .pro,选择 Desktop Qt 套件运行;

或命令行:

qmake 14_musicplayer.pro make -j4 ./14_musicplayer ```

前置条件:

  • 网络例程可直接运行(无需额外依赖);
  • 多媒体例程需先安装 GStreamer 解码插件(见 3.1 节);
  • 数据库例程使用 QSQLITE 驱动,Qt 自带,无需额外安装;
  • 音乐/视频/录音例程需要在可执行文件所在目录准备 myMusic / myVideo / Sounds 目录(录音会自动创建)。

5.3 I.MX6ULL 开发板上运行

  1. 用带对应模块的交叉编译 Qt 套件编译。嵌入式 Qt 通常已裁剪,需确认包含 networkmultimedia(含 multimediawidgets)、sql 及其插件(sqldrivers/libqsqlite.so、多媒体后端插件);
  2. 把可执行文件及依赖资源(音视频文件目录、图片资源)拷贝到开发板;
  3. 在开发板 Qt 运行环境下执行。

要点:

  • SQLite 驱动插件:开发板上的 Qt 必须带 sqldrivers/libqsqlite.so,否则 QSqlDatabase::addDatabase("QSQLITE") 会失败;可用 QSqlDatabase::drivers() 打印确认;
  • 多媒体后端:嵌入式上通常用 GStreamer 或 Qt 自带的音视频插件,播放失败时先检查插件与解码器;
  • 网络程序:开发板与 PC 需处于同一网段;组播/广播需网络设备支持。

6. 面试精选

6.1 Qt 有哪两种多线程写法?区别是什么?

一是继承 QThread 并重写 run(),只有 run() 在子线程执行,类中其他方法仍在主线程;二是继承 QObject,用 QObject::moveToThread() 把整个对象移到线程,该对象的所有槽函数都在子线程执行。Qt 4.8 之后官方推荐第二种,因为它更灵活:可以有 doWork1()doWork2() 等多份耗时工作,且通过信号槽调用天然跨线程。继承 QThread 时只有 run() 属于新线程,容易误以为其它方法也在子线程而写错。

6.2 为什么不能直接 terminate() 终止线程?怎样安全终止?

QThread::terminate() 会在任意时刻强行结束线程,此时线程可能正持有锁、正在修改共享数据,会导致资源泄漏、死锁或数据不一致,因此官方不推荐。安全做法是:在 worker 中用一个 bool 标志位控制循环,主线程通过调用一个加锁的方法(如 stopWork())把标志位置 false,循环检测到后 break 退出;随后再 quit() + wait() 让线程正常结束。本例用 QMutexLocker 保护标志位,保证两个线程访问互斥。

6.3 QMutexLocker 相比手动 lock()/unlock() 有什么好处?

QMutexLocker 是 RAII 封装:构造时加锁,析构(离开作用域)时自动解锁。即使中途 return、抛异常或提前 break,也能保证解锁,避免忘记 unlock() 造成的死锁。用法上把需要保护的代码放到一个 {} 作用域里即可;应尽量缩小持锁范围,不要在持锁期间做耗时操作(如本例 QThread::sleep(2) 放在锁外)。

6.4 Qt 中 TCP 与 UDP 的用法有何区别?read()/write() 对应传统的什么函数?

TCP 面向连接:服务端用 QTcpServer::listen() 监听,newConnection() 信号后在槽里 nextPendingConnection() 取得通信 socket;客户端用 QTcpSocket::connectToHost() 主动连接,连接成功后收发。UDP 无连接:用 QUdpSocket::bind() 绑定端口即可接收,writeDatagram() 发送,readDatagram() 接收并同时获得发送方地址端口;支持单播、广播(QHostAddress::Broadcast)和组播(joinMulticastGroup())。Qt 把 socket 当作 I/O 流,用 read()/write() 收发数据,对应传统 C socket 的 recv()/send()

6.5 Qt SQL 中 QSqlTableModelQSqlQueryModel 有何区别?如何把表显示到界面?

QSqlTableModel 面向单张表,可读可写,配合 QTableView 可直接编辑并同步到数据库,常用 setTable()setEditStrategy()select()insertRow()setData()submit()removeRow()QSqlQueryModel 面向任意查询,只读,用 setQuery() 执行 SQL、record() 取行数据。显示时用 QTableView::setModel() 设置模型即可,表头用 setHeaderData() 改成中文;若要把某行字段映射到独立控件,用 QItemSelectionModel 监听 currentRowChanged,再配合 QDataWidgetMapper::addMapping() 把列映射到输入框等控件。


延伸阅读

  • Linux TCP 编程(socket API 底层原理):[[Linux+C+C++技术体系梳理/2. Linux系统编程/15. TCP编程]]
  • 文本与绘图基础:[[05-Qt应用开发/06-文本绘图与图表]]
  • 嵌入式交叉编译与项目实战:[[05-Qt应用开发/08-嵌入式Qt交叉编译与项目实战]]

内容来源:正点原子《I.MX6U嵌入式Qt开发指南V1.2》第十章 多线程(10.1 继承 QThread 的线程、10.2 继承 QObject 的线程)、第十一章 网络编程(11.1 获取本机的网络信息、11.2 TCP 通信、11.3 UDP 通信、11.4 网络下载实例)、第十二章 多媒体(12.1 Qt 多媒体简介、12.2 音效文件播放、12.3 音乐播放器、12.4 视频播放器、12.5 录音)、第十三章 数据库(13.1 Qt SQL 简介、13.2 应用实例);例程 Qt/02/05_qthread_example1 ~ 18_sqlite_table 共 14 个。