时间的拾荒人头像
关注
Qt 网络编程与音视频开发实战:从 UDP/TCP 到 HTTP 与多媒体封面图

Qt 网络编程与音视频开发实战:从 UDP/TCP 到 HTTP 与多媒体

📑 目录


📡 1. Qt 网络部分

💡 笔记:网络编程是 Qt 开发中非常核心的部分。实际开发中,HTTP 协议往往比 TCP/UDP 使用更多一些。HTTP 协议本质也就是通过 TCP 实现的,实现一个 HTTP 客户端/服务器,本质就是基于 TCP socket 进行封装。Qt 只是提供了 HTTP 的客户端接口,而没有提供服务器端的接口。


📌写在前面:使用Qt网络编程的API,需要先在.pro文件中添加network模块,之前的文章中的Qt的各种组件,各种内容,都是包含在QtCore模块中的 - 默认包含。
**此处只需要在.pro文件中的 QT += core gui network上加上network模块即### 🎯 1.1 UDP Socket别#### 📌 1.1.1 核心 API 概览et

📌 1.1.1 核心 API 概览

UDP(User Datagram Protocol,用户数据报协议)是一种无连接的传输层协议。在 Qt 中,主要通过 QUdpSocket 类来实现 UDP 通信。

名称类型说明对标原生 API
bind(const QHostAddress&, quint16 port)方法绑定指定的地址和端口号bind
writeDatagram(const QNetworkDatagram&)方法发送一个 UDP 数据报sendto
readyRead信号有数据到达并准备就绪时触发无(类似于 IO 多路复用中的通知机制)
receiveDatagram()方法接收一个 UDP 数据报recvfrom

💡 笔记:端口号本质就是一个 2 字节的无符号整数,quint16 本质就是一个 unsigned short。

QNetworkDatagram 表⽰⼀个 UDP 数据报.

名称类型说明对标原生 API
QNetworkDatagram(const QByteArray&, const QHostAddress&, quint16)构造函数通过 QByteArray、目标 IP 地址、目标端口号构造一个 UDP 数据报,通常用于发送数据时无
data()方法获取数据报内部持有的数据,返回 QByteArray无
senderAddress()方法获取数据报中包含的对端的 IP 地址无(recvfrom 包含了该功能)
senderPort()方法获取数据报中包含的对端的端口#### 🖥️ 1.1.2 回显服务器该功能)
🖥️ 1.1.2 回显服务器

1) 创建界面:包含一个 QListWidget,用于显示收到的数据。
在这里插入图片描述

2) 创建 QUdpSocket 并初始化

修改 widget.h,添加 QUdpSocket 指针成员:

class Widget : public QWidget
{
    Q_OBJECT
public:
    Widget(QWidget *parent = nullptr);
    ~Widget();
private:
    Ui::Widget *ui;
    // 创建 socket 成员
    QUdpSocket* socket;
};

修改 widget.cpp,实例化 QUdpSocket 并进行初始化:

✏️注意:⼀般来说, 要先连接信号槽, 再绑定端⼝。
如果顺序反过来, 可能会出现端⼝绑定好了之后, 请求就过来了. 此时还没来得及连接信号槽. 那么这个请求就有可能错过了。

#include "widget.h"
#include "ui_widget.h"
#include<QMessageBox>
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    //1.先设置服务器窗口标题
    this->setWindowTitle("服务器");

    //2.实例化socket
    socket = new QUdpSocket(this);

    //3.先连接信号和槽,防止因先绑定端口而没有相应的处理动作导致错过数据
    connect(socket,&QUdpSocket::readDatagram,this,&Widget::processRequest);
    //4.绑定端口号
    bool ret = socket->bind(QHostAddress::Any,8888);
    if(!ret)
    {
        QMessageBox::critical(nullptr,"服务器启动出错",socket->errorString());
        return;
    }
}

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

3) 实现 processRequest 函数

  • 读取请求并解析
  • 根据请求计算响应
  • 把响应写回到客⼾端
