开源 C++ QT Widget 开发(十五)多媒体--音频播放

文章的目的为了记录使用C++ 进行QT Widget 开发学习的经历。临时学习,完成app的开发。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。

相关链接:

开源 C++ QT Widget 开发(一)工程文件结构-CSDN博客

开源 C++ QT Widget 开发(二)基本控件应用-CSDN博客

开源 C++ QT Widget 开发(三)图表--波形显示器-CSDN博客

开源 C++ QT Widget 开发(四)文件--二进制文件查看编辑-CSDN博客

开源 C++ QT Widget 开发(五)通讯--串口调试-CSDN博客

开源 C++ QT Widget 开发(六)通讯--TCP调试-CSDN博客

开源 C++ QT Widget 开发(七)线程--多线程及通讯-CSDN博客

开源 C++ QT Widget 开发(八)网络--Http文件下载-CSDN博客

开源 C++ QT Widget 开发(九)图表--仪表盘-CSDN博客

开源 C++ QT Widget 开发(十)IPC进程间通信--共享内存-CSDN博客

开源 C++ QT Widget 开发(十一)进程间通信--Windows 窗口通信-CSDN博客

开源 C++ QT Widget 开发(十二)图表--环境监测表盘-CSDN博客

开源 C++ QT Widget 开发(十三)IPC通讯--本地套接字 (Local Socket)

开源 C++ QT Widget 开发(十四)多媒体--录音机

开源 C++ QT Widget 开发(十五)多媒体--音频播放

推荐链接:

开源 java android app 开发(一)开发环境的搭建-CSDN博客

开源 java android app 开发(二)工程文件结构-CSDN博客

开源 java android app 开发(三)GUI界面布局和常用组件-CSDN博客

开源 java android app 开发(四)GUI界面重要组件-CSDN博客

开源 java android app 开发(五)文件和数据库存储-CSDN博客

开源 java android app 开发(六)多媒体使用-CSDN博客

开源 java android app 开发(七)通讯之Tcp和Http-CSDN博客

开源 java android app 开发(八)通讯之Mqtt和Ble-CSDN博客

开源 java android app 开发(九)后台之线程和服务-CSDN博客

开源 java android app 开发(十)广播机制-CSDN博客

开源 java android app 开发(十一)调试、发布-CSDN博客

开源 java android app 开发(十二)封库.aar-CSDN博客

推荐链接:

开源C# .net mvc 开发(一)WEB搭建_c#部署web程序-CSDN博客

开源 C# .net mvc 开发(二)网站快速搭建_c#网站开发-CSDN博客

开源 C# .net mvc 开发(三)WEB内外网访问(VS发布、IIS配置网站、花生壳外网穿刺访问)_c# mvc 域名下不可訪問內網,內網下可以訪問域名-CSDN博客

开源 C# .net mvc 开发(四)工程结构、页面提交以及显示_c#工程结构-CSDN博客

开源 C# .net mvc 开发(五)常用代码快速开发_c# mvc开发-CSDN博客

内容:Qt实现的WAV音频播放器应用程序。实现win10系统下,播放wav音频的功能。

目录:

1.功能介绍

2.核心代码分析

3.所有源码

4.显示效果

一.功能介绍

这是一个基于QMediaPlayer的音频播放器,主要功能包括:

音频播放控制:播放、停止、进度控制

文件检查:验证音频文件的存在和有效性

状态监控:实时显示播放状态和进度

错误处理:完善的错误检测和处理机制

日志系统:详细的运行日志记录

二、核心代码分析

头文件 (mainwindow.h)

复制代码
class MainWindow : public QMainWindow
{
    Q_OBJECT

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

private slots:
    void on_playButton_clicked();                      // 播放按钮点击
    void updatePlaybackProgress();                     // 更新播放进度
    void handlePlayerStateChanged(QMediaPlayer::State state); // 播放状态变化
    void handlePlayerError(QMediaPlayer::Error error); // 错误处理

private:
    Ui::MainWindow *ui;
    QMediaPlayer *player;          // 媒体播放器核心
    QTimer *progressTimer;         // 进度更新计时器
    QString audioFilePath;         // 音频文件路径

