「Qt5 开发日记」极简音乐播放 Demo
1 UI 界面设计

在 Qt Designer 中设计一个简单的音乐播放程序界面。包括:
-
⚙️ 顶部的标题栏,放在名为
titleBar的 QWidget 容器中,由一个横向的 spacer 和一个关闭按钮btnClose组成。用于自定义实现窗口的拖拽和关闭。 -
🎶 核心区域,放在名为
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 | |
-
✅ 按钮的美化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17QPushButton {
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
20QPushButton#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配合固定尺寸,形成圆形按钮
- 圆角设计:
-
🎈 进度条的美化
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
26QSlider::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 高度,圆角 3pxsub-page(已播放部分):使用蓝色渐变效果,从#0d6efd到#0b5ed7add-page(未播放部分):浅灰色
圆形滑块设计:
- 14px × 14px 正方形 +
border-radius: 7px(半径 = 宽高的一半)形成完美圆形 margin: -4px 0滑块在视觉上居中于轨道(滑块 14px 高,轨道 6px 高,差值 8px,上下各偏移 -4px)
-
⭐ 整体界面美化
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 | |
播放器在构造函数中的初始化:
1 | |
将 this 作为父对象传入,利用 Qt 的父子对象机制,当 MusicPlayer 销毁时,QMediaPlayer 也会自动销毁,无需手动 delete。
2.1 选择音频文件功能的实现
在窗口的构造函数中,需要添加 选择文件按钮 的点击按钮信号 QPushButton::clicked 与对应槽函数 onSelectFile 的连接:
1 | |
当用户点击按钮 btnSelectFile,会发出 QPushButton::clicked 信号。信号接收的对象是 this(创建的窗口类 MusicPlayer),接收信号后调用 MusicPlayer::onSelectFile 选择文件函数。
MusicPlayer::onSelectFile 函数实现:
1 | |
-
文件对话框的调用
使用QFileDialog::getOpenFileName()弹出系统原生的文件选择对话框。函数接受几个参数:- 父窗口指针 (
this) - 对话框标题 (“选择音频文件”)
- 初始目录 (空字符串表示使用默认)
- 文件过滤器 (支持多种音频格式)
- 父窗口指针 (
-
多格式支持
文件过滤器字符串使用双分号;;分隔不同的过滤选项。 -
媒体加载
当用户成功选择文件后(filePath不为空),通过m_player->setMedia()将本地文件路径转换为QMediaContent对象并加载到播放器中。使用QUrl::fromLocalFile()是为了正确处理文件路径格式。 -
UI 状态初始化
加载新文件后,需要重置播放器的 UI 状态:- 将进度条范围设为
(0, 0),因为此时还不知道音频时长 - 将进度条值设为
0 - 重置时间显示为
"0:00/0:00"
- 将进度条范围设为
当媒体文件加载完成后,播放器会自动触发 durationChanged 信号,届时会更新正确的时长范围。
2.2 播放音频文件功能的实现
在窗口的构造函数中,需要添加 播放/暂停按钮 的点击按钮信号 QPushButton::clicked 与对应槽函数 onPlayMusic 的连接:
1 | |
onPlayMusic() 函数的实现:
1 | |
还需要监听播放器的状态变化,以便动态更新按钮图标。此处用一个匿名函数实现播放/暂停按钮状态的切换:
1 | |
-
状态判断
通过m_player->state()获取当前播放器状态,判断是否正在播放。 -
播放/暂停切换
- 如果当前正在播放
PlayingState,则调用pause()暂停播放 - 如果当前不在播放状态,则调用
play()开始播放
- 如果当前正在播放
-
图标自动更新
在构造函数中已经连接了stateChanged信号,当播放器状态改变时,按钮图标会自动更新。
3 进度条设计
进度条需要实时显示播放进度,支持用户点击和拖拽来跳转播放位置。
3.1 进度条和播放器的同步
进度条与播放器的同步是一个 双向绑定问题:播放器播放时需要更新进度条,用户操作进度条时又需要控制播放器。
1 | |
- 播放器 → UI:
positionChanged和durationChanged用于更新进度条 - 进度条 → 播放器:
sliderPressed、sliderReleased和sliderClicked用于处理用户操作
当音频播放时,播放器会持续发出 QMediaPlayer::positionChanged 信号。每当发出 QMediaPlayer::positionChanged 信号,需要对进度条的位置以及播放时间进行更新:
1 | |
-
拖拽状态检查
在播放时,如果处于拖拽进度条的过程中,暂时不更新进度条的状态,由用户掌控进度条的状态。只有松开进度条滑块后才继续由播放器更新进度条状态。进度条更新了位置,进度条发出
MusicPlayer::onSliderReleased信号,进而再导致播放器位置的改变。如果不设置这个m_sliderPressed判断是否处于拖拽过程中的条件,在持续拖动进度条的过程中,音频跳转操作会出现明显的杂音。 -
信号阻塞机制
此外,还设置了 信号阻塞机制。使用blockSignals(true/false)包裹setValue()调用:- 阻止
setValue()触发valueChanged信号 - 避免引发不必要的信号级联,提高性能
- 防止进度条更新时触发其他副作用
- 阻止
-
时间显示更新
无论是否在拖拽,时间显示始终更新。使用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 | |
- 设置进度条范围:将范围设为
0到音频总时长(毫秒),使进度条的值直接对应播放位置 - 显示总时长:更新时间标签,显示"0:00/总时长"
3.2 点击进度条功能的实现
拖拽交互涉及三个关键状态:按下、拖动中、释放。
-
进度条滑块按下时
1
2
3
4void MusicPlayer::onSliderPressed()
{
m_sliderPressed = true;
}设置
m_sliderPressed标志位为true,通知onPositionChanged()函数暂停更新进度条,将进度条的控制权交给用户。 -
进度条滑块释放时
1
2
3
4
5void MusicPlayer::onSliderReleased()
{
m_sliderPressed = false;
m_player->setPosition(ui->musicSlider->value());
}当进度条滑块松开:
- 重置
m_sliderPressed标志位,继续由播放器自动更新进度条 - 调用
setPosition()将播放器跳转到用户拖拽后的位置
- 重置
标准的 QSlider 类其实是 不支持 进度条点击跳转的,此时需要通过自定义一个 Slider 子类,重写鼠标点击事件,扩展进度条跳转的功能。
1 | |
通过重写鼠标点击事件,当鼠标在点击的位置松开时发送 自定义 sliderClicked 信号,调用对应槽函数实现播放器位置的跳转:
1 | |
在 Slider::mouseReleaseEvent 中:
-
状态检查
首先检查
m_isPressed标志位,确保这是一个完整的"按下→释放"操作。在mousePressEvent中m_isPressed标志位被设置为true,并记录了点击的坐标位置。 -
水平与垂直的不同处理
- 水平进度条:比较 X 坐标,使用
width()计算比例 - 垂直进度条:比较 Y 坐标,使用
height()计算比例
- 水平进度条:比较 X 坐标,使用
-
位置差异计算
1
2
3int 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 个像素是一个在容错性和准确性之间取得平衡的中间值。
-
坐标转换:从像素到播放位置
1
2
3
4qint64 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
2signals:
void sliderClicked(qint64 value); // 点击轨道时发出目标位置 -
垂直进度条的特殊处理
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 | |
自定义 sliderClicked 信号传递的是鼠标点击的位置。对应槽函数接收点击位置的值,直接将播放器跳转到对应位置。