#include "widget.h"
#include "ui_widget.h"
#include<QMessageBox>
#include<QNetworkDatagram>
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    //1.先设置服务器窗口标题
    this->setWindowTitle("服务器");

    //2.实例化socket
    socket = new QUdpSocket(this);

    //3.先连接信号和槽,防止因先绑定端口而没有相应的处理动作导致错过数据
    connect(socket,&QUdpSocket::readyRead,this,&Widget::processRequest);
    //4.绑定端口号
    bool ret = socket->bind(QHostAddress::Any,8888);
    if(!ret)
    {
        QMessageBox::critical(nullptr,"服务器启动出错",socket->errorString());
        return;
    }
}

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

void Widget::processRequest()
{
    //1.读取请求
    const QNetworkDatagram& requestDatagram = socket->receiveDatagram();
    QString request = requestDatagram.data();

    //2.根据请求计算响应
    const QString& response = process(request);
    //3.把响应写回到客户端 - 构建一个响应数据包
    QNetworkDatagram responseDatagram(response.toUtf8(),
                                      requestDatagram.senderAddress(),
                                      requestDatagram.senderPort());
    socket->writeDatagram(responseDatagram);
    //显示打印日志
    QString log = "[" + requestDatagram.senderAddress().toString() + ":" +
    QString::number(requestDatagram.senderPort())
     + "] req: " + request + ", resp: " + response;
     ui->listWidget->addItem(log);

}

QString Widget::process(const QString &request)
{

}




4) 实现 process 函数

由于我们此处是实现回显服务器,所以 process 方法中并没有包含实质性的内容:

QString Widget::process(const QString& request)
{
    return request;
}

💡 笔记:"根据请求处理响应"是服务器开发中的最核心的步骤。一个商业服务器程序,这里的逻辑可能是几万行几十万行代码量级的。

此时,服务器程序编写完毕。但是直接运行还看不出效果,还需要搭配客户端来使用。

💻 1.1.3 回显客户端

1) 创建界面:包含一个 QLineEdit、QPushButton、QListWidget。

  • 先使用水平布局把 QLineEdit 和 QPushButton 放好,并设置这两个控件的垂直方向的 sizePolicy 为 Expanding。
  • 再使用垂直布局把 QListWidget 和上面的水平布局放好。
  • 设置垂直布局的 layoutStretch 为 5, 1。

在这里插入图片描述

2) 在 mainwindow.cpp 中,先创建两个全局常量,表示服务器的 IP 和端口

//先定义两个常量,表示服务器的IP和端口号
const QString& IP = "127.0.0.1";
const quint16 PORT = 8888;

3) 创建 QUdpSocket 成员

修改 mainwindow.h,定义成员:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include<QUdpSocket>


QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT


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

private slots:
    void on_pushButton_clicked();

private:
    Ui::MainWindow *ui;

    //创建一个socket
    QUdpSocket* socket;
    //先定义两个常量,表示服务器的IP和端口号
    const QString SERVER_IP = "127.0.0.1";
    const quint16 SERVER_PORT = 8888;
};
#endif // MAINWINDOW_H

修改 mainwindow.cpp,初始化 socket:

#include "mainwindow.h"
#include "ui_mainwindow.h"

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

    //1.设置窗口名称
    this->setWindowTitle("客户端");
    //2.实例化socket
    socket = new QUdpSocket(this);
}

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

4) 给发送按钮 slot 函数,实现发送请求

void MainWindow::on_pushButton_clicked()
{
    //1.获取到输入框的内容
    const QString& text = ui->lineEdit->text();
    //2.构造请求数据
    QNetworkDatagram requestDatagram(text.toUtf8(),
                                     QHostAddress(IP),
                                     PORT);
    //3.发送请求
    socket->writeDatagram(requestDatagram);
    //4.消息添加到列表框中
    ui->listWidget->addItem("客户端说:" + text);
    //5.清空输入框
    ui->lineEdit->setText("");
}

5) 再次修改 MainWindow 的构造函数,通过信号槽,来处理服务器的响应

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include<QLineEdit>
#include<QNetworkDatagram>
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    //1.设置窗口名称
    this->setWindowTitle("客户端");
    //2.实例化socket
    socket = new QUdpSocket(this);

    connect(socket,&QUdpSocket::readyRead,this,[=](){
        const QNetworkDatagram responseDatagram = socket->receiveDatagram();
        QString response = responseDatagram.data();
        ui->listWidget->addItem(QString("服务器说:") + response);
    });
}

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


