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
💡 关联知识:[[05-Qt应用开发/06-文本绘图与图表]]、[[05-Qt应用开发/08-嵌入式Qt交叉编译与项目实战]]、[[05-Qt应用开发/07-多线程网络多媒体与数据库]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/15. TCP编程]]
本篇是 PDF 第二篇提高篇的最后一组内容,把四块「让界面能用起来」的能力串在一起:
QThread 把耗时操作放到子线程;QTcpSocket/QTcpServer/QUdpSocket 做 TCP/UDP 通信,用 QHostInfo 查本机信息,用 QNetworkAccessManager 下载文件;QSound 播音效、QMediaPlayer 播音乐和视频、QAudioRecorder 录音;QSqlQuery 执行 SQL、QSqlTableModel/QSqlQueryModel 把表映射到界面。对应例程:Qt/02/05_qthread_example1 ~ Qt/02/18_sqlite_table,共 14 个。桌面端与 I.MX6ULL 开发板的界面代码完全一致,差异只在交叉编译环境与可用的多媒体插件/解码器。
应用程序跑起来后一般情况下只有一个线程,就是主线程,线程内的操作都是顺序执行的。假设界面上某个操作比较耗时,例如在嵌入式 CPU 上拍照保存照片(嵌入式 CPU 主频通常远低于 PC,处理速度也慢):
用户体验很差。解决办法是创建一个单独的线程来执行耗时操作,主线程继续负责界面刷新。
官方文档:
QThread类提供了一种独立于平台的方法来管理线程。QThread对象在程序中管理一个控制线程,线程在run()中开始执行。默认情况下run()通过调用exec()启动事件循环,并在线程中运行 Qt 事件循环。也可以通过QObject::moveToThread()把 worker 对象移动到线程中使用。
QThread 是 Qt 实现多线程的核心类。Qt 有两种多线程写法:
| 方法 | 核心思想 | 说明 |
|---|---|---|
继承 QThread,重写 run() |
子类中只有 run() 在新线程执行 |
Qt 4.8 之前的主流写法 |
继承 QObject,moveToThread() |
整个 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 提供了 QMutex、QMutexLocker、QReadLocker、QWriteLocker 等类用于线程之间的同步。
例程 05_qthread_example1。通过点击按钮开启线程,线程执行完成时发送 resultReady(const QString &s) 信号给主线程。
#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
#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();
}
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() 对其影响有限,主要是演示退出流程。点击「开启线程」按钮后延时 2s,应用程序输出窗口打印「线程开启成功」。2s 内多次点击不会重复开启线程(线程还在运行),且点击按钮没有卡顿——延时操作在子线程里,按钮在主线程里。
关闭程序后,子线程在主线程析构函数里退出,等待 2s 后打印「线程已经结束」。
例程 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
#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 ¶meter) {
/* 标志位为真 */
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
#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;
}
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) 不会持锁;resultReady。stopWork() 不能放在槽函数下:它是被主线程直接调用的(不是信号触发),所以是普通 public 成员函数,不是槽。里面同样用 QMutexLocker 加锁后把 isCanRun = false,通知循环跳出。QMutexLocker 的价值:在构造时加锁、析构时解锁,即使中途 return/异常也能保证解锁,避免忘记 unlock() 造成死锁。doWork1() 结束不等于线程结束:因为 worker 被移到了线程里,线程会一直存在,直到对象被销毁,或调用 quit()/exit() 退出线程事件循环。stopWork() 让循环跳出,再 workerThread.quit(),最后 wait(2000) 确认退出。点击「开启线程」后,输出窗口每隔 2 秒打印「正在运行 doWork1 函数」;点击「打断线程」后打印「打断 doWork1 函数」,循环结束。再次点击「开启线程」可再次运行 doWork1()。
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。原书只提到 Qt 提供
QMutex、QMutexLocker、QReadLocker、QWriteLocker等类用于线程同步,此处的对照表为 Qt 官方文档标准内容,补充以便实际开发查阅。
| 类 | 作用 | 典型用法 |
|---|---|---|
QMutex |
互斥锁,保护临界区 | lock.lock(); ...; lock.unlock(); |
QMutexLocker |
RAII 方式安全使用 QMutex |
QMutexLocker locker(&lock); 作用域结束自动解锁 |
QReadLocker / QWriteLocker |
配合 QReadWriteLock 的读/写锁 |
读多写少场景提升并发 |
QSemaphore |
信号量,控制同时访问资源的线程数 | 生产者/消费者缓冲 |
QWaitCondition |
条件变量,等待某个条件成立 | 线程间通知 |
⚠️ 易错点:跨线程访问共享变量(如本例的
isCanRun)必须加锁,否则可能出现读到旧值、编译器重排等问题。锁的粒度要尽量小,不要在处理耗时操作时持锁。
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。原书示例通过信号把子线程结果传回主线程,但未展开连接类型,以下为 Qt 官方文档中信号槽跨线程的标准规则。
Qt 的信号槽天生支持跨线程:
Qt::AutoConnection:若信号发射者与接收者不在同一线程,自动使用 Qt::QueuedConnection(把调用排入接收者线程的事件循环,异步执行);在同一线程则用 Qt::DirectConnection(直接调用)。Qt::QueuedConnection 要求参数类型可被元对象系统复制,因此自定义类型需要 qRegisterMetaType() 注册。resultReady 信号、由主线程打印的原因。Qt 网络模块提供了编写 TCP/IP 客户端和服务器的类:
QTcpSocket、QTcpServer、QUdpSocket,代表低层网络概念;QNetworkRequest、QNetworkReply、QNetworkAccessManager,用通用协议执行网络操作;QNetworkConfiguration、QNetworkConfigurationManager、QNetworkSession 等类实现承载管理。在项目中使用 Qt 网络模块,需要在 .pro 文件里增加:
QT += network
为什么先获取本机网络信息? 建立网络通信前至少要获取对方的 IP 地址。网络应用中经常需要本机的主机名、IP 地址、MAC 地址等信息,Qt 提供了 QHostInfo 和 QNetworkInterface 类用于此类信息查询。
例程 07_networkhostinfo。点击「获取本机信息」按钮后延时 1s 刷新并显示网络接口信息,点击「清空文本信息」清空显示。
#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();
}
QHostInfo::localHostName():获取主机名称。QNetworkInterface::allInterfaces():获取所有网络接口列表。直接打印 list 可以看到所有网络信息,但要从接口里提取细节需要 QNetworkAddressEntry。interface.addressEntries():从接口获取所有地址条目。entry.ip().protocol() == QAbstractSocket::IPv4Protocol:一个接口下可能有 IPv4 和 IPv6 两个地址,用协议类型过滤只保留 IPv4。点击「获取本机信息」,文本浏览框打印主机名、网卡名、MAC 地址、IPv4 地址、子网掩码、广播地址。lo 是本地回环网卡,ens33 是 VMware 虚拟出来的网卡(Ubuntu 虚拟机环境)。点击「清空文本信息」清空内容。
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 通信必须先建立连接,通信端分为客户端和服务端:
注意:在 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()
流程:获取本地 IP → 创建 tcpServer 与 tcpSocket → 点击监听即绑定本地 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 的话,客户端仍能继续发数据。流程:获取本地 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。两个程序可运行在同一台机器(服务端监听本机 IP,客户端连接同一 IP)或分别运行在开发板与 Ubuntu 上。服务端先点「开始监听」,客户端点「连接服务端」,双方即可互发消息;服务端会显示客户端 IP 与端口,客户端会显示连接状态。停止监听或断开连接时,双方界面上的按钮状态自动切换。
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["组播:加入该组的主机收到"]
流程:绑定本地端口 → 接收数据 → 向目标 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 并丢弃待处理数据,用于解绑。组播的关键是先绑定端口,再加入组播组,发送时以组播地址和端口为目标。
核心代码:
/* 构造函数中设置组播 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 时会过滤掉环回地址,保证显示的是实际网卡地址。UDP 单播:服务端绑定端口,发送端填写目标 IP 和端口即可互发。广播:发送时选择广播,同一网段所有监听该端口的程序都能收到。组播:双方都加入同一组播地址(如 239.255.255.1)并绑定同一端口,即可互相收发。
例程使用 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。FF D8 开头、FF D9 结尾,用于判断下载到的确实是图片而不是错误页面。QCoreApplication::applicationDirPath():可执行文件所在目录,避免相对路径歧义。运行效果:输入图片 URL(浏览器右键复制图片地址),点击「下载」,进度条随下载推进,完成后图片显示在窗口上方,并保存为可执行文件目录下的「下载图.jpg」。
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_OBJECTpublic:
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_OBJECTpublic:
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_OBJECTpublic:
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 ¤t,
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 开发板上运行
- 用带对应模块的交叉编译 Qt 套件编译。嵌入式 Qt 通常已裁剪,需确认包含
network、multimedia(含multimediawidgets)、sql及其插件(sqldrivers/libqsqlite.so、多媒体后端插件);- 把可执行文件及依赖资源(音视频文件目录、图片资源)拷贝到开发板;
- 在开发板 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 中
QSqlTableModel与QSqlQueryModel有何区别?如何把表显示到界面?
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 个。