「Qt5 开发日记」极简音乐播放 Demo

1 UI 界面设计

在 Qt Designer 中设计一个简单的音乐播放程序界面。包括:

  1. ⚙️ 顶部的标题栏,放在名为 titleBar 的 QWidget 容器中,由一个横向的 spacer 和一个关闭按钮 btnClose 组成。用于自定义实现窗口的拖拽和关闭。

  2. 🎶 核心区域,放在名为 widget 的 QWidget 容器中,由一个选择文件按钮 btnSelectFile、一个自定义 Slider 类(继承官方的 QSlider)进度条 musicSlider、一个文本标签(用于实时显示音乐播放进度)currentTime 和一个播放/暂停按钮 btnPlayMusic 组成。

可以选择音频文件,点击播放按钮进行音乐播放。播放过程中可以继续点击暂停按钮暂停。播放/暂停期间可以点击进度条调整播放位置,也可以拖拽进度条滑块调整播放位置。

当然,这些 Qt Designer 中的设置,最终呈现的 UI 界面还需要添加 icons 图标、QSS 样式来美化。😎

1.1 UI 所需资源文件

创建 Qt 资源文件,主要存放 UI 界面中需要用到的 icons 图标和 .qss 样式文件。

icons 图标文件我都是从 iconfont 阿里巴巴矢量图标库 中找现成的 .svg 格式的图标。UI 界面中需要的图标包括关闭按钮 btnClose 的图标、选择文件按钮 btnSelectFile 的图标以及播放/暂停按钮 btnPlayMusic 切换播放暂停状态的图标。

1.2 QSS 样式美化

在 窗口类的构造函数中添加 QSS 样式设置:

1
2
3
4
5
6
7
// 设置界面 QSS 样式
QFile styleFile(":/styles/light.qss");
if(styleFile.open(QIODevice::ReadOnly)){
QString styleSheet = QString::fromUtf8(styleFile.readAll());
setStyleSheet(styleSheet);
styleFile.close();
}
  1. 按钮的美化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    QPushButton {
    background-color: #ffffff;
    color: #333333;
    border: 1px solid #dee2e6;
    border-radius: 8px;
    padding: 8px 16px;
    font-size: 14px;
    min-height: 20px;
    }
    QPushButton:hover {
    background-color: #e9ecef;
    border-color: #0d6efd;
    }
    QPushButton:pressed {
    background-color: #0d6efd;
    color: white;
    }
    • 圆角设计border-radius: 8px 创建柔和的圆角效果
    • 交互反馈:hover 状态显示淡蓝色边框,pressed 状态变为蓝色背景
    • 扁平风格:白色背景 + 浅灰色边框

    图标按钮特殊处理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    QPushButton#btnSelectFile,
    QPushButton#btnPlayMusic {
    min-width: 32px;
    min-height: 32px;
    padding: 4px;
    border-radius: 20px;
    border: none;
    background-color: transparent;
    }
    QPushButton#btnSelectFile:hover,
    QPushButton#btnPlayMusic:hover {
    border: none;
    background-color: rgba(13, 110, 253, 0.12); /* 悬停:淡蓝色高亮 */
    }
    QPushButton#btnSelectFile:pressed,
    QPushButton#btnPlayMusic:pressed {
    border: none;
    background-color: rgba(13, 110, 253, 0.2); /* 按下:略深蓝色高亮 */
    color: inherit;
    }
    • 极简设计:透明背景、无边框
    • 微妙反馈:使用半透明蓝色背景(12% 和 20% 透明度)提供交互反馈
    • 圆形效果border-radius: 20px 配合固定尺寸,形成圆形按钮
  2. 🎈 进度条的美化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    QSlider::groove:horizontal {
    border: none;
    height: 6px;
    background: #e9ecef;
    border-radius: 3px;
    }
    QSlider::sub-page:horizontal {
    background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
    stop:0 #0d6efd, stop:1 #0b5ed7);
    border-radius: 3px;
    }
    QSlider::add-page:horizontal {
    background: #e9ecef;
    border-radius: 3px;
    }
    QSlider::handle:horizontal {
    width: 14px;
    height: 14px;
    margin: -4px 0;
    background: #0d6efd;
    border: none;
    border-radius: 7px; /* 圆形:半径=宽高的一半 */
    }
    QSlider::handle:horizontal:hover {
    background: #0b5ed7;
    }

    进度条状态采用三段式设计

    • groove(轨道):浅灰色背景,6px 高度,圆角 3px
    • sub-page(已播放部分):使用蓝色渐变效果,从 #0d6efd#0b5ed7
    • add-page(未播放部分):浅灰色

    圆形滑块设计

    • 14px × 14px 正方形 + border-radius: 7px(半径 = 宽高的一半)形成完美圆形
    • margin: -4px 0 滑块在视觉上居中于轨道(滑块 14px 高,轨道 6px 高,差值 8px,上下各偏移 -4px)
  3. 整体界面美化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 初始化 UI 设置
    ui->horizontalLayout->setAlignment(Qt::AlignVCenter);
    ui->horizontalLayout->setContentsMargins(4, 0, 4, 0);
    ui->horizontalLayout->setSpacing(6); // 控件间距
    ui->horizontalLayout->setAlignment(ui->btnSelectFile, Qt::AlignVCenter); // 核心区域分别设置控件垂直居中对齐
    ui->horizontalLayout->setAlignment(ui->musicSlider, Qt::AlignVCenter);
    ui->horizontalLayout->setAlignment(ui->btnPlayMusic, Qt::AlignVCenter);
    ui->horizontalLayout_2->setContentsMargins(4, 2, 4, 2); // 左上右下边距
    ui->horizontalLayout_2->setSpacing(0); // 控件间距
    ui->verticalLayout->setContentsMargins(0, 0, 0, 0); // 主布局无边距
    ui->verticalLayout->setSpacing(0); // titleBar 和 widget 间距为 0
    setWindowFlags(Qt::FramelessWindowHint | windowFlags()); // 无边框窗口
    setAttribute(Qt::WA_TranslucentBackground); // 透明背景
    ui->currentTime->setText("0:00/0:00");
    ui->btnSelectFile->setIcon(QIcon(":/icons/selectFile.svg"));
    ui->btnSelectFile->setIconSize(QSize(24, 24));
    ui->btnSelectFile->setText("");
    ui->btnPlayMusic->setIcon(QIcon(":/icons/pause.svg"));
    ui->btnPlayMusic->setIconSize(QSize(24, 24));
    ui->btnPlayMusic->setText("");
    ui->btnClose->setIcon(QIcon(":/icons/close.svg"));
    ui->btnClose->setIconSize(QSize(12, 12));
    ui->btnClose->setText("");