void MainWindow::on_pushButton_clicked()
{
    //1.获取到输入框的内容
    const QString& text = ui->lineEdit->text();
    //2.构造请求数据
    QNetworkDatagram requestDatagram(text.toUtf8(),
                                     QHostAddress(IP),
                                     PORT);
    //3.发送请求
    socket->writeDatagram(requestDatagram);
    //4.消息添加到列表框中
    ui->listWidget->addItem("客户端说:" + text);
    //5.清空输入框
    ui->lineEdit->setText("");
}

最终执行效果:启动多个客户端都可以正常工作。
在这里插入图片描述


🔗 1.2 TCP Socket

📌 1.2.1 核心 API 概览

核心类是两个:QTcpServer 和 QTcpSocket。

QTcpServer 用于监听端口,和获取客户端连接。

名称类型说明对标原生 API
listen(const QHostAddress&, quint16 port)方法绑定指定的地址和端口号,并开始监听bind 和 listen
nextPendingConnection()方法从系统中获取到一个已经建立好的 tcp 连接。返回一个 QTcpSocket,表示这个客户端的连接。通过这个 socket 对象完成和客户端之间的通信accept
newConnection信号有新的客户端建立连接好之后触发无(但是类似于 IO 多路复用中的通知机制)

QTcpSocket 用于客户端和服务器之间的数据交互。

名称类型说明对标原生 API
readAll()方法读取当前接收缓冲区中的所有数据。返回 QByteArray 对象read
write(const QByteArray&)方法把数据写入 socket 中write
deleteLater方法暂时把 socket 对象标记为无效。Qt 会在下个事件循环中析构释放该对象无(但是类似于"半自动化的垃圾回收")
readyRead信号有数据到达并准备就绪时触发无(但是类似于 IO 多路复用中的通知机制)
disconnected信号连接断开时触发无(但是类似于 IO 多路复用中的通知机制)

💡 笔记:QByteArray 用于表示一个字节数组,可以很方便的和 QString 进行相互转换。例如:使用 QString 的构造函数即可把 QByteArray 转成 QString;使用 QString 的 toUtf8 函数即可把 QString 转成 QByteArray。读写都是使用 QByteArray,与 UDP 不同。

💡 笔记:事件循环,简单理解,可以认为是 Qt 程序内部,带有一个"生物钟"这样的东西,周期性的执行一些逻辑。
deleteLater 函数这个操作,不是立即销毁 clientSocket,而是告诉 Qt,在下一轮事件循环中,再进行上述的销毁操作。槽函数都是在事件循环中执行的,进入下一轮事件循环,意味着上一轮事件循环肯定结束了,也意味着当前的槽函数肯定是执行完毕了。

🖥️ 1.2.2 回显服务器

1) 创建界面:包含一个 QListWidget,用于显示收到的数据。

2) 创建 QTcpServer 并初始化

修改 widget.h,添加 QTcpServer 指针成员:

#ifndef WIDGET_H
#define WIDGET_H
#include<QTcpSocket>
#include <QWidget>
#include<QTcpServer>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

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

    void processConnection();

    QString process(QString request);

private:
    Ui::Widget *ui;
    //创建QTcpServer服务器指针
    QTcpServer* tcpServer;
};
#endif // WIDGET_H

修改 widget.cpp,实例化 QTcpServer 并进行后续初始化操作:

  • 设置窗口标题
  • 实例化 TCP server(父元素设为当前控件,会在父元素销毁时被一起销毁)
  • 通过信号槽,处理客户端建立的新连接
  • 监听端口
#include "widget.h"
#include "ui_widget.h"
#include<QHostAddress>
#include<QMessageBox>
#include<QListWidget>
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    //1.设置窗口标题
    this->setWindowTitle("服务器");

    //2.实例化TcpServer
    tcpServer = new QTcpServer(this);

    //先绑定消息到来时的处理函数
    //3.通过信号槽,处理客户端建立的新链接
    connect(tcpServer,&QTcpServer::newConnection,this,&Widget::processConnection);

    //4.监听端口 - 相当于bind和listen
    bool ret = tcpServer->listen(QHostAddress::Any,8888);
    if(!ret)
    {
        //消息对话框QMessageBox
        QMessageBox::critical(nullptr,"服务器启动失败!",tcpServer->errorString());
        exit(1);
    }
}