    void logMessage(const QString &message); // 日志记录
    bool checkAudioFile();                   // 文件检查
};

函数功能详细分析

  1. 构造函数 MainWindow::MainWindow()

功能:初始化播放器界面和组件

复制代码
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
    , player(new QMediaPlayer(this))        // 创建媒体播放器
    , progressTimer(new QTimer(this))       // 创建进度计时器
{
    ui->setupUi(this);
    setWindowTitle("16K WAV音频播放器 - Qt5.14"); // 设置窗口标题

    audioFilePath = QDir::currentPath() + "/Output.wav"; // 设置默认文件路径
    ui->filePathLabel->setText("文件: " + audioFilePath); // 显示文件路径

    player->setVolume(80); // 设置默认音量(80%)

    // 信号槽连接
    connect(player, SIGNAL(stateChanged(QMediaPlayer::State)),
            this, SLOT(handlePlayerStateChanged(QMediaPlayer::State)));
    connect(player, SIGNAL(error(QMediaPlayer::Error)),
            this, SLOT(handlePlayerError(QMediaPlayer::Error)));
    connect(progressTimer, &QTimer::timeout, this, &MainWindow::updatePlaybackProgress);

    // UI初始化
    ui->playButton->setText("开始播放");
    ui->statusLabel->setText("就绪");
    ui->progressBar->setValue(0);

    // 文件检查
    if (checkAudioFile()) {
        logMessage("音频文件检查正常,准备就绪");
    } else {
        ui->playButton->setEnabled(false); // 文件无效时禁用播放按钮
    }
}
  1. 析构函数 MainWindow::~MainWindow()

功能:安全清理资源

复制代码
MainWindow::~MainWindow()
{
    if (player->state() == QMediaPlayer::PlayingState) {
        player->stop(); // 如果正在播放,先停止
    }
    delete ui;
}
  1. checkAudioFile() - 音频文件检查

功能:验证音频文件的存在和有效性

复制代码
bool MainWindow::checkAudioFile()
{
    QFileInfo fileInfo(audioFilePath);

    if (!fileInfo.exists()) {
        logMessage("错误:音频文件不存在 - " + audioFilePath);
        ui->statusLabel->setText("错误:文件不存在");
        return false;
    }

    if (fileInfo.size() == 0) {
        logMessage("警告:音频文件为空");
        ui->statusLabel->setText("警告:文件为空");
        return false;
    }

    logMessage(QString("找到音频文件:%1 (%2 字节)")
              .arg(audioFilePath)
              .arg(fileInfo.size()));
    return true;
}
  1. on_playButton_clicked() - 播放控制

功能:处理播放/停止按钮点击事件

复制代码
void MainWindow::on_playButton_clicked()
{
    if (player->state() == QMediaPlayer::PlayingState) {
        // 停止播放
        logMessage("用户停止播放");
        player->stop();
        progressTimer->stop();
        ui->playButton->setText("开始播放");
    } else {
        // 开始播放
        if (!checkAudioFile()) {
            logMessage("播放失败:音频文件不可用");
            return;
        }

        logMessage("开始播放音频文件...");
        player->setMedia(QUrl::fromLocalFile(audioFilePath)); // 设置媒体源

        if (player->mediaStatus() == QMediaPlayer::InvalidMedia) {
            logMessage("错误:无法加载媒体文件");
            ui->statusLabel->setText("错误:无效的媒体文件");
            return;
        }

        player->play(); // 开始播放
        progressTimer->start(100); // 启动进度更新计时器(100ms间隔)
        logMessage("播放器启动完成");
    }
}
  1. updatePlaybackProgress() - 进度更新

功能:实时更新播放进度显示