2 基础按钮功能

音乐播放器最核心也是最基础的功能是 选择音频文件 和 音频文件的播放。

需要在创建的窗口类头文件中添加必要的音频播放器成员变量:

1
2
3
#include <QMediaPlayer>

QMediaPlayer* m_player;

播放器在构造函数中的初始化:

1
2
3
4
5
6
7
8
9
MusicPlayer::MusicPlayer(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MusicPlayer)
, m_player(new QMediaPlayer(this))
{
ui->setupUi(this);

// ... 其余代码
}

this 作为父对象传入,利用 Qt 的父子对象机制,当 MusicPlayer 销毁时,QMediaPlayer 也会自动销毁,无需手动 delete

2.1 选择音频文件功能的实现

在窗口的构造函数中,需要添加 选择文件按钮 的点击按钮信号 QPushButton::clicked 与对应槽函数 onSelectFile 的连接:

1
connect(ui->btnSelectFile, &QPushButton::clicked, this, &MusicPlayer::onSelectFile);

当用户点击按钮 btnSelectFile,会发出 QPushButton::clicked 信号。信号接收的对象是 this(创建的窗口类 MusicPlayer),接收信号后调用 MusicPlayer::onSelectFile 选择文件函数。

MusicPlayer::onSelectFile 函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void MusicPlayer::onSelectFile()
{
QString filePath = QFileDialog::getOpenFileName(
this, "选择音频文件", "",
"音频文件 (*.mp3 *.wav *.flac *.ogg *.m4a *.aac *.wma *.opus);;MP3 文件 (*.mp3);;WAV 文件 (*.wav);;FLAC 文件 (*.flac);;所有文件 (*)"
);
if(!filePath.isEmpty()){
m_player->setMedia(QMediaContent(QUrl::fromLocalFile(filePath)));
// 先初始化进度条范围 (0, 0)
// 等发出 durationChanged 信号再更新进度条范围
ui->musicSlider->setRange(0, 0);
ui->musicSlider->setValue(0);
ui->currentTime->setText("0:00/0:00");
}
}
  1. 文件对话框的调用
    使用 QFileDialog::getOpenFileName() 弹出系统原生的文件选择对话框。函数接受几个参数:

    • 父窗口指针 (this)
    • 对话框标题 (“选择音频文件”)
    • 初始目录 (空字符串表示使用默认)
    • 文件过滤器 (支持多种音频格式)
  2. 多格式支持
    文件过滤器字符串使用双分号 ;; 分隔不同的过滤选项。

  3. 媒体加载
    当用户成功选择文件后(filePath 不为空),通过 m_player->setMedia() 将本地文件路径转换为 QMediaContent 对象并加载到播放器中。使用 QUrl::fromLocalFile() 是为了正确处理文件路径格式。

  4. UI 状态初始化
    加载新文件后,需要重置播放器的 UI 状态:

    • 将进度条范围设为 (0, 0),因为此时还不知道音频时长
    • 将进度条值设为 0
    • 重置时间显示为 "0:00/0:00"