3) 继续修改 widget.cpp,实现处理连接的具体方法 processConnection

  • 获取到新的连接对应的 socket
  • 通过信号槽,处理收到请求的情况
  • 通过信号槽,处理断开连接的情况
void Widget::processConnection()
{
    //获取到新链接对应的socket
    QTcpSocket* clientSocket = tcpServer->nextPendingConnection();
    QString log = QString("[") + clientSocket->peerAddress().toString() +
            ":" + QString::number(clientSocket->peerPort()) + "] 客户端上线!";
    ui->listWidget->addItem(log);

    //2.通过信号槽,处理到请求的情况 - 缓冲区中的数据就绪触发信号
    connect(clientSocket,&QTcpSocket::readyRead,this,[=](){
        //a)读取请求
        QString request = clientSocket->readAll();
        //b)根据请求处理响应
        const QString& response = process(request);
        //c)将响应写回客户端 - 将字符串转化为QByteArray(字节数组)
        clientSocket->write(response.toUtf8());

        QString log = QString("[") +clientSocket->peerAddress().toString() + ":"
                +QString::number(clientSocket->peerPort()) +"] req:"
                + request +",resp:" + response;
        ui->listWidget->addItem(log);

    });

    //3.通过信号槽,处理连接断开的情况
    connect(clientSocket,&QTcpSocket::disconnected,this,[=]()
    {
        QString log = QString("[") + clientSocket->peerAddress().toString()
                + ":" + clientSocket->peerPort() + "] 客户端已经下线!";
        //删除clientSocket - 断开的连接会在下一个时间循环启动时清除掉
        clientSocket->deleteLater();
    });

}

4) 实现 process 方法,实现根据请求处理响应

由于我们此处是实现回显服务器,所以 process 方法中并没有包含实质性的内容:

QString Widget::process(const QString &request)
{
    return request;
}

💡 笔记:"根据请求处理响应"是服务器开发中的最核心的步骤。一个商业服务器程序,这里的逻辑可能是几万行几十万行代码量级的。

💻 1.2.3 回显客户端

1) 创建界面:包含一个 QLineEdit、QPushButton、QListWidget。

  • 先使用水平布局把 QLineEdit 和 QPushButton 放好,并设置这两个控件的垂直方向的 sizePolicy 为 Expanding。
  • 再使用垂直布局把 QListWidget 和上面的水平布局放好。
  • 设置垂直布局的 layoutStretch 为 5, 1。

2) 创建 QTcpSocket 并实例化

修改 widget.h,创建成员:

class Widget : public QWidget
{
    Q_OBJECT
public:
    Widget(QWidget *parent = nullptr);
    ~Widget();
private:
    Ui::Widget *ui;
    // 新增 QTcpSocket
    QTcpSocket* socket;
};

修改 widget.cpp,对 QTcpSocket 进行实例化:

  • 设置窗口标题
  • 实例化 socket 对象(父元素设为当前控件,会在父元素销毁时被一起销毁)
  • 和服务器建立连接
  • 等待并确认连接是否出错
#include "widget.h"
#include "ui_widget.h"
#include<QMessageBox>
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    //1.设置窗口标题
    this->setWindowTitle("客户端");

    //2.实例化tcp客户端
    tcpClient = new QTcpSocket(this);

    //3.和tcp服务器建立连接
    tcpClient->connectToHost("127.0.0.1",8888);

    //4.等待并确认连接是否出错
    if(!tcpClient->waitForConnected())
    {
        QMessageBox::critical(nullptr,"连接服务器出错!",tcpClient->errorString());
        exit(1);
    }
}

3) 修改 widget.cpp,给按钮增加点击的 slot 函数,实现发送请求给服务器

void Widget::on_pushButton_clicked()
{
    // 获取输入框的内容
    const QString& text = ui->lineEdit->text();
    // 清空输入框内容
    ui->lineEdit->setText("");
    // 把消息显示到界面上
    ui->listWidget->addItem(QString("客户端说: ") + text);
    // 发送消息给服务器
    socket->write(text.toUtf8());
}

