【Qt系统相关】Qt系统相关

文章目录

  • [1. Qt事件](#1. Qt事件)
    • [1.1 事件介绍](#1.1 事件介绍)
    • [1.2 事件处理](#1.2 事件处理)
    • [1.3 按键事件](#1.3 按键事件)
    • [1.4 定时器](#1.4 定时器)
    • [1.5 事件分发器](#1.5 事件分发器)
    • [1.5 事件过滤器](#1.5 事件过滤器)
  • [2. Qt文件](#2. Qt文件)
  • [3. 多线程](#3. 多线程)
  • [4. Qt 网络](#4. Qt 网络)
    • [4.1 UDP Socket](#4.1 UDP Socket)
    • [4.2 TCP Socket](#4.2 TCP Socket)
    • [4.3 HTTP Client](#4.3 HTTP Client)
    • [4.4 其他模块](#4.4 其他模块)
  • [5. Qt 音视频](#5. Qt 音视频)
    • [5.1 Qt 音频](#5.1 Qt 音频)
    • [5.2 Qt 视频](#5.2 Qt 视频)

1. Qt事件

1.1 事件介绍

虽然Qt是跨平台的C++开发框架,Qt的很多能力其实是操作系统提供的.只不过 Qt 封装了系统的 API.

程序是运行在操作系统上的,需要系统给我们提供支撑~~

用户进行的各种操作,就可能会产生出信号,可以给某个信号指定槽函数。当信号触发时,就能够自动的执行到对应的槽函数.

事件非常类似.

用户进行的各种操作,也会产生事件。程序员同样可以给事件关联上处理函数(处理的逻辑),当事件触发的时候,就能够执行到对应的代码.

事件本身是操作系统提供的机制,Qt 也同样把操作系统事件机制进行了封装,拿到了 Qt 中,但是由于事件对应的代码编写起来不是很方便,Qt对于事件机制又进行了进一步的封装,就得到了信号槽

信号槽就是对于事件的进一步封装事件是信号槽的底层机制.

实际Qt开发程序过程中,绝大部分和用户之间进行的交互都是通过"信号槽"来完成的,有些特殊情况下,信号槽不一定能搞定.(某个用户的动作行为,Qt 没有提供对应的信号.)此时就需要通过重写事件处理函数的形式,来手动处理事件的响应逻辑

用户进行了很多操作,就会产生很多的事件(当然也会产生很多的信号)

事件是应用程序内部或者外部产⽣的事情或者动作的统称。在 Qt 中使用一个对象来表示一个事件。所有的 Qt 事件均继承于抽象类 QEvent。事件是由系统或者 Qt 平台本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件是在用户操作时发出,如键盘事件、鼠标事件等,另一些事件则是由系统本身自动发出,如定时器事件。常见的 Qt 事件如下:

常见事件描述:

事件名称 描述
鼠标事件 鼠标左键、鼠标右键、鼠标滚轮,鼠标的移动,鼠标按键的按下和松开
键盘事件 按键类型、按键按下、按键松开
定时器事件 定时时间到达
进入离开事件 鼠标的进入和离开
滚轮事件 鼠标滚轮滚动
绘屏事件 重绘屏幕的某些部分
显示隐藏事件 窗口的显示和隐藏
移动事件 窗口位置的变化
窗口事件 是否为当前窗口
大小改变事件 窗口大小改变
焦点事件 键盘焦点移动
拖拽事件 用鼠标进行拖拽

1.2 事件处理

事件处理一般常用的方法为:重写相关的 Event 函数

在 Qt 中,几乎所有的 Event 函数都是虚函数,所以可以重新实现。如:在实现鼠标的进入和离开事件 时,直接重新实现 enterEvent() 和 leaveEvent() 即可。enterEvent() 和 leaveEvent() 函数原型如下:

代码示例:处理一下鼠标进入和鼠标离开.

编辑ui文件

拖拽一个QLabel控件,我们期望实现鼠标在进入和离开这个标签时触发事件

为了能够清晰看到这个标签的边界,我们修改一下这个属性

有了边框,方便观察当前鼠标是否进入和离开~

这里需要创建 QLabel 的子类,重写 enterEvent 和 leaveEvent

点击choose,填写类名和要继承的父类

点击下一步,然后直接点击完成即可

在头文件中,我们需要修改一下

添加头文件以及需要重写的函数,还有为了能够将自定义的类也加入到对象树中,可以在形参中加入父控件的指针,如下:

cpp 复制代码
#ifndef LABEL_H
#define LABEL_H

#include <QWidget>
#include <QLabel>

class Label : public QLabel
{
    Q_OBJECT
public:
    Label(QWidget* parent);
    
    void enterEvent(QEvent* event);
    void leaveEvent(QEvent* event);
};

#endif // LABEL_H

注意:要想重写父类的函数,就需要确保你这边写的函数名字和函数的参数列表都完全一致 (形参名无所谓),谨防单词拼写错误~~

在label.cpp文件中重写函数,这里我们就简单打印出日志

cpp 复制代码
void Label::enterEvent(QEvent *event)
{
    (void) event;
    qDebug() << "enterEvent";
}

void Label::leaveEvent(QEvent *event)
{
    (void) event;
    qDebug() << "leaveEvent";
}

上述代码,虽然重写了这俩函数,但是还有点问题~~

当前在界面上创建的这个 label 其实是QLabel, 不是咱们自己写的 Label,必须要确保界面上的这个 label 是一个咱们自己定义的 Label 类的实例, 才会执行到~ ~

右键选择 QLabel 控件,选择 "提升为"

一定要确保你的类名以及头文件的名字和上述自定义的类名头文件啥的都匹配!然后点击 "添加"

最后点击 "提升"

现在在右侧窗口中可以看到,标签的类型已经提升为我们自定义的 Label 类

运行结果:

还有一些其他的鼠标事件和上面示例是类似的,都是根据实际需求来重写函数,其余操作都是相同的,这里就不一一演示了


1.3 按键事件

Qt 中的按键事件是通过 QKeyEvent 类来实现的。当键盘上的按键被按下或者被释放时,键盘事件便会触发。

要想获取到用户的键盘按键~~

QShortCut 这是信号槽机制封装过,获取键盘按键的方式

站在更底层的角度,也可以通过事件获取到当前用户键盘按下的情况 keyPressEvent (QKeyEvent* )

在帮助文档中查找 QKeyEvent 类如下:

查找按键事件中所有的按键类型:在帮助文档中输入:Qt::Key,如下图:

代码示例:当某个按键被按下时,输出:某个按键被按下了;

新建项目,在头文件 "widget.h" 中声明虚函数 keyPressEvent();如下图:

在 "widget.cpp" 文件中重写 keyPressEvent() 虚函数;

cpp 复制代码
void MainWindow::keyPressEvent(QKeyEvent *event)
{
    if (event->key() == Qt::Key_A) 
    {
        qDebug() << "按下了 A 键";
    }
}

运行结果:

组合按键

在 Qt 助手中搜索:Qt::KeyboardModifier,如下图示:

Qt::KeyboardModifier 中定义了在处理键盘事件时对应的修改键。在 Qt 中,键盘事件可以与修改键一起使用,以实现一些复杂的交互操作。KeyboardModifier 中修改键的具体描述如下:

Qt::NoModifier 无修改键
Qt::ShiftModifier Shift 键
Qt::ControlModifier Ctrl 键
Qt::AltModifier Alt 键
Qt::MetaModifier Meta键(在Windows上指Windows键,在macOS上指Command键)
Qt::KeypadModifier 使用键盘上的数字键盘进行输入时,Num Lock键处于打开状态
Qt::GroupSwitchModifier 用于在输入法 组之间 切换

代码如下:

cpp 复制代码
void MainWindow::keyPressEvent(QKeyEvent *event)
{
    if (event->key() == Qt::Key_A && event->modifiers() == Qt::ControlModifier)
    {
        qDebug() << "按下了 ctrl + A 键";
    }
}

运行结果:


1.4 定时器

Qt 中在进行窗口程序的处理过程中,经常要周期性的执行某些操作,或者制作一些动画效果,使用定时器就可以实现。所谓定时器就是在间隔一定时间后,去执行某一个任务。定时器在很多场景下都会使用到,如弹窗自动关闭之类的功能等。

Qt中的定时器分为 QTimerEvent 和 QTimer 这2个类。

  • QTimerEvent类 用来描述一个定时器事件。在使用时需要通过 startTimer() 函数来开启一个定时器,这个函数需要输入一个以毫秒为单位的整数作为参数来表明设定的时间,它返回的整型值代表这个定时器。当定时器溢出时(即定时时间到达)就可以在 timerEvent() 函数中获取该定时器的编号来进行相关操作。
  • QTimer类 来实现一个定时器,它提供了更高层次的编程接接,如:可以使用信号和槽,还可以设置只运行一次的定时器。(我们在之前介绍显示类控件 LCD Bumber时就已经使用过)

代码示例:

拖拽一个LCD Number控件

开启定时器事件

cpp 复制代码
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    
    // 开启定时器事件.
    // 此处 timerId 是一个定时器的身份标识.
    int timerId = this->startTimer(1000);
}

此处 timerld 类似于 Linux 中谈到的"文件描述符',起到身份标识效果

由于后面还会使用到 timerId ,所以我们将它放在成员变量中

重写虚函数

cpp 复制代码
void MainWindow::timerEvent(QTimerEvent *event)
{
    // 如果一个程序中存在多个定时器 (startTimer 创建的定时器), 此时每个定时器都会触发 timerEvent 函数.
    // 先判定一下这次触发是否是想要的定时器触发的.
    if(event->timerId() != this->timerId)
    {
        // 如果不是我们的定时器触发的, 就直接忽略.
        // 当前程序中只有这一个定时器.
        return;
    }
    
    int value = ui->lcdNumber->intValue();
    if(value <= 0)
    {
        // 终止定时器
        this->killTimer(this->timerId);
        return;
    }
    value -= 1;
    ui->lcdNumber->display(value);
}

运行结果:


1.5 事件分发器

概述

在 Qt 中,事件分发器(Event Dispatcher) 是一个核心概念,用于处理 GUI 应用程序中的事件。事件分发器负责将事件从一个对象传递到另一个对象,直到事件被处理或被取消。每个继承自 QObject类 或 QObject类 本身都可以在本类中重写 bool event(QEvent *e) 函数,来实现相关事件的捕获和拦截

事件分发器工作原理

在 Qt 中,我们发送的事件都是传给了 QObject 对象,更具体点是传给了 QObject 对象的 event() 函 数。所有的事件都会进入到这个函数里面,那么我们处理事件就要重写这个 event() 函数。event() 函数本身不会去处理事件,而是根据 事件类型(type值) 调用不同的**事件处理函数。**事件分发器就是工作在应用程序向下分发事件的过程中,如下图:

如上图,事件分发器用于分发事件 。在此过程中,事件分发器也可以做拦截操作。事件分发器主要是通过 bool event(QEvent *e) 函数来实现。其返回值为布尔类型,若为 ture,代表拦截,不向下分发

Qt 中的事件是封装在 QEvent类 中,在 Qt 助手中输入 QEvent 可以查看其所包括的事件类型,如下图示:

示例:

1、在 "widget.h" 头文件中声明 鼠标点击事件 和 事件分发器;如下图⽰:

2、在 "widget.cpp" 文件中实现 鼠标点击事件 和 拦截事件;

执行结果如下:


1.5 事件过滤器

在 Qt 中,一个对象可能经常要查看或拦截另外一个对象的事件,如对话框想要拦截按键事件,不让别的组件接收到,或者修改按键的默认值等。通过上面的学习,我们已经知道,Qt 创建了 QEvent事件对象之后,会调用QObject 的 event()函数 处理事件的分发。显然,我们可以在 event()函数 中实现拦截的操作。由于 event()函数是 protected 的,因此,需要继承已有类。如果组件很多,就需要重写很多个event()函数。这当然相当麻烦,更不用说重写 event()函数还得小心一堆问题。好在 Qt 提供了另外一种机制来达到这一目的:事件过滤器。

事件过滤器是在应用程序分发到 event事件分发器 之前,再做一次更高级的拦截。如下图示:

事件过滤器的一般使用步骤:

  1. 安装事件过滤器;
  2. 重写事件过滤器函数:eventfilter() 。

示例:

  1. 新建 Qt 项目,基类选择 QWidget,同时勾选 UI 界面文件,如下图示;
  1. 设计 UI 文件,如下图示;
  1. 在项目新添加一个类:MyLabel;

先选中项目名称 QEvent,点击鼠标右键,选择 add new ... ,弹出如下对话框:

  1. 选择:Choose ... ,弹出如下界面:
  1. 此时项目中会新添加以下两个文件:
  1. 在 UI 文件中选中 Label,右键 ------> 提升为...
  1. 当点击 "提升为... " 之后,弹出如下对话框:
  1. 在 "mylabel.h" 中声明 鼠标点击事件 和 事件分发器;
  1. 在 "mylabel.cpp" 文件中实现鼠标点击事件和事件分发器;
  1. 在 "widget.h" 头文件中声明事件过滤器函数;
  1. 在 "widget.cpp" 文件中实现事件过滤器的两个步骤;

执行结果如下所示:


2. Qt文件

Qt 文件概述

文件操作是应用程序必不可少的部分。Qt 作为一个通用开发库,提供了跨平台的文件操作能力。 Qt 提供了很多关于文件的类,通过这些类能够对文件系统进行操作,如文件读写、文件信息获取、文件复制或重命名等。

输入输出设备类

在 Qt 中,文件读写的类为 QFile 。QFile 的父类为 QFileDevice ,QFileDevice 提供了文件交互操作的 底层功能。 QFileDevice 的父类是 QIODevice,QIODevice 的父类为 QObject 。

QIODevice 是 Qt 中所有输入输出设备(input/output device,简称 I/O 设备)的基础类,I/O 设备就 是能进行数据输入和输出的设备,例如文件是一种 I/O 设备,网络通信中的 socket 是 I/O 设备, 串口、蓝牙等通信接口也是 I/O 设备,所以它们也是从 QIODevice 继承来的。Qt 中主要的一些 I/O 设备类的继承关系如下图所示:

上图中各类的说明如下:

  • QFile 是用于文件操作和文件数据读写的类,使用 QFile 可以读写任意格式的文件。
  • QSaveFile 是用于安全保存文件的类。使用 QSaveFile 保存文件时,它会先把数据写入一个临时文件,成功提交后才将数据写入最终的文件。如果保存过程中出现错误,临时文件里的数据不会被写入最终文件,这样就能确保最终文件中不会丢失数据或被写入部分数据。 在保存比较大的文件或复杂格式的文件时可以使用这个类,例如从网络上下载文件等。
  • QTemporaryFile 是用于创建临时文件的类。使用函数 QTemporaryFile::open() 就能创建一个文件名唯一的临时文件,在 QTemporaryFile 对象被删除时,临时文件被自动删除。
  • QTcpSocket 和 QUdpSocket 是分别实现了 TCP 和 UDP 的类。
  • QSerialPort 是实现了串口通信的类,通过这个类可以实现计算机与串口设备的通信。
  • QBluetoothSocket 是用于蓝牙通信的类。手机和平板计算机等移动设备有蓝牙通信模块,笔记本电脑一般也有蓝牙通信模块。通过QBluetoothSocket类,就可以编写蓝牙通信程。如编程实现笔记本电脑与手机的蓝牙通信。
  • QProcess 类用于启动外部程序,并且可以给程序传递参数。
  • QBuffer 以一个 QByteArray 对象作为数据缓冲区,将 QByteArray 对象当作一个 I/O 设备来读写。

文件读写类

在 Qt 中,文件的读写主要是通过 QFile 类来实现。在 QFile 类中提供了一些用来读写文件的方法。对于文件的操作主要有:

  • 读数据:QFile 类中提供了多个方法用于读取文件内容;如 read()、readAll()、readLine()等。
  • 写数据:QFile 类中提供了多个方法用于往文件中写内容;如 write()、writeData()等。
  • 关闭文件:文件使用结束后必须用函数 close() 关闭文件。

访问一个设备之前,需要使用 open()函数 打开该设备,而且必须指定正确的打开模式,QIODevice 中所有的打开模式由 QIODevice::OpenMode 枚举变量定义,其取值如下:

QIODevice::NotOpen 没有打开设备
QIODevice::ReadOnly 以只读方式打开设备
QIODevice::WriteOnly 以只写方式打开设备
QIODevice::ReadWrite 以读写方式打开设备
QIODevice::Append 以追加方式打开设备,数据将写到文件末尾
QIODevice::Truncate 每次打开文件后重写文件内容,原内容将被删除
QIODevice::Text 在读文件时,行尾终止符会被转换为 '\n';当写入文件时,行尾终止符会被转换为本地编码。如 Win32上为'\r\n';
QIODevice::Unbuffered 无缓冲形式打开文件,绕过设备中的任何缓冲区
QIODevice::NewOnly 文件存在则打开失败,不存在则创建文件

代码示例:文件的打开和保存

在 mainWindow.cpp 文件中编写代码,创建需要使用的控件

cpp 复制代码
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    QMenuBar* menubar = this->menuBar();
    QMenu* menu = new QMenu("文件");
    menubar->addMenu(menu);

    QAction* action1 = new QAction("打开");
    QAction* action2 = new QAction("保存");
    menu->addAction(action1);
    menu->addAction(action2);

    edit = new QPlainTextEdit();
    QFont font;
    font.setPixelSize(20);
    edit->setFont(font);
    this->setCentralWidget(edit);
    // 连接 QAction 的信号槽.
    connect(action1, &QAction::triggered, this, &MainWindow::handleAction1);
    connect(action2, &QAction::triggered, this, &MainWindow::handleAction2);
}

实现槽函数:

cpp 复制代码
void MainWindow::handleAction1()
{
    // 1. 先弹出 "打开文件" 对话框. 让用户选择打开哪个文件.
    QString path = QFileDialog::getOpenFileName(this);
    // 2. 把文件名显示到状态栏里.
    QStatusBar* statusBar = this->statusBar();
    statusBar->showMessage(path);
    
    // 3. 根据用户选择的路径, 构造一个 QFile 对象. 并打开文件
    QFile file(path);
    bool ret = file.open(QFile::ReadOnly);
    if(!ret)
    {
        // 打开文件失败!
        statusBar->showMessage(path + " 打开失败!");
        return;
    }
    // 4. 读取文件了.
    QString text = file.readAll();
    // 5. 关闭文件
    file.close();
    // 6. 读到的内容设置到输入框中.
    edit->setPlainText(text);
}

void MainWindow::handleAction2()
{
    // 1. 先弹出 "保存文件" 对话框. 让用户选择哪个文件.
    QString path = QFileDialog::getSaveFileName(this);
    
    // 2. 在状态栏中显示这个文件名.
    QStatusBar* statusBar = this->statusBar();
    statusBar->showMessage(path);
    // 3. 根据用户选择的路径, 构造一个 QFile 对象, 并打开文件.
    QFile file(path);
    bool ret = file.open(QFile::WriteOnly);
    if(!ret)
    {
        // 打开文件失败!
        statusBar->showMessage(path + " 打开失败!");
        return;
    }
    // 4. 写文件.
    const QString& text = edit->toPlainText();
    file.write(text.toUtf8());
    // 5. 关闭文件
    file.close();
}

注意:

将输入框中的内容保存到文件时,需要通过 write 写入,参数需要 QByteArray 类型,所以我们需要将QString类型转换

运行结果:

此时我们可以打开该文件查看

此时我们将内容修改一下,再通过打开该文件显示在输入框中查看

再次运行打开hello.txt文件

文件和目录信息类

QFileInfo 是 Qt 提供的一个用于获取文件和目录信息的类,如获取文件名、文件大小、文件修改日期等。QFileInfo类中提供了很多的方法,常用的有:

  • isDir() 检查该文件是否是目录;
  • isExecutable() 检查该文件是否是可执行文件;
  • fileName() 获得文件名;
  • completeBaseName() 获取完整的文件名;
  • suffix() 获取文件后缀名;
  • completeSuffix() 获取完整的文件后缀;
  • size() 获取我呢件大小;
  • isFile() 判断是否为文件;
  • fileTime() 获取文件创建时间、修改时间、最近访问时间等;

代码示例:查看文件信息

编辑ui文件

拖拽一个按钮

编写槽函数

cpp 复制代码
void MainWindow::on_pushButton_clicked()
{
    QString path = QFileDialog::getOpenFileName(this);
    
    QFileInfo fileInfo(path);
    // 打印相关属性
    qDebug() << "文件名: " << fileInfo.fileName();
    qDebug() << "后缀名: " << fileInfo.suffix();
    qDebug() << "文件路径: " << fileInfo.path();
    qDebug() << "文件大小: " << fileInfo.size();
    qDebug() << "是否为文件: " << fileInfo.isFile();
    qDebug() << "是否为目录: " << fileInfo.isDir();
}

运行结果:


3. 多线程

Qt 多线程概述

在 Qt 中,多线程的处理一般是通过 QThread类 来实现。

QThread 代表一个在应用程序中可以独立控制的线程,也可以和进程中的其他线程共享数据。

QThread 对象管理程序中的一个控制线程。

QThread 常用 API

run() 线程的入口函数...
start() 通过调用 run() 开始执行线程。操作系统将根据优先级参数调度线程。如果线程已 经在运行,这个函数什么也不做。
currentThread() 返回一个指向管理当前执行线程的 QThread的指针。
isRunning() 如果线程正在运行则返回true;否则返回false。
sleep() / msleep() / usleep() 使线程休眠,单位为秒 / 毫秒 / 微秒
wait() 阻塞线程,直到满足以下任何一个条件: 与此 QThread 对象关联的线程已经完成执行(即当它从run()返回时)。如果线程已 经完成,这个函数将返回 true。如果线程尚未启动,它也返回 true。已经过了几毫秒。如果时间是 ULONG_MAX(默认值),那么等待永远不会超时(线程必须从run()返回)。如果等待超时,此函数将返回 false。 这提供了与 POSIX pthread_join() 函数类似的功能。
terminate() 终止线程的执行。线程可以立即终止,也可以不立即终止,这取决于操作系统的调 度策略。在terminate() 之后使用 QThread::wait() 来确保。
finished() 当线程结束时会发出该信号,可以通过该信号来实现线程的清理工作。

使用线程

创建线程的步骤:

  1. 自定义一个类,继承于 QThread,并且只有一个线程处理函数(和主线程不是同一个线程),这个线程处理函数主要就是重写父类中的 run() 函数。
  2. 线程处理函数里面写入需要执行的复杂数据处理;
  3. 启动线程不能直接调用 run() 函数,需要使用对象来调用 start() 函数实现线程启动;
  4. 线程处理函数执行结束后可以定义一个信号来告诉主线程;
  5. 最后关闭线程。

之前基于定时器,写过倒计时这样的程序.

也可以通过线程,来完成类似的功能.

代码示例:创建另一个线程,新线程中进行计时(搞一个循环,每循环一次, sleep 1s, sleep 完成,就可以更新界面了.)

由于存在线程安全问题,多个线程同时对于界面的状态进行修改,此时就会导致界面就出错了,Qt 选择了一刀切!针对界面的控件状态进行任何修改,务必在主线程中执行

首先新建 Qt 项目,设计 UI界面如下:

新建一个类,继承于 QThread类;

之前章节已经详细介绍过如何创建,这里具体过程不再演示

在 thread.cpp 中重写 run 函数

cpp 复制代码
void Thread::run()
{
    // 在这个 run 中, 能否直接去进行修改界面内容呢?
    // 不可以的!!!
    // 虽然不可以修改界面, 但是可以针对时间来进行计时.
    // 当每到了一秒钟的时候, 通过信号槽, 来通知主线程, 负责更新的界面内容.
    for(int i = 0; i < 5; i++)
    {
        sleep(1);
        // 发送一个信号, 通知主线程.
        emit notify();
    }
}

只需要在 thread.h 文件中定义信号,系统会给我们实现

在主线程中更新界面,在这之前我们需要先将新线程发送的信号和主线程来处理更新界面进行关联 connect

同时由于定义的thread 对象后面都需要用到,所以我们定义为成员变量

cpp 复制代码
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    // 连接信号槽, 通过槽函数更新界面.
    connect(&thread, &Thread::notify, this, &MainWindow::handle);
}

实现更新界面的槽函数:

cpp 复制代码
void MainWindow::handle()
{
    // 此处修改界面内容.
    int value = ui->lcdNumber->intValue();
    value--;
    ui->lcdNumber->display(value);
}

实现按钮的槽函数:

cpp 复制代码
void MainWindow::on_pushButton_clicked()
{
    thread.start();
}

运行结果:

线程安全

实现线程互斥和同步常⽤的类有:

  • 互斥锁:QMutex、QMutexLocker
  • 条件变量:QWaitCondition
  • 信号量:QSemaphore
  • 读写锁:QReadLocker、QWriteLocker、QReadWriteLock

互斥锁

互斥锁是一种保护和防止多个线程同时访问同一对象实例的方法,在 Qt 中,互斥锁主要是通过 QMutex类来处理。

  • QMutex

特点:QMutex 是 Qt 框架提供的互斥锁类,用于保护共享资源的访问,实现线程间的互斥操作。

用途:在多线程环境下,通过互斥锁来控制对共享数据的访问,确保线程安全。

cpp 复制代码
QMutex mutex;
mutex.lock(); //上锁
//访问共享资源
//...
mutex.unlock(); //解锁
  • QMutexLocker

特点:QMutexLocker 是 QMutex 的辅助类,使用 RAII(Resource Acquisition Is Initialization)方式 对互斥锁进行上锁和解锁操作。

用途:简化对互斥锁的上锁和解锁操作,避免忘记解锁导致的死锁等问题。

cpp 复制代码
QMutex mutex;
{
 QMutexLocker locker(&mutex); //在作⽤域内⾃动上锁
 
 //访问共享资源
 //...
} //在作用域结束时自动解锁
  • QReadWriteLocker、QReadLocker、QWriteLocker

特点:

QReadWriteLock 是读写锁类,用于控制读和写的并发访问。

QReadLocker 用于读操作上锁,允许多个线程同时读取共享资源。

QWriteLocker 用于写操作上锁,只允许一个线程写入共享资源。

用途:在某些情况下,多个线程可以同时读取共享数据,但只有一个线程能够进行写操作。读写锁提供了更高效的并发访问方式。

cpp 复制代码
QReadWriteLock rwLock;
//在读操作中使用读锁
{
 QReadLocker locker(&rwLock); //在作用域内自动上读锁
 
 //读取共享资源
 //...
 
} //在作用域结束时自动解读锁
//在写操作中使用写锁
{
 QWriteLocker locker(&rwLock); //在作用域内自动上写锁
 
 //修改共享资源
 //...
 
} //在作用域结束时自动解写锁

这里就不演示了,用法和C++中的锁是相同的,

Qt 的锁 和 C++ 标准库中的锁,本质上都是封装的系统提供的锁~~编写多线程程序的时候,可以使用Qt的锁,也可以使用C++的锁

C++的锁能不能锁 Qt 的线程?也是可以的~~ (虽然混着用也行,一般不建议)

条件变量

在多线程编程中,假设除了等待操作系统正在执行的线程之外,某个线程还必须等待某些条件满足才能执行,这时就会出现问题。这种情况下,线程会很自然地使用锁的机制来阻塞其他线程,因为这只是线程的轮流使用,并且该线程等待某些特定条件,人们会认为需要等待条件的线程,在释放互斥锁或读写锁之后进入了睡眠状态,这样其他线程就可以继续运行。当条件满足时,等待条件的线程将被另一个线程唤醒。

在 Qt 中,专门提供了 QWaitCondition类 来解决像上述这样的问题。

特点:QWaitCondition 是 Qt 框架提供的条件变量类,用于线程之间的消息通信和同步。

用途:在某个条件满足时等待或唤醒线程,用于线程的同步和协调。

cpp 复制代码
QMutex mutex;
QWaitCondition condition;

//在等待线程中
mutex.lock();

//检查条件是否满足,若不满足则等待
while (!conditionFullfilled()) 
{
    condition.wait(&mutex); //等待条件满足并释放锁
}

//条件满足后继续执⾏
//...

mutex.unlock();

//在改变条件的线程中
mutex.lock();

//改变条件
changeCondition();
condition.wakeAll(); //唤醒等待的线程

mutex.unlock();

信号量

有时在多线程编程中,需要确保多个线程可以相应的访问一个数量有限的相同资源。例如,运行程序的设备可能是非常有限的内存,因此我们更希望需要大量内存的线程将这一事实考虑在内,并根据可用的内存数量进行相关操作,多线程编程中类似问题通常用信号量来处理。信号量类似于增强的互斥锁,不仅能完成上锁和解锁操作,而且可以跟踪可用资源的数量。

特点:QSemaphore 是 Qt 框架提供的计数信号量类,用于控制同时访问共享资源的线程数量。

用途:限制并发线程数量,用于解决一些资源有限的问题。

cpp 复制代码
QSemaphore semaphore(2); //同时允许两个线程访问共享资源
//在需要访问共享资源的线程中
semaphore.acquire(); //尝试获取信号量,若已满则阻塞
//访问共享资源
//...
semaphore.release(); //释放信号量
//在另一个线程中进⾏类似操作

4. Qt 网络

和多线程类似, Qt 为了支持跨平台, 对网络编程的 API 也进行了重新封装.

咱们接下来的内容中重点介绍 Qt 的网络相关的 API 的使用. 对于之前 Linux 网络中介绍过的网络相关概念和原理, 下面均不再解释

注意:实际 Qt 开发中进行网络编程, 也不一定使用 Qt 封装的网络 API, 也有一定可能使用的是系统原生API 或者其他第三方框架的 API.

进行网络编程的时候,本质上是在编写应用层代码.需要传输层提供支持

传输层最核心的协议,有 UDP 和TCP,并且这俩协议差别还很大,Qt 也就提供了两套 API

在进行网络编程之前, 需要在项目中的 .pro 文件中添加 network 模块.

添加之后要手动编译一下项怒, 使 Qt Creator 能够加载对应模块的头文件.

之前学过的Qt的各种控件,各种内容.都是包含在QtCore模块中的(默认就添加的)

为啥Qt 要划分出这些模块呢??

Qt 本身是一个非常庞大,包罗万象的框架.

如果把所有的 Qt 的功能都放到一起,即使咱们就只是写一个简单的 hello world,此时生成的可执行程序也会非常庞大.(这里就包含了大量其实没有使用的功能)

模块化处理~~其他的功能分别封装成不同的模块

默认情况下这些额外的模块不会参与编译,需要在.pro 文件中,引I入对应的模块,才能把对应功能给编译加载进来.

Qt 其实提供了 静态库 的版本 和 动态库 的版本.


4.1 UDP Socket

核心 API 概览

主要的类有两个. QUdpSocketQNetworkDatagram

QUdpSocket 表示一个 UDP 的 socket 文件

名称 类型 说明 对标原生 API
bind(const QHostAddress&, quint16) 方法 绑定指定的端口号. bind
receiveDatagram() 方法 返回 QNetworkDatagram . 读取一个 UDP 数据报. recvfrom
writeDatagram(const QNetworkDatagram&) 方法 发送一个 UDP 数据报. sendto
readyRead 信号 在收到数据并准备就绪后触发. 无 (类似于 IO 多路复用的通知机制)

QNetworkDatagram 表示一个 UDP 数据报.

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

回显服务器

创建界面, 包含一个 QListWidget 用来显示消息

在项目中的 .pro 文件中添加 network 模块.

创建 QUdpSocket 成员

修改 widget.h

cpp 复制代码
class Widget : public QWidget
{
    Q_OBJECT

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

private:
    Ui::Widget *ui;
    
    QUdpSocket* socket;
};

修改 widget.cpp, 完成 socket 后续的初始化

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

cpp 复制代码
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, 8080);
    if(!ret)
    {
        // 绑定失败
        QMessageBox::critical(this, "服务器启动出错", socket->errorString());
        return;
    }
}

实现 processRequest , 完成处理请求的过程

  • 读取请求并解析
  • 根据请求计算响应
  • 把响应写回到客户端
cpp 复制代码
void Widget::processRequest()
{
    // 读取请求并解析
    const QNetworkDatagram& requestDatagram = socket->receiveDatagram();
    QString request = requestDatagram.data();
    // 根据请求计算响应. (由于是回显服务器. 响应不需要计算, 就是请求本身).
    QString response = process(request);
    // 把响应写回给客户端
    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);
}

实现 process 函数

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

cpp 复制代码
QString Widget::process(const QString &request)
{
    // 由于当前是回显服务器, 响应就是和请求完全一样.
    // 对于一个成熟的商业服务器, 这里请求->响应的计算过程可能是非常复杂的. (业务逻辑).
    return request;
}

此时, 服务器程序编写完毕.

但是直接运行还看不出效果. 还需要搭配客户端来使用

回显客户端

创建界面. 包含一个 QLineEdit , QPushButton , QListWidget

  • 先使用水平布局把 QLineEdit 和 QPushButton 放好, 并设置这两个控件的垂直方向的 sizePolicy 为 Expanding
  • 再使用垂直布局把 QListWidget 和上面的水平布局放好.
  • 设置垂直布局的 layoutStretch 为 5, 1 (当然这个尺寸比例根据个人喜好微调).

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

cpp 复制代码
// 定义两个常量, 描述服务器的 地址 和 端口.
const QString SERVER_IP = "127.0.0.1";
const quint16 SERVER_PORT = 8080;

创建 QUdpSocket 成员

修改 widget.h, 定义成员

cpp 复制代码
class Widget : public QWidget
{
    Q_OBJECT

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

private:
    Ui::Widget *ui;
    
    QUdpSocket* socket;
};

修改 widget.cpp, 初始化 socket

cpp 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    
    // 修改窗口标题
    this->setWindowTitle("客户端");
    
    socket = new QUdpSocket(this);
}

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

cpp 复制代码
void Widget::on_pushButton_clicked()
{
    // 1. 获取到输入框的内容
    const QString& text = ui->lineEdit->text();
    // 2. 构造 UDP 的请求数据.
    QNetworkDatagram requestDatagram(text.toUtf8(), QHostAddress(SERVER_IP), SERVER_PORT);
    // 3. 发送请求数据.
    socket->writeDatagram(requestDatagram);
    // 4. 把发送的请求也添加到列表框中.
    ui->listWidget->addItem("客户端说: " + text);
    // 5. 把输入框的内容也清空一下.
    ui->lineEdit->setText("");
}

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

cpp 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    // 修改窗口标题
    this->setWindowTitle("客户端");

    socket = new QUdpSocket(this);
    
    connect(socket, &QUdpSocket::readyRead, this, &Widget::processResponse);
}

void Widget::processResponse()
{
    // 通过这个函数来处理收到的响应.
    // 1. 读取到响应数据
    const QNetworkDatagram& responseDatagram = socket->receiveDatagram();
    QString response = responseDatagram.data();
    // 2. 把响应数据显示到界面上.
    ui->listWidget->addItem("服务器说: " + response);
}

最终执行效果

启动多个客户端都可以正常工作


4.2 TCP Socket

核心 API 概览

核心类是两个: QTcpServerQTcpSocket

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

名称 类型 说明 对标原生 API
listen(const QHostAddress&, quint16 port) 方法 绑定指定的地址和端口号, 并开始监听. bind 和 listen
nextPendingConnection() 方法 从系统中获取到一个已经建立好的 tcp 连接. 返回一个 QTcpSocket , 表示这个客户端的连接. 通过这个 socket 对象完成和客户端之间的通信.
acceptnewConnection 信号 有新的客户端建立连接好之后触发. 无(但是类似于 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.

回显服务器

创建界面. 包含一个 QListWidget , 用于显示收到的数据

创建 QTcpServer 并初始化

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

cpp 复制代码
class Widget : public QWidget
{
    Q_OBJECT

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

private:
    Ui::Widget *ui;
    
    QTcpServer* tcpServer;
};

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

  • 设置窗口标题
  • 实例化 TCP server. (父元素设为当前控件, 会在父元素销毁时被一起销毁).
  • 通过信号槽, 处理客户端建立的新连接.
  • 监听端口
cpp 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    
    // 设置窗口标题
    this->setWindowTitle("服务器");
    // 创建 QTcpServer 的实例
    tcpServer = new QTcpServer(this);
    // 通过信号槽, 指定如何处理连接.
    connect(tcpServer, &QTcpServer::newConnection, this, &Widget::processConnection);
    
    bool ret = tcpServer->listen(QHostAddress::Any, 8080);
    if(!ret)
    {
        QMessageBox::critical(this, "服务器启动失败!", tcpServer->errorString());
        exit(1);
    }
}

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

  • 获取到新的连接对应的 socket.
  • 通过信号槽, 处理收到请求的情况
  • 通过信号槽, 处理断开连接的情况
cpp 复制代码
void Widget::processConnection()
{
    // 1. 通过 tcpServer 拿到一个 socket 对象, 通过这个对象来和客户端进行通信.
    QTcpSocket* clientSocket = tcpServer->nextPendingConnection();
    QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客户端上线!";
    ui->listWidget->addItem(log);
    // 2. 通过信号槽, 来处理客户端发来请求的情况.  
    connect(clientSocket, &QTcpSocket::readyRead, this, [=]{
        // a) 读取出请求数据. 此处 readAll 返回的是 QByteArray, 通过赋值转成 QString
        QString request = clientSocket->readAll();
        // b) 根据请求处理响应
        const QString& response = process(request);
        // c) 把响应写回到客户端
        clientSocket->write(response.toUtf8());
        // d) 把上述信息记录到日志中.
        QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] "
                        + " req: " + request + ", resp: " + response;
        ui->listWidget->addItem(log);
    });
    
    // 3. 通过信号槽, 来处理客户端断开连接的情况.
    connect(clientSocket, &QTcpSocket::disconnected, this, [=]{
        // a) 把断开连接的信息通过日志显示出来.
        QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客户端下线!";
        ui->listWidget->addItem(log);
        // b) 手动释放 clientSocket. 直接使用 delete 是下策, 使用 deleteLater 更加合适的.
        clientSocket->deleteLater();
    });
}

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

回显客户端

创建界面. 包含一个 QLineEdit , QPushButton , QListWidget

  • 先使用水平布局把 QLineEdit 和 QPushButton 放好, 并设置这两个控件的垂直方向的 sizePolicy 为 Expanding
  • 再使用垂直布局把 QListWidget 和上面的水平布局放好.
  • 设置垂直布局的 layoutStretch 为 5, 1 (当然这个尺寸比例根据个人喜好微调)

创建 QTcpSocket 并实例化

修改 widget.h, 创建成员.

cpp 复制代码
class Widget : public QWidget
{
    Q_OBJECT

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

private:
    Ui::Widget *ui;
    
    QTcpSocket* tcpSocket;
};

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

  • 设置窗口标题
  • 实例化 socket 对象 (父元素设为当前控件, 会在父元素销毁时被一起销毁).
  • 和服务器建立连接.
  • 等待并确认连接是否出错.
cpp 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    this->setWindowTitle("客户端");

    socket = new QTcpSocket(this);
    // 和服务器建立连接.
    socket->connectToHost("127.0.0.1", 8080);

    // 连接信号槽, 处理响应
    connect(socket, &QTcpSocket::readyRead, this, [=]{
        // a) 读取出响应内容
        QString response = socket->readAll();
        // b) 把响应内容显示到界面上.
        ui->listWidget->addItem("服务器说: " + response);
    });
    // 等待连接建立的结果. 确认是否连接成功.
    bool ret = socket->waitForConnected();
    if(!ret)
    {
        QMessageBox::critical(this, "连接服务器失败!", socket->errorString());
        exit(1);
    }
}

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

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

运行结果:


4.3 HTTP Client

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

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

核心 API

关键类主要是三个. QNetworkAccessManager , QNetworkRequest , QNetworkReply .

QNetworkAccessManager 提供了 HTTP 的核心操作.

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

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

如果需要发送一个带有 body 的请求(比如 post), 会在 QNetworkAccessManager 的 post 方法中通过单独的参数来传入 body.

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

其中的 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 会在客户端收到完整的响应数据之后触发.

代码示例:给服务器发送一个 GET 请求.

创建界面. 包含一个 QLineEdit , QPushButton

  • 先使用水平布局把 QLineEdit 和 QPushButton 放好, 并设置这两个控件的垂直方向的 sizePolicy 为 Expanding
  • 再使用垂直布局把 QPlainTextEdit 和上面的水平布局放好. ( QPlainTextEdit 的 readOnly 设为 true )
  • 设置垂直布局的 layoutStretch 为 5, 1 (当然这个尺寸比例根据个人喜好微调).

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

修改 widget.h, 创建 QNetworkAccessManager 属性

cpp 复制代码
class Widget : public QWidget
{
    Q_OBJECT

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

private:
    Ui::Widget *ui;
    
    QNetworkAccessManager* manager;
};

修改 widget.cpp, 创建实例

cpp 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    
    this->setWindowTitle("客户端");
    
    manager = new QNetworkAccessManager(this);
}

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

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

运行结果:

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


4.4 其他模块

Qt 中还提供了 FTP, DNS, SSL 等网络相关的组件工具. 此处不再展开介绍. 有需要的同学可以⾃行翻阅官方文档学习相关 API 的使用.


5. Qt 音视频

5.1 Qt 音频

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

通过帮助手册查看 QSound 类如下:

注意: 使用 QSound 类时,需要添加模块:multimedia.

核心API概览

  • play():开始或继续播放当前源。

代码示例:通过按钮播放音频

先准备一个wav格式的音频文件在桌面

然后使用qrc管理,导入到项目中

编辑ui文件,拖拽一个按钮

编辑 widget.cpp 文件,创建QSound实例

cpp 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    
    sound = new QSound(":/manbo.wav", this);
}

编写槽函数

cpp 复制代码
void Widget::on_pushButton_clicked()
{
    sound->play();
}

运行结果:

5.2 Qt 视频

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

核心API概览

setMedia() 设置当前媒体源。
setVideoOutput() 将QVideoWidget视频输出附加到媒体播放器。 如果媒体播放器已经附加了视频输出,将更换一个新的。

示例

首先在 .pro 文件中添加 multimedia 和 multimediawidgets 两个模块;如下图示:

cpp 复制代码
/********************************* 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()
{
 
}
相关推荐
醇氧2 小时前
【学习】封锁协议
网络·学习·oracle
徐子元竟然被占了!!2 小时前
ISIS实验1
网络
广州赛远2 小时前
埃夫特ER6L码垛机器人防护服等级解析:避开选型误区与性能陷阱
网络·人工智能
黄昏恋慕黎明2 小时前
网络聊天室测试报告
网络
2301_805962932 小时前
ESP32 HTTP OTA 本地测试教程
网络·网络协议·http
小陈工2 小时前
ModelEngine智能体开发实战:知识库自动生成与多Agent协作
大数据·网络·数据库·人工智能·python·django·异步
奥地利落榜美术生灬2 小时前
图床项目总结
网络
Rabbit_QL2 小时前
【HTTP协议解读】01小白篇:理解互联网的通信语言HTTP
网络·网络协议·http
liulilittle2 小时前
LINUX RING BUFFER TUN/TAP 2
linux·运维·服务器·开发语言·网络·c++