当媒体文件加载完成后,播放器会自动触发 durationChanged 信号,届时会更新正确的时长范围。

2.2 播放音频文件功能的实现

在窗口的构造函数中,需要添加 播放/暂停按钮 的点击按钮信号 QPushButton::clicked 与对应槽函数 onPlayMusic 的连接:

1
connect(ui->btnPlayMusic, &QPushButton::clicked, this, &MusicPlayer::onPlayMusic);

onPlayMusic() 函数的实现:

1
2
3
4
5
6
7
8
void MusicPlayer::onPlayMusic()
{
if(m_player->state() == QMediaPlayer::PlayingState){
m_player->pause();
}else{
m_player->play();
}
}

还需要监听播放器的状态变化,以便动态更新按钮图标。此处用一个匿名函数实现播放/暂停按钮状态的切换:

1
2
3
4
5
6
7
connect(m_player, &QMediaPlayer::stateChanged, this, [=](QMediaPlayer::State state){
if(state == QMediaPlayer::PlayingState){
ui->btnPlayMusic->setIcon(QIcon(":/icons/play.svg"));
}else{
ui->btnPlayMusic->setIcon(QIcon(":/icons/pause.svg"));
}
});
  1. 状态判断
    通过 m_player->state() 获取当前播放器状态,判断是否正在播放。

  2. 播放/暂停切换

    • 如果当前正在播放 PlayingState,则调用 pause() 暂停播放
    • 如果当前不在播放状态,则调用 play() 开始播放
  3. 图标自动更新
    在构造函数中已经连接了 stateChanged 信号,当播放器状态改变时,按钮图标会自动更新。


3 进度条设计

进度条需要实时显示播放进度,支持用户点击和拖拽来跳转播放位置。

3.1 进度条和播放器的同步

进度条与播放器的同步是一个 双向绑定问题:播放器播放时需要更新进度条,用户操作进度条时又需要控制播放器。

1
2
3
4
5
6
7
8
// 播放器相关
connect(m_player, &QMediaPlayer::positionChanged, this, &MusicPlayer::onPositionChanged);
connect(m_player, &QMediaPlayer::durationChanged, this, &MusicPlayer::onDurationChanged);

// 进度条相关
connect(ui->musicSlider, &QSlider::sliderPressed, this, &MusicPlayer::onSliderPressed);
connect(ui->musicSlider, &QSlider::sliderReleased, this, &MusicPlayer::onSliderReleased);
connect(ui->musicSlider, &Slider::sliderClicked, this, &MusicPlayer::onSliderClicked);
  • 播放器 → UI: positionChangeddurationChanged 用于更新进度条
  • 进度条 → 播放器: sliderPressedsliderReleasedsliderClicked 用于处理用户操作

当音频播放时,播放器会持续发出 QMediaPlayer::positionChanged 信号。每当发出 QMediaPlayer::positionChanged 信号,需要对进度条的位置以及播放时间进行更新:

1
2
3
4
5
6
7
8
9
10
11
12
void MusicPlayer::onPositionChanged(qint64 position)
{
// 更新进度条,在拖拽过程中不更新
if(!m_sliderPressed){
ui->musicSlider->blockSignals(true);
ui->musicSlider->setValue(static_cast<int>(position));
ui->musicSlider->blockSignals(false);
}

// 更新时间
ui->currentTime->setText(getFormatTime(position) + "/" + getFormatTime(m_player->duration()));
}
  1. 拖拽状态检查
    在播放时,如果处于拖拽进度条的过程中,暂时不更新进度条的状态,由用户掌控进度条的状态。只有松开进度条滑块后才继续由播放器更新进度条状态。

    进度条更新了位置,进度条发出 MusicPlayer::onSliderReleased 信号,进而再导致播放器位置的改变。如果不设置这个 m_sliderPressed 判断是否处于拖拽过程中的条件,在持续拖动进度条的过程中,音频跳转操作会出现明显的杂音。

  2. 信号阻塞机制
    此外,还设置了 信号阻塞机制。使用 blockSignals(true/false) 包裹 setValue() 调用:

    • 阻止 setValue() 触发 valueChanged 信号
    • 避免引发不必要的信号级联,提高性能
    • 防止进度条更新时触发其他副作用
  3. 时间显示更新
    无论是否在拖拽,时间显示始终更新。使用 getFormatTime() 工具函数将毫秒转换为易读的时间格式,显示为"当前时间/总时长"的形式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 获取格式化时间
    static QString getFormatTime(qint64 totalMs){
    if(totalMs < 0){
    return "0:00";
    }

    int totalSeconds = static_cast<int>(totalMs / 1000);
    int hours = totalSeconds / 3600;
    int minutes = (totalSeconds % 3600) / 60;
    int seconds = totalSeconds % 60;

    if(hours > 0){
    return QString("%1:%2:%3")
    .arg(hours)
    .arg(minutes, 2, 10, QChar('0'))
    .arg(seconds, 2, 10, QChar('0'));
    }else{
    return QString("%1:%2")
    .arg(minutes)
    .arg(seconds, 2, 10, QChar('0'));
    }
    }

当音频文件加载完成后,播放器会发出 durationChanged 信号:

1
2
3
4
5
void MusicPlayer::onDurationChanged(qint64 duration)
{
ui->musicSlider->setRange(0, static_cast<int>(duration));
ui->currentTime->setText("0:00/" + getFormatTime(duration));
}
  1. 设置进度条范围:将范围设为 0 到音频总时长(毫秒),使进度条的值直接对应播放位置
  2. 显示总时长:更新时间标签,显示"0:00/总时长"

3.2 点击进度条功能的实现

拖拽交互涉及三个关键状态:按下、拖动中、释放。

  1. 进度条滑块按下时

    1
    2
    3
    4
    void MusicPlayer::onSliderPressed()
    {
    m_sliderPressed = true;
    }

    设置 m_sliderPressed 标志位为 true,通知 onPositionChanged() 函数暂停更新进度条,将进度条的控制权交给用户。

  2. 进度条滑块释放时

    1
    2
    3
    4
    5
    void MusicPlayer::onSliderReleased()
    {
    m_sliderPressed = false;
    m_player->setPosition(ui->musicSlider->value());
    }

    当进度条滑块松开:

    • 重置 m_sliderPressed 标志位,继续由播放器自动更新进度条
    • 调用 setPosition() 将播放器跳转到用户拖拽后的位置

标准的 QSlider 类其实是 不支持 进度条点击跳转的,此时需要通过自定义一个 Slider 子类,重写鼠标点击事件,扩展进度条跳转的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef SLIDER_H
#define SLIDER_H

#include <QSlider>
#include <QMouseEvent>

class Slider : public QSlider
{
Q_OBJECT
public:
explicit Slider(QWidget *parent = nullptr);

signals:
void sliderClicked(qint64 value); // 点击轨道时发出目标位置

protected:
void mousePressEvent(QMouseEvent *ev) override;
void mouseReleaseEvent(QMouseEvent *ev) override;

private:
bool m_isPressed = false;
QPoint m_pressPoint; // 点击位置坐标点
};

#endif // SLIDER_H

通过重写鼠标点击事件,当鼠标在点击的位置松开时发送 自定义 sliderClicked 信号,调用对应槽函数实现播放器位置的跳转:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include "slider.h"
#include <QtMath>

Slider::Slider(QWidget *parent) : QSlider(parent)
{

}

void Slider::mousePressEvent(QMouseEvent *ev)
{
m_isPressed = true; // 设置点击中状态
m_pressPoint = ev->pos(); // 记录点击的位置
QSlider::mousePressEvent(ev);
}