4) 修改 widget.cpp 中的 Widget 构造函数,通过信号槽,处理收到的服务器的响应

// 处理服务器返回的响应.
connect(socket, &QTcpSocket::readyRead, this, [=]() {
    QString response = socket->readAll();
    qDebug() << response;
    ui->listWidget->addItem(QString("服务器说: ") + response);
});

先启动服务器,再启动客户端(可以启动多个),最终执行效果:由于我们使用信号槽处理同一个客户端的多个请求,不涉及到循环,也就不会使客户端之间相互影响了。
在这里插入图片描述


🌐 1.3 HTTP Client

进行 Qt 开发时,和服务器之间的通信很多时候也会用到 HTTP 协议。

  • 通过 HTTP 从服务器获取数据。
  • 通过 HTTP 向服务器提交数据。

💡 笔记:实际开发过程中,HTTP 获取到的数据,也不一定是 HTML,更大的可能是客户端和服务器开发约定好交互的数据格式。按照约定的格式,客户端拿到之后,进行解析,并展示到界面上。

📌 1.3.1 核心 API

关键类主要是三个:QNetworkAccessManager、QNetworkRequest、QNetworkReply。

QNetworkAccessManager 提供了 HTTP 的核心操作。

方法说明
get(const QNetworkRequest&)发起一个 HTTP GET 请求。返回 QNetworkReply 对象。
post(const QNetworkRequest&, const QByteArray&)发起一个 HTTP POST 请求。返回 QNetworkReply 对象。

QNetworkRequest 表示一个 HTTP 请求(不含 body)。

如果需要发送一个带有 bodQNetworkAccessManager 的 post 方法中通过单独的参数来传入 body。

方法说明
QNetworkRequest(const QUrl&)通过 URL 构造一个 HTTP 请求。
setHeader(QNetworkRequest::KnownHeaders header, const QVariant& value)设置请求头。

💡 笔记:QVariant 表示一个类型可变的值,类似于 C 语言中的 void*。

其中的 QNetworkRequest::KnownHeaders 是一个枚举类型,常用取值:

取值说明
ContentTypeHeader描述 body 的类型。
ContentLengthHeader描述 body 的长度。
LocationHeader用于重定向报文中指定重定向地址。(响应中使用,请求用不到)
CookieHeader设置 cookie。
UserAgentHeader设置 User-Agent。

QNetworkReply 表示一个 HTTP 响应。这个类同时也是 QIODevice 的子类。

方法说明
error()获取出错状态。
errorString()获取出错原因的文本。
readAll()读取响应 body。
header(QNetworkRequest::KnownHeaders header)读取响应指定 header 的值。

此外,QNetworkReply 还有一个重要的信号 finished,会在客户端收到完整的响应数据之后触发。

🖥️ 1.3.2 代码示例

给服务器发送一个 GET 请求。

1) 创建界面:包含一个 QLineEdit、QPushButton。

  • 先使用水平布局把 QLineEdit 和 QPushButton 放好,并设置这两个控件的垂直方向的 sizePolicy 为 Expanding。
  • 再使用垂直布局把 QPlainTextEdit 和上面的水平布局放好。(QPlainTextEdit 的 readOnly 设为 true)
  • 设置垂直布局的 layoutStretch 为 5, 1。

💡 笔记:此处建议使用 QPlainTextEdit 而不是 QTextEdit。主要因为 QTextEdit 要进行富文本解析,如果得到的 HTTP 响应体积很大,就会导致界面渲染缓慢甚至被卡住。

2) 修改 widget.h,创建 QNetworkAccessManager 属性

class Widget : public QWidget
{
    Q_OBJECT
public:
    Widget(QWidget *parent = nullptr);
    ~Widget();
private slots:
    void on_pushButton_clicked();
private:
    Ui::Widget *ui;
    // 新增属性
    QNetworkAccessManager* manager;
};

3) 修改 widget.cpp,创建实例

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    // 实例化属性
    manager = new QNetworkAccessManager(this);
}

4) 编写按钮的 slot 函数,实现发送 HTTP 请求功能