复制代码
void MainWindow::updatePlaybackProgress()
{
    if (player->duration() > 0) {
        qint64 position = player->position(); // 当前位置(毫秒)
        qint64 duration = player->duration(); // 总时长(毫秒)

        int seconds = position / 1000;        // 转换为秒
        int totalSeconds = duration / 1000;

        // 格式化时间显示 (MM:SS / MM:SS)
        ui->timeLabel->setText(
            QString("%1:%2 / %3:%4")
            .arg(seconds / 60, 2, 10, QLatin1Char('0'))  // 当前分钟
            .arg(seconds % 60, 2, 10, QLatin1Char('0'))  // 当前秒
            .arg(totalSeconds / 60, 2, 10, QLatin1Char('0')) // 总分钟
            .arg(totalSeconds % 60, 2, 10, QLatin1Char('0')) // 总秒
        );

        // 计算进度百分比
        int progress = (duration > 0) ? static_cast<int>(position * 100 / duration) : 0;
        ui->progressBar->setValue(progress);

        // 每秒记录一次进度日志(避免过于频繁)
        static int lastSecond = -1;
        if (seconds != lastSecond) {
            lastSecond = seconds;
            logMessage(QString("播放进度: %1% (%2/%3 秒)")
                      .arg(progress)
                      .arg(seconds)
                      .arg(totalSeconds));
        }
    }
}
  1. handlePlayerStateChanged() - 状态变化处理

功能:响应播放器状态变化

复制代码
void MainWindow::handlePlayerStateChanged(QMediaPlayer::State state)
{
    switch (state) {
    case QMediaPlayer::StoppedState:
        logMessage("播放停止");
        ui->statusLabel->setText("播放完成");
        ui->playButton->setText("开始播放");
        progressTimer->stop(); // 停止进度更新
        ui->progressBar->setValue(100); // 进度条置满
        break;

    case QMediaPlayer::PlayingState:
        logMessage("播放进行中...");
        ui->statusLabel->setText("正在播放");
        ui->playButton->setText("停止播放");
        logMessage(QString("媒体时长: %1 毫秒").arg(player->duration()));
        break;

    case QMediaPlayer::PausedState:
        logMessage("播放暂停");
        ui->statusLabel->setText("播放暂停");
        ui->playButton->setText("继续播放");
        break;
    }
}
  1. handlePlayerError() - 错误处理

功能:处理播放器错误

复制代码
void MainWindow::handlePlayerError(QMediaPlayer::Error error)
{
    QString errorMsg;
    switch (error) {
    case QMediaPlayer::NoError:
        return;
    case QMediaPlayer::ResourceError:
        errorMsg = "资源错误:无法访问媒体文件";
        break;
    case QMediaPlayer::FormatError:
        errorMsg = "格式错误:不支持的媒体格式"; // WAV格式可能不支持
        break;
    case QMediaPlayer::NetworkError:
        errorMsg = "网络错误";
        break;
    case QMediaPlayer::AccessDeniedError:
        errorMsg = "访问被拒绝:没有足够的权限";
        break;
    default:
        errorMsg = "未知错误";
        break;
    }

    logMessage(errorMsg);
    ui->statusLabel->setText("播放错误");
    ui->playButton->setText("开始播放");

    QMessageBox::warning(this, "播放错误", errorMsg); // 弹出错误对话框
}
  1. logMessage() - 日志记录

功能:统一的日志记录系统

复制代码
void MainWindow::logMessage(const QString &message)
{
    QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss");
    QString logEntry = QString("[%1] %2").arg(timestamp).arg(message);

    ui->logTextEdit->append(logEntry); // 添加到UI日志框
    qDebug() << logEntry;              // 输出到控制台

    // 自动滚动到日志底部
    QTextCursor cursor = ui->logTextEdit->textCursor();
    cursor.movePosition(QTextCursor::End);
    ui->logTextEdit->setTextCursor(cursor);
}