void Slider::mouseReleaseEvent(QMouseEvent *ev)
{
if(m_isPressed){
if(orientation() == Qt::Horizontal){
int current_x = ev->pos().x();
int end_x = m_pressPoint.x();
if(qAbs(current_x - end_x) < 3){ // 点击进度条和松开的位置相差不到 3 个像素,视为点击进度条位置,跳转播放位置
qint64 mx = maximum();
if(mx > 0 && width() > 0){
// 视为点击跳转,发出自定义信号,跳转到转换后的位置
emit(sliderClicked(static_cast<qint64>(mx * (static_cast<double>(end_x) / width()))));
}
}
}else{
int current_y = ev->pos().y();
int end_y = m_pressPoint.y();
if(qAbs(current_y - end_y) < 3){
qint64 mx = maximum();
if(mx > 0 && height() > 0){
emit(sliderClicked(static_cast<qint64>(mx * (static_cast<double>(height() - end_y) / height()))));
}
}
}
}
m_isPressed = false; // 重置点击状态
QSlider::mouseReleaseEvent(ev);
}

Slider::mouseReleaseEvent 中:

  1. 状态检查

    首先检查 m_isPressed 标志位,确保这是一个完整的"按下→释放"操作。在 mousePressEventm_isPressed 标志位被设置为 true,并记录了点击的坐标位置。

  2. 水平与垂直的不同处理

    • 水平进度条:比较 X 坐标,使用 width() 计算比例
    • 垂直进度条:比较 Y 坐标,使用 height() 计算比例
  3. 位置差异计算

    1
    2
    3
    int current_x = ev->pos().x();
    int end_x = m_pressPoint.x();
    if(qAbs(current_x - end_x) < 3){
    • ev->pos().x(): 获取鼠标释放时的 X 坐标
    • m_pressPoint.x(): 获取鼠标按下时的 X 坐标
    • qAbs(): 使用 Qt 提供的绝对值函数计算距离

    当位置相差小于 3 个像素,视为点击进度条。3 个像素是一个在容错性和准确性之间取得平衡的中间值。

  4. 坐标转换:从像素到播放位置

    1
    2
    3
    4
    qint64 mx = maximum();
    if(mx > 0 && width() > 0){
    emit(sliderClicked(static_cast<qint64>(mx * (static_cast<double>(end_x) / width()))));
    }

    步骤 1: 获取进度条最大值

    1
    qint64 mx = maximum();

    在音乐播放器中,这个值就是音频的总时长(毫秒)。例如,一首 3 分钟的歌曲maximum() 返回 180000。

    步骤 2: 边界检查

    1
    if(mx > 0 && width() > 0){

    确保不会发生除零错误,也避免在进度条未初始化时进行无意义的计算。

    步骤 3: 比例计算

    1
    static_cast<double>(end_x) / width()
    • end_x: 点击位置的 X 坐标(像素)
    • width(): 进度条的总宽度(像素)
    • 比值: 得到点击位置在进度条上的百分比

    举例说明:

    • 进度条宽度 400 像素
    • 点击位置 X = 100
    • 比例 = 100 / 400 = 0.25 (即 25% 的位置)

    步骤 4: 转换为实际值

    1
    mx * (static_cast<double>(end_x) / width())

    将百分比乘以最大值,得到实际的播放位置:

    步骤 5: 发出信号

    1
    emit(sliderClicked(static_cast<qint64>(...)));

    通过自定义信号 sliderClicked 将计算出的位置传递给外部:

    1
    2
    signals:
    void sliderClicked(qint64 value); // 点击轨道时发出目标位置
  5. 垂直进度条的特殊处理

    1
    emit(sliderClicked(static_cast<qint64>(mx * (static_cast<double>(height() - end_y) / height()))));

    height() - end_y 而不是直接用 end_y 是因为屏幕坐标系的原点在左上角:

    • Y 坐标向下递增
    • 但进度条的值应该是从下到上递增

    通过 height() - end_y 进行坐标系转换,确保点击进度条底部时值为 0,点击顶部时值为最大值。


自定义 Slider 类发出一个自定义 sliderClicked 信号,调用槽函数 onSliderClicked:

1
2
3
4
void MusicPlayer::onSliderClicked(qint64 value)
{
m_player->setPosition(value);
}

自定义 sliderClicked 信号传递的是鼠标点击的位置。对应槽函数接收点击位置的值,直接将播放器跳转到对应位置。



「Qt5 开发日记」极简音乐播放 Demo
https://marisamagic.github.io/2026/02/27/20260227/
作者
MarisaMagic
发布于
2026年2月27日
许可协议