void Widget::on_pushButton_clicked()
{
    // 1. 获取到输入框中的 URL, 构造 QUrl 对象
    QUrl url(ui->lineEdit->text());
    // 2. 构造 HTTP 请求对象
    QNetworkRequest request(url);
    // 3. 发送 GET 请求
    QNetworkReply* response = manager->get(request);
    // 4. 通过信号槽来处理响应
    connect(response, &QNetworkReply::finished, this, [=]() {
        if (response->error() == QNetworkReply::NoError) {
            // 响应正确
            QString html(response->readAll());
            ui->plainTextEdit->setPlainText(html);
            // qDebug() << html;
        } else {
            // 响应出错
            ui->plainTextEdit->setPlainText(response->errorString());
        }
        response->deleteLater();
    });
}

执行程序,观察效果。
在这里插入图片描述

发送 POST 请求代码也是类似,使用 manager->post() 即可。此处不再演示。


🧩 1.4 其他模块

Qt 中还提供了 FTP、DNS、SSL 等网络相关的组件工具,感兴趣的读者可以自行查看Qt中的参考手册或者借助于AI进行学习。


🎵 2. Qt 音视频

🔊 2.1 Qt 音频

在 Qt 中,音频主要是通过 QSound 类来实现。但是需要注意的是 QSound 类只支持播放 wav 格式的音频文件。也就是说如果想要添加音频效果,那么首先需要将非 wav 格式的音频文件转换为 wav 格式。

💡 笔记:使用 QSound 类时,需要添加模块:multimedia。

📌 2.1.1 核心 API 概览
方法说明
play()开始或继续播放当前源。
🖥️ 2.1.2 示例
/********************************* SoundTest.pro *********************************/
QT       += core gui multimedia   //添加音频模块
/********************************* widget.cpp *********************************/
#include "widget.h"
#include "ui_widget.h"
#include <QSound>  //添加音频头文件

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    //实例化对象
    QSound *sound = new QSound(":/sound.wav",this);
    connect(ui->btn,&QPushButton::clicked,[=](){
        sound->play(); //播放
    });
}

Widget::~Widget()
{
    delete ui;
}
🎬 2.2 Qt 视频

在 Qt 中,视频播放的功能主要是通过 QMediaPlayer 类和 QVideoWidget 类来实现。在使用这两个类时要添加对应的模块 multimedia 和 multimediawidgets。

📌 2.2.1 核心 API 概览
方法说明
setMedia()设置当前媒体源。
setVideoOutput()将 QVideoWidget 视频输出附加到媒体播放器。如果媒体播放器已经附加了视频输出,将更换一个新的。
🖥️ 2.2.2 示例

首先在 .pro 文件中添加 multimedia 和 multimediawidgets 两个模块。

/************************player.pro*******************************/
QT       += core ui
# 添加视频模块
QT += multimedia
QT += multimediawidgets
/********************************* widget.h *********************************/
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QHBoxLayout>  //水平布局
#include <QVBoxLayout>  //垂直布局
#include <QVideoWidget> //显示视频
#include <QMediaPlayer> //播放声音
#include <QPushButton> //按钮
#include <QStyle> //设置图标
#include <QFileDialog> //选择文件/文件夹

class Widget : public QWidget
{
    Q_OBJECT
public:
    Widget(QWidget *parent = nullptr);
    ~Widget();
public slots:
    void chooseVideo();
private:
    QMediaPlayer *mediaPlayer;
    QVideoWidget *videoWidget;
    QVBoxLayout *vbox;
    //创建两个按钮:选择视频按钮和开播放按钮
    QPushButton *chooseBtn,*playBtn;
};
#endif // WIDGET_H
/********************************* widget.cpp *********************************/
#include "widget.h"
#include <QMediaPlayer>
#include <QSlider>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    //对象实例化
    mediaPlayer = new QMediaPlayer(this);
    videoWidget = new QVideoWidget(this);
    //设置播放画面的窗口
    videoWidget->setMinimumSize(600,600);
    //实例化窗口布局---垂直布局
    this->vbox = new QVBoxLayout(this);
    this->setLayout(this->vbox);
    //实例化选择视频按钮
    chooseBtn = new QPushButton("选择视频",this);
    //实例化播放按钮
    playBtn = new QPushButton(this);
    //设置图标代替文件
    playBtn->setIcon(this->style()->standardIcon(QStyle::SP_MediaPlay));
    //实例化一个水平布局,将以上控件放入水平布局中
    QHBoxLayout *hbox = new QHBoxLayout;
    //添加控件
    hbox->addWidget(chooseBtn);
    hbox->addWidget(playBtn);
    //将播放窗口和水平布局都添加到垂直布局中
    vbox->addWidget(videoWidget);
    //布局中添加布局
    vbox->addLayout(hbox);
    //将选择视频对应的按钮和槽函数进行关联
    connect(chooseBtn,&QPushButton::clicked,this,&Widget::chooseVideo);
}