三、所有源码

  1. pro文件

    QT += core gui
    QT += core gui multimedia
    greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

    CONFIG += c++11

    The following define makes your compiler emit warnings if you use

    any Qt feature that has been marked deprecated (the exact warnings

    depend on your compiler). Please consult the documentation of the

    deprecated API in order to know how to port your code away from it.

    DEFINES += QT_DEPRECATED_WARNINGS

    You can also make your code fail to compile if it uses deprecated APIs.

    In order to do so, uncomment the following line.

    You can also select to disable deprecated APIs only up to a certain version of Qt.

    #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0

    SOURCES +=
    main.cpp
    mainwindow.cpp

    HEADERS +=
    mainwindow.h

    FORMS +=
    mainwindow.ui

    Default rules for deployment.

    qnx: target.path = /tmp/$${TARGET}/bin
    else: unix:!android: target.path = /opt/$${TARGET}/bin
    !isEmpty(target.path): INSTALLS += target

  2. mainwindow.h文件

    #ifndef MAINWINDOW_H
    #define MAINWINDOW_H

    #include <QMainWindow>
    #include <QMediaPlayer>
    #include <QTimer>
    #include <QFileInfo>

    QT_BEGIN_NAMESPACE
    namespace Ui {
    class MainWindow;
    }
    QT_END_NAMESPACE

    class MainWindow : public QMainWindow
    {
    Q_OBJECT

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

    private slots:
    void on_playButton_clicked();
    void updatePlaybackProgress();
    void handlePlayerStateChanged(QMediaPlayer::State state);
    void handlePlayerError(QMediaPlayer::Error error);

    private:
    Ui::MainWindow *ui;
    QMediaPlayer *player;
    QTimer *progressTimer;
    QString audioFilePath;

    复制代码
     void logMessage(const QString &message);
     bool checkAudioFile();

    };

    #endif // MAINWINDOW_H

3.mianwindow.cpp文件

复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDir>
#include <QDateTime>
#include <QFileInfo>
#include <QDebug>
#include <QMessageBox>

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

    // 设置窗口标题
    setWindowTitle("16K WAV音频播放器 - Qt5.14");

    // 设置音频文件路径
    audioFilePath = QDir::currentPath() + "/Output.wav";
    ui->filePathLabel->setText("文件: " + audioFilePath);

    // 配置播放器
    player->setVolume(80); // 设置音量 0-100

    // 连接信号槽
    connect(player, SIGNAL(stateChanged(QMediaPlayer::State)),
            this, SLOT(handlePlayerStateChanged(QMediaPlayer::State)));
    connect(player, SIGNAL(error(QMediaPlayer::Error)),
            this, SLOT(handlePlayerError(QMediaPlayer::Error)));
    connect(progressTimer, &QTimer::timeout, this, &MainWindow::updatePlaybackProgress);

    // 初始化UI状态
    ui->playButton->setText("开始播放");
    ui->statusLabel->setText("就绪");
    ui->progressBar->setValue(0);

    // 检查音频文件
    if (checkAudioFile()) {
        logMessage("音频文件检查正常,准备就绪");
    } else {
        ui->playButton->setEnabled(false);
    }
}

MainWindow::~MainWindow()
{
    if (player->state() == QMediaPlayer::PlayingState) {
        player->stop();
    }
    delete ui;
}

bool MainWindow::checkAudioFile()
{
    QFileInfo fileInfo(audioFilePath);

    if (!fileInfo.exists()) {
        logMessage("错误:音频文件不存在 - " + audioFilePath);
        ui->statusLabel->setText("错误:文件不存在");
        return false;
    }

    if (fileInfo.size() == 0) {
        logMessage("警告:音频文件为空");
        ui->statusLabel->setText("警告:文件为空");
        return false;
    }

    logMessage(QString("找到音频文件:%1 (%2 字节)")
              .arg(audioFilePath)
              .arg(fileInfo.size()));
    return true;
}

void MainWindow::on_playButton_clicked()
{
    if (player->state() == QMediaPlayer::PlayingState) {
        // 停止播放
        logMessage("用户停止播放");
        player->stop();
        progressTimer->stop();
        ui->playButton->setText("开始播放");
    } else {
        // 开始播放
        if (!checkAudioFile()) {
            logMessage("播放失败:音频文件不可用");
            return;
        }

        logMessage("开始播放音频文件...");
        player->setMedia(QUrl::fromLocalFile(audioFilePath));

        if (player->mediaStatus() == QMediaPlayer::InvalidMedia) {
            logMessage("错误:无法加载媒体文件");
            ui->statusLabel->setText("错误:无效的媒体文件");
            return;
        }

        player->play();

        // 启动进度更新计时器
        progressTimer->start(100);
        logMessage("播放器启动完成");
    }
}