void Widget::chooseVideo()
{
    //选择视频,返回一个播放视频的名字
    QString name = QFileDialog::getSaveFileName(this,"选择视频",".","WMV(*.wmv)");
    //设置媒体声音
    mediaPlayer->setMedia(QUrl(name));
    //输出视频画面
    mediaPlayer->setVideoOutput(videoWidget);
    //播放
    mediaPlayer->play();
}

Widget::~Widget()
{
}

📝 小总结

本章围绕 Qt 网络编程与音视频模块展开,核心要点如下:

  • 事件机制:事件是信号槽的底层机制,信号槽是事件的进一步封装。绝大多数交互通过信号槽完成,特殊场景需要重写事件处理函数(如 enterEvent、mousePressEvent、keyPressEvent 等)。
  • 事件分发器与事件过滤器:事件分发器通过重写 bool event(QEvent* e) 实现拦截;事件过滤器则是在事件分发之前做更高级的拦截,通过 installEventFilter 安装、重写 eventFilter 实现。
  • 文件操作:QFile 负责文件读写,QFileInfo 获取文件信息,QSaveFile 安全保存文件,QTemporaryFile 创建临时文件。
  • 多线程:通过继承 QThread 并重写 run() 实现,启动线程调用 start(),通过信号与主线程通信。
  • UDP 通信:无连接协议,使用 QUdpSocket,通过 bind 绑定端口、writeDatagram 发送、receiveDatagram 接收。
  • TCP 通信:面向连接的可靠协议,服务器用 QTcpServer 监听、nextPendingConnection 获取连接;客户端用 QTcpSocket 连接服务器,通过 readyRead 信号读取数据。
  • HTTP 通信:基于 TCP 封装,Qt 提供客户端接口(QNetworkAccessManager、QNetworkRequest、QNetworkReply),不提供服务器端接口。
  • 音视频:QSound 播放 wav 音频;QMediaPlayer + QVideoWidget 播放视频。

🎯 经典面试题

1. 信号槽与事件的区别是什么?

信号槽是事件的进一步封装,事件是信号槽的底层机制。用户操作会产生事件,Qt 将操作系统的事件机制封装后进一步封装为信号槽。绝大多数用户交互通过信号槽完成,特殊情况下需要重写事件处理函数。

2. 事件分发器和事件过滤器有什么区别?

事件分发器通过重写 bool event(QEvent* e) 函数实现,所有事件都会进入该函数,根据事件类型分发到对应的事件处理函数,返回 true 表示拦截。事件过滤器则是在事件分发之前做更高级的拦截,通过 installEventFilter 安装,重写 eventFilter 函数实现,适合多个组件统一拦截的场景。

3. UDP 和 TCP 在 Qt 中的使用有何不同?

UDP 是无连接协议,使用 QUdpSocket,通过 bind 绑定端口、writeDatagram 发送数据报、receiveDatagram 接收数据报。TCP 是面向连接的可靠协议,服务器使用 QTcpServer 监听端口、nextPendingConnection 获取连接;客户端使用 QTcpSocket 的 connectToHost 建立连接,读写都使用 QByteArray。

4. deleteLater() 的作用是什么?为什么使用它?

deleteLater() 不是立即销毁对象,而是将对象标记为无效,Qt 会在下一轮事件循环中析构释放该对象。这样可以避免在槽函数执行过程中直接删除对象导致的问题,确保当前槽函数执行完毕后再销毁。

5. Qt 中如何实现多线程?

自定义一个类继承 QThread,重写 run() 函数作为线程处理函数,通过 start() 启动线程(不能直接调用 run()),线程执行结束后通过信号通知主线程,最后关闭线程。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2303_77004089/article/details/167085123

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--