void MainWindow::updatePlaybackProgress()
{
    if (player->duration() > 0) {
        qint64 position = player->position();
        qint64 duration = player->duration();

        int seconds = position / 1000;
        int totalSeconds = duration / 1000;

        // 更新时间显示
        ui->timeLabel->setText(
            QString("%1:%2 / %3:%4")
            .arg(seconds / 60, 2, 10, QLatin1Char('0'))
            .arg(seconds % 60, 2, 10, QLatin1Char('0'))
            .arg(totalSeconds / 60, 2, 10, QLatin1Char('0'))
            .arg(totalSeconds % 60, 2, 10, QLatin1Char('0'))
        );

        // 更新进度条
        int progress = (duration > 0) ? static_cast<int>(position * 100 / duration) : 0;
        ui->progressBar->setValue(progress);

        // 记录播放进度
        static int lastSecond = -1;
        if (seconds != lastSecond) {
            lastSecond = seconds;
            logMessage(QString("播放进度: %1% (%2/%3 秒)")
                      .arg(progress)
                      .arg(seconds)
                      .arg(totalSeconds));
        }
    }
}

void MainWindow::handlePlayerStateChanged(QMediaPlayer::State state)
{
    switch (state) {
    case QMediaPlayer::StoppedState:
        logMessage("播放停止");
        ui->statusLabel->setText("播放完成");
        ui->playButton->setText("开始播放");
        progressTimer->stop();
        ui->progressBar->setValue(100);
        break;

    case QMediaPlayer::PlayingState:
        logMessage("播放进行中...");
        ui->statusLabel->setText("正在播放");
        ui->playButton->setText("停止播放");
        logMessage(QString("媒体时长: %1 毫秒").arg(player->duration()));
        break;

    case QMediaPlayer::PausedState:
        logMessage("播放暂停");
        ui->statusLabel->setText("播放暂停");
        ui->playButton->setText("继续播放");
        break;
    }
}

void MainWindow::handlePlayerError(QMediaPlayer::Error error)
{
    QString errorMsg;
    switch (error) {
    case QMediaPlayer::NoError:
        return;
    case QMediaPlayer::ResourceError:
        errorMsg = "资源错误:无法访问媒体文件";
        break;
    case QMediaPlayer::FormatError:
        errorMsg = "格式错误:不支持的媒体格式";
        break;
    case QMediaPlayer::NetworkError:
        errorMsg = "网络错误";
        break;
    case QMediaPlayer::AccessDeniedError:
        errorMsg = "访问被拒绝:没有足够的权限";
        break;
    default:
        errorMsg = "未知错误";
        break;
    }

    logMessage(errorMsg);
    ui->statusLabel->setText("播放错误");
    ui->playButton->setText("开始播放");

    QMessageBox::warning(this, "播放错误", errorMsg);
}

void MainWindow::logMessage(const QString &message)
{
    QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss");
    QString logEntry = QString("[%1] %2").arg(timestamp).arg(message);

    ui->logTextEdit->append(logEntry);
    qDebug() << logEntry;

    // 自动滚动到底部
    QTextCursor cursor = ui->logTextEdit->textCursor();
    cursor.movePosition(QTextCursor::End);
    ui->logTextEdit->setTextCursor(cursor);
}

4.mainwindow.ui文件

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>600</width>
    <height>500</height>
   </rect>
  </property>
  <property name="minimumSize">
   <size>
    <width>600</width>
    <height>500</height>
   </size>
  </property>
  <property name="windowTitle">
   <string>16K WAV音频播放器</string>
  </property>
  <widget class="QWidget" name="centralWidget">
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <widget class="QLabel" name="titleLabel">
      <property name="text">
       <string>16K WAV音频播放器 (Qt5.14)</string>
      </property>
      <property name="alignment">
       <set>Qt::AlignCenter</set>
      </property>
      <property name="font">
       <font>
        <pointsize>16</pointsize>
        <bold>true</bold>
       </font>
      </property>
     </widget>
    </item>
    <item>
     <widget class="QLabel" name="filePathLabel">
      <property name="text">
       <string>文件路径显示</string>
      </property>
      <property name="frameShape">
       <enum>QFrame::Panel</enum>
      </property>
      <property name="frameShadow">
       <enum>QFrame::Sunken</enum>
      </property>
      <property name="wordWrap">
       <bool>true</bool>
      </property>
     </widget>
    </item>
    <item>
     <widget class="QLabel" name="timeLabel">
      <property name="text">
       <string>00:00 / 00:00</string>
      </property>
      <property name="alignment">
       <set>Qt::AlignCenter</set>
      </property>
      <property name="font">
       <font>
        <pointsize>18</pointsize>
        <bold>true</bold>
       </font>
      </property>
     </widget>
    </item>
    <item>
     <widget class="QProgressBar" name="progressBar">
      <property name="value">
       <number>0</number>
      </property>
      <property name="textVisible">
       <bool>true</bool>
      </property>
      <property name="format">
       <string>%p%</string>
      </property>
     </widget>
    </item>
    <item>
     <widget class="QPushButton" name="playButton">
      <property name="text">
       <string>开始播放</string>
      </property>
      <property name="minimumSize">
       <size>
        <width>0</width>
        <height>50</height>
       </size>
      </property>
      <property name="styleSheet">
       <string>QPushButton {
    background-color: #4CAF50;
    color: white;
    font-weight: bold;
    font-size: 14px;
    border-radius: 5px;
    border: none;
}
QPushButton:hover {
    background-color: #45a049;
}
QPushButton:pressed {
    background-color: #3d8b40;
}
QPushButton:disabled {
    background-color: #cccccc;
}</string>
      </property>
     </widget>
    </item>
    <item>
     <widget class="QLabel" name="statusLabel">
      <property name="text">
       <string>就绪</string>
      </property>
      <property name="alignment">
       <set>Qt::AlignCenter</set>
      </property>
      <property name="frameShape">
       <enum>QFrame::Box</enum>
      </property>
      <property name="font">
       <font>
        <pointsize>10</pointsize>
        <bold>true</bold>
       </font>
      </property>
     </widget>
    </item>
    <item>
     <widget class="QGroupBox" name="groupBox">
      <property name="title">
       <string>播放日志</string>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout_2">
       <item>
        <widget class="QTextEdit" name="logTextEdit">
         <property name="maximumHeight">
          <number>200</number>
         </property>
         <property name="readOnly">
          <bool>true</bool>
         </property>
         <property name="placeholderText">
          <string>播放日志将显示在这里...</string>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
    </item>
   </layout>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>

四、显示效果

工程文件夹下需要有Output.wav,点击播放按钮则开始播放。

相关推荐
兆龙电子单片机设计2 小时前
【STM32项目开源】STM32单片机智能恒温箱控制系统
stm32·单片机·物联网·开源·毕业设计
大写-凌祁3 小时前
零基础入门深度学习:从理论到实战,GitHub+开源资源全指南(2025最新版)
人工智能·深度学习·开源·github
零点零一3 小时前
VS+QT的编程开发工作:关于QT VS tools的使用 qt的官方帮助
开发语言·qt
JosieBook3 小时前
【远程运维】Linux 远程连接 Windows 好用的软件:MobaXterm 实战指南
linux·运维·windows
文档搬运工3 小时前
Linux MInt启动速度的优化
linux
Broken Arrows4 小时前
Linux学习——管理网络安全(二十一)
linux·学习·web安全
鹅毛在路上了5 小时前
C++, ffmpeg, libavcodec-RTSP拉流,opencv实时预览
c++·opencv·ffmpeg
Light605 小时前
领码方案|Linux 下 PLT → PDF 转换服务超级完整版:异步、权限、进度
linux·pdf·可观测性·异步队列·plt转pdf·权限治理·进度查询
John_ToDebug5 小时前
定制 ResourceBundle 的实现与 DuiLib 思想在 Chromium 架构下的应用解析
c++·chrome·ui