【基于Qt的QQ音乐播放器开发实战:从0到1打造全功能音乐播放应用】
🌹 作者: 云小逸
🤟 个人主页: 云小逸的主页
🤟 motto: 要敢于一个人默默的面对自己,强大自己才是核心。不要等到什么都没有了,才下定决心去做。种一颗树,最好的时间是十年前,其次就是现在!学会自己和解,与过去和解,努力爱自己。希望春天来之前,我们一起面朝大海,春暖花开!
🥇 专栏:
- 动态规划
- C 语言
- C++
- Java 语言
- Linux 编程
- 算法
- 待续…
文章目录
- 📚 前言
- 一、项目前期规划与技术架构设计
- 1. 需求分析与功能拆解
- 核心功能列表
- 界面布局规划
- 2. 技术选型与架构设计
- 核心技术栈
- 架构分层
- 二、界面开发:从基础布局到自定义控件
- 1. Head区开发:细节决定体验
- 布局步骤
- 交互细节
- 2. Body左侧导航区:自定义按钮控件BtForm
- 控件设计
- 页面切换逻辑
- 3. 右侧内容区:页面切换与自定义布局
- 推荐页(RecPage)
- 我喜欢页/本地音乐页/最近播放页(CommonPage)
- 三、音乐核心功能实现:从播放到管理
- 1. 媒体播放引擎初始化
- QMediaPlayer配置
- 播放控制逻辑
- 2. 播放模式切换
- 三种模式实现
- 状态同步
- 3. 音乐文件管理
- 元数据解析
- 收藏与历史记录
- 四、数据持久化:SQLite数据库集成
- 1. 数据库表设计
- musicInfo表结构
- 2. 数据库操作封装
- 写入数据
- 查询数据
- 更新数据
- 3. 程序启动与退出处理
- 五、高级功能实现:动画与细节优化
- 1. 动画效果详解
- 按钮跳动动画
- 歌词窗口弹出动画
- 2. 用户体验优化
- 系统托盘功能
- 单实例运行
- 六、调试与问题解决
- 1. 常见问题汇总
- 问题1:窗口拖拽与按钮点击冲突
- 问题2:歌词不同步
- 2. 性能优化
- 列表项复用
- 数据库批量操作
- 七、项目打包与跨平台部署
- 1. Windows平台打包
- 步骤
- 注意事项
- 2. Linux/macOS平台适配
- 差异点
- 八、项目总结与未来规划
- 1. 技术亮点
- 2. 开发经验
- 3. 未来扩展方向
- 4. 面试高频问题解答
- 📣 结语
📚 前言
在Qt框架的学习旅程中,理论知识的积累需要通过实践项目来深化理解。作为一名专注于C++和Qt开发的学习者,我决定通过开发一个完整的音乐播放器项目,将所学的界面设计、自定义控件、媒体播放、数据库集成等知识串联起来。这个项目不仅是对所学内容的综合检验,更是一次从需求分析到项目落地的全流程实战。接下来,我将以详细的技术解析和开发步骤,带你走进这个项目的完整实现过程。
一、项目前期规划与技术架构设计
1. 需求分析与功能拆解
核心功能列表
功能模块 | 详细功能点 |
---|---|
界面交互 | 无边框窗口、鼠标拖拽、按钮动画、多页面切换、歌词窗口弹出动画、系统托盘图标 |
音乐播放 | 播放/暂停、上一曲/下一曲、多种播放模式、音量调节、进度条拖拽、歌词同步显示 |
音乐管理 | 本地音乐加载、收藏管理、最近播放记录、歌曲信息解析(标题/歌手/专辑) |
数据持久化 | 歌曲信息存储、收藏状态保存、历史播放记录存储、SQLite数据库集成 |
自定义控件 | 带动画的导航按钮、可交互列表项、个性化进度条、弹出式音量调节窗口 |
界面布局规划
- Head区:包含应用图标、搜索框(预留扩展)、功能按钮(皮肤切换、最小化、关闭)
- Body区左侧:在线音乐与我的音乐分类导航,使用自定义按钮控件(BtForm)
- Body区右侧:通过QStackedWidget实现页面切换,包含推荐页(轮播图)、我喜欢页、本地音乐页、最近播放页
- 播放控制区:歌曲信息显示、播放控制按钮、进度条、音量调节、歌词按钮
2. 技术选型与架构设计
核心技术栈
- 界面层:Qt Widget + Qt Designer(可视化布局) + QSS(界面美化)
- 逻辑层:自定义控件(继承QWidget/QPushButton)、信号槽机制、事件处理(鼠标事件、动画事件)
- 数据层:QMediaPlayer(媒体解析)、QMediaPlaylist(播放列表)、SQLite(数据持久化)
- 工具层:QPropertyAnimation(动画效果)、QFileDialog(文件选择)、QSharedMemory(单实例检测)
架构分层
// 核心类结构
class QQMusic : public QWidget {Q_OBJECT
public:QQMusic(QWidget *parent = nullptr);~QQMusic();void initUI(); // 界面初始化void initPlayer(); // 播放器初始化void initSQLite(); // 数据库初始化void connectSignalAndSlot(); // 信号槽连接
private:Ui::QQMusic *ui;QMediaPlayer *player;QMediaPlaylist *playList;QSqlDatabase sqlite;CommonPage *currentPage; // 当前显示页面// 其他成员变量...
};class BtForm : public QWidget {Q_OBJECT
public:BtForm(QWidget *parent = nullptr);void setIcon(const QString &iconPath, const QString &text, int pageId);void showAnimation(bool isShow); // 显示/隐藏动画
signals:void clicked(int pageId); // 点击信号
private:Ui::BtForm *ui;QPropertyAnimation *lineAnimations[4]; // 4个动画对象int pageId;
};
二、界面开发:从基础布局到自定义控件
1. Head区开发:细节决定体验
布局步骤
- 控件拖拽:从Qt Designer拖拽Widget作为Head容器,设置固定高度80px,水平布局。
- 左侧图标区:添加QLabel作为logo,设置背景图片为QQ音乐图标,通过QSS实现居中显示:
#logo {background-image: url(:/images/Logo.png);background-repeat: no-repeat;background-position: center;background-size: 80%; // 图标缩放比例
}
- 右侧功能区:搜索框使用QLineEdit,设置圆角边框和内边距;功能按钮(皮肤、最小化、关闭)通过QPushButton实现,去除文本,设置背景图片:
// 最小化按钮QSS
#min {background-image: url(:/images/min.png);width: 30px;height: 30px;
}
- 布局优化:使用Horizontal Spacer分隔控件,确保按钮右对齐,通过setMinimumSize和setMaximumSize固定按钮大小。
交互细节
- 禁止最大化:通过
ui->max->setEnabled(false)
禁用最大化按钮,保持窗口固定大小。 - 鼠标悬停效果:所有按钮使用QSS设置悬停时半透明背景,提升交互反馈:
QPushButton:hover {background-color: rgba(230, 0, 0, 0.1); // 淡红色背景
}
2. Body左侧导航区:自定义按钮控件BtForm
控件设计
- 结构:包含图标(QLabel)、文本(QLabel)、动画竖条(4个QLabel),整体布局在QWidget中,水平布局。
- 动画实现:使用QPropertyAnimation控制竖条的高度变化,实现上下跳动效果:
// line1动画设置
line1Anim = new QPropertyAnimation(ui->line1, "geometry");
line1Anim->setDuration(1500); // 动画时长1.5秒
line1Anim->setKeyValueAt(0, QRect(0, 15, 2, 0)); // 起始位置(底部隐藏)
line1Anim->setKeyValueAt(0.5, QRect(0, 0, 2, 15)); // 中间最高位置
line1Anim->setLoopCount(-1); // 无限循环
line1Anim->start();
- 点击事件:重写mousePressEvent,改变按钮背景色并发射信号:
void BtForm::mousePressEvent(QMouseEvent *event) {if (event->button() == Qt::LeftButton) {// 绿色背景ui->bgWidget->setStyleSheet("#bgWidget { background-color: #1ECD97; }");emit clicked(pageId); // 发送页面ID}QWidget::mousePressEvent(event);
}
页面切换逻辑
- 主窗口处理:在QQMusic类中接收BtForm的clicked信号,遍历所有BtForm控件,清除其他按钮的选中样式,显示当前按钮动画:
void QQMusic::onBtFormClick(int pageId) {QList<BtForm*> btForms = findChildren<BtForm*>();for (BtForm *btn : btForms) {if (btn->pageId() == pageId) {btn->showAnimation(true); // 显示动画btn->setStyleSheet("#bgWidget { background-color: #1ECD97; }");} else {btn->showAnimation(false); // 隐藏动画btn->setStyleSheet(""); // 恢复默认样式}}ui->stackedWidget->setCurrentIndex(pageId - 1); // 切换页面索引currentPage = getPageByIndex(pageId - 1); // 更新当前页面指针
}
3. 右侧内容区:页面切换与自定义布局
推荐页(RecPage)
- 轮播图效果:使用QScrollArea包裹内容,左右按钮切换推荐内容,自定义RecBox控件包含左右按钮和滚动列表:
// RecBox布局
leftBtn = new QPushButton("<", this);
rightBtn = new QPushButton(">", this);
listWidget = new QListWidget(this);
listWidget->setFlow(QListWidget::LeftToRight); // 水平排列
listWidget->setMovement(QListWidget::Snap); // 吸附式滚动
- RecBoxItem动画:鼠标悬停时图片上移10px,使用事件过滤器检测鼠标进入和离开:
bool RecBoxItem::eventFilter(QObject *watched, QEvent *event) {if (watched == ui->imageBox && event->type() == QEvent::Enter) {QPropertyAnimation *anim = new QPropertyAnimation(ui->imageBox, "pos");anim->setDuration(100);anim->setStartValue(ui->imageBox->pos());anim->setEndValue(ui->imageBox->pos() - QPoint(0, 10));anim->start();return true;} else if (watched == ui->imageBox && event->type() == QEvent::Leave) {QPropertyAnimation *anim = new QPropertyAnimation(ui->imageBox, "pos");anim->setDuration(150);anim->setStartValue(ui->imageBox->pos());anim->setEndValue(ui->imageBox->pos() + QPoint(0, 10));anim->start();return true;}return QWidget::eventFilter(watched, event);
}
我喜欢页/本地音乐页/最近播放页(CommonPage)
- 通用布局:包含标题Label、播放全部按钮、歌曲列表(QListWidget),使用ListItemBox作为列表项:
// ListItemBox结构
QPushButton *likeBtn; // 收藏按钮
QLabel *nameLabel, *singerLabel, *albumLabel;
// QSS样式:收藏按钮未选中时灰色,选中时红色
#likeBtn {background-image: url(:/images/like_gray.png);
}
#likeBtn:hover {background-image: url(:/images/like_red.png);
}
- 数据绑定:通过setMusicInfo方法接收Music对象,更新标签文本和收藏状态:
void ListItemBox::setMusicInfo(const Music &music) {nameLabel->setText(music.name());singerLabel->setText(music.singer());albumLabel->setText(music.album());likeBtn->setChecked(music.isLiked());
}
三、音乐核心功能实现:从播放到管理
1. 媒体播放引擎初始化
QMediaPlayer配置
void QQMusic::initPlayer() {player = new QMediaPlayer(this);playList = new QMediaPlaylist(this);// 默认播放模式:列表循环playList->setPlaybackMode(QMediaPlaylist::Loop);player->setPlaylist(playList);// 音量初始化player->setVolume(20);// 信号连接connect(player, &QMediaPlayer::stateChanged, this, &QQMusic::onPlayStateChanged);connect(playList, &QMediaPlaylist::currentIndexChanged, this, &QQMusic::onCurrentIndexChanged);
}
播放控制逻辑
- 播放/暂停按钮:根据播放器状态切换图标和操作:
void QQMusic::onPlayButtonClicked() {if (player->state() == QMediaPlayer::PlayingState) {player->pause();ui->playBtn->setIcon(QIcon(":/images/pause.png"));} else {player->play();ui->playBtn->setIcon(QIcon(":/images/play.png"));}
}
- 上一曲/下一曲:调用QMediaPlaylist的previous()和next(),并更新界面显示:
void QQMusic::onPreviousClicked() {playList->previous();updateCurrentSongInfo(); // 更新歌曲信息显示
}void QQMusic::onNextClicked() {playList->next();updateCurrentSongInfo();
}
2. 播放模式切换
三种模式实现
模式 | 实现代码 | 图标变化 |
---|---|---|
随机播放 | playList->setPlaybackMode(QMediaPlaylist::Random); | 切换为随机图标 |
单曲循环 | playList->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop); | 切换为循环图标 |
列表循环 | playList->setPlaybackMode(QMediaPlaylist::Loop); | 切换为列表图标 |
状态同步
- 通过监听
playbackModeChanged
信号,更新按钮图标:
void QQMusic::onPlaybackModeChanged(QMediaPlaylist::PlaybackMode mode) {switch (mode) {case QMediaPlaylist::Random:ui->modeBtn->setIcon(QIcon(":/images/shuffle.png"));break;case QMediaPlaylist::CurrentItemInLoop:ui->modeBtn->setIcon(QIcon(":/images/single_loop.png"));break;case QMediaPlaylist::Loop:ui->modeBtn->setIcon(QIcon(":/images/list_loop.png"));break;}
}
3. 音乐文件管理
元数据解析
- 使用QMediaPlayer解析歌曲信息,处理缺失字段(如未知歌手、未知专辑):
void Music::parseMetaData(const QUrl &url) {QMediaPlayer player;player.setMedia(url);while (!player.isMetaDataAvailable()) {QCoreApplication::processEvents(); // 处理事件循环,防止假死}name = player.metaData("Title").toString().trim() ?: "未知歌曲";singer = player.metaData("Author").toStringList().join(",").trim() ?: "未知歌手";album = player.metaData("AlbumTitle").toString().trim() ?: "未知专辑";duration = player.duration();
}
收藏与历史记录
- 收藏功能:点击ListItemBox的收藏按钮,更新Music对象的isLike状态,并刷新数据库:
void ListItemBox::onLikeBtnClicked() {isLiked = !isLiked;likeBtn->setIcon(isLiked ? ":/images/like_red.png" : ":/images/like_gray.png");emit likeStatusChanged(musicId, isLiked); // 发射信号到主窗口
}// 主窗口处理信号
void QQMusic::onLikeStatusChanged(QString musicId, bool isLiked) {Music *music = musicList.findMusicById(musicId);if (music) {music->setIsLike(isLiked);musicList.updateToDB(music); // 更新数据库currentPage->refreshList(); // 刷新当前页面列表}
}
- 历史记录:监听当前播放索引变化,标记歌曲为已播放:
void QQMusic::onCurrentIndexChanged(int index) {if (index >= 0 && index < playList->mediaCount()) {QMediaContent content = playList->media(index);QString musicId = getMusicIdByContent(content); // 通过内容获取musicIdMusic *music = musicList.findMusicById(musicId);if (music && !music->isHistory()) {music->setIsHistory(true);musicList.updateToDB(music); // 更新历史状态ui->recentPage->refreshList(); // 刷新最近播放页}}
}
四、数据持久化:SQLite数据库集成
1. 数据库表设计
musicInfo表结构
字段名 | 类型 | 说明 |
---|---|---|
id | INTEGER | 主键,自增 |
musicId | VARCHAR(200) | 唯一标识(UUID生成) |
musicName | VARCHAR(50) | 歌曲名称 |
musicSinger | VARCHAR(50) | 歌手 |
albumName | VARCHAR(50) | 专辑 |
duration | BIGINT | 时长(毫秒) |
musicUrl | VARCHAR(256) | 文件路径 |
isLike | INTEGER | 收藏状态(0/1) |
isHistory | INTEGER | 历史播放状态(0/1) |
2. 数据库操作封装
写入数据
void MusicList::insertMusic(const Music &music) {QSqlQuery query;query.prepare("INSERT INTO musicInfo (musicId, musicName, musicSinger, albumName, duration, musicUrl, isLike, isHistory) ""VALUES (:id, :name, :singer, :album, :duration, :url, :like, :history)");query.bindValue(":id", music.id());query.bindValue(":name", music.name());query.bindValue(":singer", music.singer());query.bindValue(":album", music.album());query.bindValue(":duration", music.duration());query.bindValue(":url", music.url().toLocalFile());query.bindValue(":like", music.isLiked() ? 1 : 0);query.bindValue(":history", music.isHistory() ? 1 : 0);if (!query.exec()) {qWarning() << "插入数据失败:" << query.lastError().text();}
}
查询数据
QVector<Music> MusicList::loadAllMusics() {QVector<Music> musics;QSqlQuery query("SELECT * FROM musicInfo");while (query.next()) {Music music;music.setId(query.value("musicId").toString());music.setName(query.value("musicName").toString());music.setSinger(query.value("musicSinger").toString());music.setAlbum(query.value("albumName").toString());music.setDuration(query.value("duration").toLongLong());music.setUrl(QUrl::fromLocalFile(query.value("musicUrl").toString()));music.setIsLike(query.value("isLike").toBool());music.setIsHistory(query.value("isHistory").toBool());musics.append(music);}return musics;
}
更新数据
void MusicList::updateMusic(const Music &music) {QSqlQuery query;query.prepare("UPDATE musicInfo SET musicName=:name, musicSinger=:singer, albumName=:album, isLike=:like, isHistory=:history WHERE musicId=:id");query.bindValue(":name", music.name());query.bindValue(":singer", music.singer());query.bindValue(":album", music.album());query.bindValue(":like", music.isLiked() ? 1 : 0);query.bindValue(":history", music.isHistory() ? 1 : 0);query.bindValue(":id", music.id());if (!query.exec()) {qWarning() << "更新数据失败:" << query.lastError().text();}
}
3. 程序启动与退出处理
- 启动时加载数据:在QQMusic构造函数中调用
loadFromDB()
,从数据库恢复歌曲列表:
void QQMusic::initSQLite() {sqlite = QSqlDatabase::addDatabase("QSQLITE");sqlite.setDatabaseName("QQMusic.db");if (!sqlite.open()) {QMessageBox::critical(this, "数据库错误", "无法打开数据库:" + sqlite.lastError().text());return;}// 创建表(如果不存在)createTables();// 加载数据musicList.loadFromDB();// 刷新界面refreshAllPages();
}
- 退出时保存数据:重写closeEvent,调用
saveToDB()
保存所有修改:
void QQMusic::closeEvent(QCloseEvent *event) {musicList.saveToDB(); // 保存所有歌曲信息event->accept(); // 允许关闭
}
五、高级功能实现:动画与细节优化
1. 动画效果详解
按钮跳动动画
- 技术点:QPropertyAnimation控制QLabel的geometry属性,实现竖条的上下移动:
// BtForm构造函数中初始化动画
for (int i = 0; i < 4; i++) {QLabel *line = getLine(i); // 获取第i个竖条animations[i] = new QPropertyAnimation(line, "geometry");animations[i]->setDuration(1800 + i * 100); // 不同延迟实现波浪效果animations[i]->setKeyValueAt(0, QRect(line->x(), 15, 2, 0)); // 底部animations[i]->setKeyValueAt(0.5, QRect(line->x(), 0, 2, 15)); // 顶部animations[i]->setLoopCount(-1); // 无限循环animations[i]->start();
}
歌词窗口弹出动画
- 滑动效果:使用QPropertyAnimation控制窗口的y坐标,实现从底部滑出:
void LrcPage::showAnimation() {QPropertyAnimation *anim = new QPropertyAnimation(this, "pos");anim->setDuration(300);anim->setStartValue(QPoint(pos().x(), height())); // 初始位置在窗口下方anim->setEndValue(pos()); // 显示位置anim->start();
}void LrcPage::hideAnimation() {QPropertyAnimation *anim = new QPropertyAnimation(this, "pos");anim->setDuration(300);anim->setStartValue(pos());anim->setEndValue(QPoint(pos().x(), height())); // 隐藏到窗口下方connect(anim, &QPropertyAnimation::finished, this, &LrcPage::hide);anim->start();
}
2. 用户体验优化
系统托盘功能
- 实现步骤:
- 创建QSystemTrayIcon并设置图标
- 创建右键菜单(还原、退出)
- 连接托盘点击事件显示主窗口:
void QQMusic::initTray() {trayIcon = new QSystemTrayIcon(this);trayIcon->setIcon(QIcon(":/images/tray_icon.png"));trayIcon->setToolTip("QQ音乐播放器");QMenu *trayMenu = new QMenu(this);trayMenu->addAction("显示", this, &QWidget::showNormal);trayMenu->addAction("退出", qApp, &QApplication::quit);trayIcon->setContextMenu(trayMenu);connect(trayIcon, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) {if (reason == QSystemTrayIcon::DoubleClick) {showNormal();}});trayIcon->show();
}
单实例运行
- 共享内存检测:在main函数中使用QSharedMemory检测是否已运行:
int main(int argc, char *argv[]) {QApplication a(argc, argv);QSharedMemory shared("QQMusicSingleInstance");if (shared.attach()) {QMessageBox::information(nullptr, "提示", "程序已运行!");return 0;}shared.create(1);QQMusic w;w.show();int ret = a.exec();shared.detach();return ret;
}
六、调试与问题解决
1. 常见问题汇总
问题1:窗口拖拽与按钮点击冲突
- 现象:点击按钮时偶尔触发窗口移动
- 解决:添加标志位
isDragging
,在mousePressEvent中判断点击位置是否在按钮上:
void QQMusic::mousePressEvent(QMouseEvent *event) {if (event->button() == Qt::LeftButton) {// 检测点击是否在按钮区域QWidget *child = childAt(event->pos());isDragging = !child->isKindOf(QPushButton::staticMetaObject.className());dragPos = event->globalPos() - pos();}QWidget::mousePressEvent(event);
}void QQMusic::mouseMoveEvent(QMouseEvent *event) {if (event->buttons() & Qt::LeftButton && isDragging) {move(event->globalPos() - dragPos);event->accept();}QWidget::mouseMoveEvent(event);
}
问题2:歌词不同步
- 原因:LRC时间解析错误,未处理不同格式(如[00:00.00]和[0:00.00])
- 解决:使用正则表达式统一解析时间格式:
QRegExp timeRegex("\\[(\\d+):(\\d+\\.?\\d*)\\]");
while (timeRegex.indexIn(line) != -1) {int min = timeRegex.cap(1).toInt();double sec = timeRegex.cap(2).toDouble();qint64 time = (min * 60 + sec) * 1000; // 转换为毫秒// 添加到歌词列表
}
2. 性能优化
列表项复用
- 问题:大量歌曲时界面卡顿
- 解决:使用QListWidget的setItemDelegate,重用ListItemBox实例,避免频繁创建销毁:
class ListItemDelegate : public QItemDelegate {
public:void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override {// 重用逻辑...}
};
数据库批量操作
- 问题:单条插入效率低
- 解决:使用事务处理批量插入:
void MusicList::batchInsert(const QVector<Music> &musics) {sqlite.transaction();QSqlQuery query;query.prepare("INSERT INTO musicInfo (...) VALUES (?, ?, ?, ...)");for (const Music &music : musics) {query.addBindValue(music.id());// 添加其他参数...query.addQuery();}query.execBatch();sqlite.commit();
}
七、项目打包与跨平台部署
1. Windows平台打包
步骤
- 编译Release版本:在Qt Creator中选择Release配置,构建项目。
- 创建目录:新建文件夹,复制生成的.exe文件。
- 运行windeployqt:
windeployqt QQMusic.exe
- 处理依赖:手动复制缺失的插件(如sqldrivers/qsqlite.dll)。
注意事项
- 64位/32位匹配:确保windeployqt版本与编译环境一致。
- 资源文件:通过qrc文件管理图片和歌词文件,确保打包时包含所有资源。
2. Linux/macOS平台适配
差异点
- 路径处理:使用QUrl处理文件路径,避免硬编码斜杠。
- 图标设置:使用.icns(macOS)和.png(Linux)格式图标。
- 打包工具:Linux使用linuxdeployqt,macOS使用macdeployqt。
八、项目总结与未来规划
1. 技术亮点
- 自定义控件体系:通过继承QWidget实现高度定制化的交互元素,提升界面一致性和可维护性。
- 数据驱动界面:通过信号槽机制实现音乐信息、播放状态与界面的实时同步。
- 轻量级持久化:SQLite数据库实现简单高效的数据存储,无需额外服务器支持。
2. 开发经验
- 模块化设计:将界面、逻辑、数据层分离,降低耦合度,方便后续扩展。
- 文档与注释:及时记录自定义控件的接口和逻辑,避免后期维护困难。
- 调试工具:善用Qt的QDebug、断点调试和Profiler分析性能瓶颈。
3. 未来扩展方向
- 网络模块:
- 实现在线音乐搜索(调用API)
- 支持歌单同步与云存储
- 功能增强:
- 均衡器调节(QAudioEqualizer)
- 歌词编辑与同步功能
- 界面优化:
- 支持皮肤更换(动态加载QSS样式)
- 响应式布局适配不同屏幕尺寸
4. 面试高频问题解答
Q:如何处理大量音乐文件的加载性能问题?
A:通过异步加载和分页显示,使用QThreadPool配合QRunnable实现多线程解析元数据,避免主线程阻塞。同时利用数据库索引加速查询,例如对musicUrl建立唯一索引。
Q:为什么选择SQLite而非其他数据库?
A:对于本地音乐播放器,数据量较小,SQLite无需安装、轻量级且跨平台,完全满足需求。若未来扩展为网络版,可切换为MySQL等客户端/服务器架构数据库。
Q:如何实现歌词的精确同步?
A:解析LRC文件时存储时间戳列表,监听QMediaPlayer的positionChanged信号,通过二分查找快速定位当前歌词行,结合动画实现滚动效果。
📣 结语
这个QQ音乐播放器项目是一次充满挑战的实践之旅,从界面的像素级打磨到播放逻辑的复杂实现,每一步都加深了我对Qt框架的理解。在这个过程中,我学会了如何将理论知识转化为实际代码,如何通过调试解决复杂问题,以及如何设计可扩展的软件架构。
技术的学习没有终点,未来我将继续探索Qt的更多可能性,比如3D界面、视频播放等功能。如果你在开发过程中遇到问题,欢迎在评论区留言,我们可以一起讨论解决方案。记住,每一次代码的敲击都是进步的印记,坚持下去,你一定能成为更优秀的开发者!
最后,感谢你的耐心阅读!如果觉得这篇博客对你有帮助,别忘了点赞、收藏和关注,我们下次项目实战再见! (๑•̀ㅂ•́)و✧
相关文章:
【基于Qt的QQ音乐播放器开发实战:从0到1打造全功能音乐播放应用】
🌹 作者: 云小逸 🤟 个人主页: 云小逸的主页 🤟 motto: 要敢于一个人默默的面对自己,强大自己才是核心。不要等到什么都没有了,才下定决心去做。种一颗树,最好的时间是十年前,其次就是现在&…...
蓝桥杯 3. 密码脱落
密码脱落 原题目链接 题目描述 X 星球的考古学家发现了一批古代留下来的密码。 这些密码是由 A、B、C、D 四种植物的种子串成的序列。 仔细分析发现,这些密码串当初应该是前后对称的(即镜像串)。 由于年代久远,其中许多种子…...
数学基础 -- 欧拉恒等式的魅力:让复数旋转起来!
公式推导: e i π − 1 e^{i\pi} -1 eiπ−1 被誉为数学中最美的公式之一,它连接了五个数学中最重要的常数: e i π 1 0 (欧拉恒等式) e^{i\pi} 1 0 \tag{欧拉恒等式} eiπ10(欧拉恒等式) 这不仅是巧合,而是复数与三角函数…...
keil修改字体无效,修改字体为“微软雅黑”方法
在网上下载了微软雅黑字体,微软雅黑参考下载链接 结果在Edit->Configuration中找不到这个字体 这个时候可以在keil的安装目录中找到UV4/global.prop文件 用记事本打开它进行编辑,把字体名字改成微软雅黑 重新打开keil就发现字体成功修改了。 这个…...
LeetCode 每日一题 2845. 统计趣味子数组的数目
2845. 统计趣味子数组的数目 给你一个下标从 0 开始的整数数组 nums ,以及整数 modulo 和整数 k 。 请你找出并统计数组中 趣味子数组 的数目。 如果 子数组 nums[l…r] 满足下述条件,则称其为 趣味子数组 : 在范围 [l, r] 内,设 …...
二进制兼容性分析方法
I. 引言 在软件工程领域,二进制兼容性(Binary Compatibility)是一个核心概念,它指的是一个计算系统能够运行为另一个系统编译的可执行代码(通常是机器码)的能力,而无需重新编译 。这与源代码兼…...
在 WSL 安装 OpenFOAM-12
在 WSL 安装 OpenFOAM-12 参考链接安装教程问题整理1、安装完成后运行测试算例 Alllrun 脚本报错 参考链接 OpenFOAM OpenFoam-v12 OpenFOAM-v12-Ubuntu 安装教程 直接在 OpenFOAM 官网找到 Down -> OpenFOAM v12 选择 DownLoad v12 | Ubuntu -> Read More 具体安装过…...
Linux-06 ubuntu 系统截图软件使用简单记录
文章目录 原委一、Shutter二、Flameshot三、Ksnip 原委 原先使用的 Flameshot 截图软件,在ubuntu 18.04下可以正常使用。 系统升级到22.04 后,安装后的只能截图,不能标注,想着修复下。 以下是个人备忘录记录,如何使用…...
基于python代码的通过爬虫方式实现快手发布视频(2025年4月)
1、将真实的快手创作服务平台的cookie贴到代码目录中kuaishou_cookie.txt文件里,运行python脚本即可; 2、运行之前根据import提示安装一些常见依赖,比如requests等; 3、__NS_sig3.js的源码见快手NS sig3签名算法(2025年1月)_快手sig3算法源码-CSDN博客 4、2025年4月份…...
人工智能与机器学习:Python从零实现逻辑回归模型
🧠 向所有学习者致敬! “学习不是装满一桶水,而是点燃一把火。” —— 叶芝 我的博客主页: https://lizheng.blog.csdn.net 🌐 欢迎点击加入AI人工智能社区! 🚀 让我们一起努力,共创…...
大模型助力嘉兴妇幼:数据分类分级的智能化飞跃
在医疗行业数字化转型进程中,数据已成为驱动服务升级与业务创新的核心要素。作为医疗行业数字化的探索者,嘉兴市妇幼保健院携手美创打造的智能化数据分类分级项目,数据识别率和分类分级率高达99%,分类分级准确率达90%,…...
MyBatisPlus文档
一、MyBatis框架回顾 使用springboot整合Mybatis,实现Mybatis框架的搭建 1、创建示例项目 (1)、创建工程 新建工程 创建空工程 创建模块 创建springboot模块 选择SpringBoot版本 (2)、引入依赖 <dependencies><dependency><groupId>org.springframework.…...
支持Function Call的本地ollama模型对比评测-》开发代理agent
目标是开发支持多个 Function 定义的智能体代理系统 ✅ 第一部分:是否支持多个函数(multi-function calling) 模型名称支持多个函数注册是否支持函数选择(name 匹配)是否能生成结构化参数OpenChat 3.5 / 3.5-0106✅&a…...
Docker拉取镜像代理配置实践与经验分享
Docker拉取镜像代理配置实践与经验分享 一、背景概述 在企业内网环境中,我们部署了多台用于测试与学习的服务器。近期,接到领导安排,需在其中一台服务器上通过Docker安装n8n应用程序。然而在实际操作过程中,遭遇Docker官方镜像库…...
Jinja 的详细介绍和学习方法
Jinja 是一种流行的 模板引擎(Template Engine),主要用于生成动态的文本内容(如 HTML、XML、配置文件等)。它最初是为 Python 的 Web 框架(如 Flask、Django)设计的,但也可以独立使用…...
在 Windows 系统上升级 Node.js
一、查询电脑端已经安装的 Node.js 版本 1、通过【winR】 键,输入 cmd,点击【确定】按钮打开 cmd 窗口 2、命令行界面输入 node -v 查看目前 Node.js 版本 3、命令行界面输入 npm -v 查看目前 npm 版本 二、进入官网地址下载安装包 1、官网地址&#x…...
特斯拉宣布启动自动驾驶网约车测试,无人出租车服务进入最后准备阶段
特斯拉公司于4月24日正式宣布,已在美国得克萨斯州奥斯汀和加利福尼亚州旧金山湾区启动自动驾驶网约车服务的员工内部测试。这项测试将为今年夏季计划推出的完全无人驾驶出租车服务进行最后的验证和准备。 此次测试使用约200辆经过特殊改装的Model 3车型,…...
C++面试复习(7)2025.4.25
1,说一说常用的 Linux 命令(NIUKE) 1,cat 查看文件内容 cat filename2,rm 删除 rm filename:删除文件。 rm -r directory:删除目录及其内容。 rm -f filename:强制删除文件(不询问确认&…...
使用 uv 工具快速创建 MCP 服务(Trae 配置并调用 MCP 服务)
文章目录 下载Traeuv 工具教程参考我的这篇文章创建 uv 项目main.pyTrae 配置 MCP 服务添加 MCP创建智能体 使用智能体调用 MCP 创建 demo 表查询 demo 表结构信息demo 表插入 2 条测试数据查询 demo 表中的数据 下载Trae https://www.trae.com.cn/ uv 工具教程参考我的这篇…...
07 Python 字符串全解析
文章目录 一. 字符串的定义二. 字符串的基本用法1. 访问字符串中的字符2. 字符串切片3. 字符串拼接4. 字符串重复5.字符串比较6.字符串成员运算 三. 字符串的常用方法1. len() 函数2. upper() 和 lower() 方法3. strip() 方法4. replace() 方法5. split() 方法 四. 字符串的进阶…...
IEEE期刊目录重磅更新!共242本期刊被收录!
🔥 🔥 🔥 🔥 【IEEE期刊目录】 2025年2月,IEEE出版社更新了242本期刊目录,其中: • 完全OA期刊36本:SCI有6本,ESCI有15本; • 混合OA期刊183本&…...
微型计算机原理与接口技术第六版第四章课后习题答案-周荷琴,冯焕清-中国科学技术大学出版社
第六版书籍编排的第3章和第4章,仅这两章习题答案跟第四版的答案是相同的,可以参考第四版的答案 原第四版习题答案,蓝奏云: https://wwss.lanzouq.com/iWXOY1kvk3yf 第六版的第四章的习题我没有单独编辑,它是在上面第四…...
反爬策略应对指南:淘宝 API 商品数据采集的 IP 代理与请求伪装技术
一、引言 在电商数据驱动决策的时代,淘宝平台海量的商品数据极具价值。然而,淘宝为保障平台安全和用户体验,构建了严密的反爬体系。当采集淘宝 API 商品数据时,若不采取有效措施,频繁的请求极易触发反爬机制&#x…...
前端技术Ajax入门
1.1 AJAX 概念和 axios 使用 目标 了解 AJAX 概念并掌握 axios 库基本使用 讲解 1. 什么是 AJAX? 使用浏览器的 XMLHttpRequest 对象与服务器通信。在浏览器网页中,通过 AJAX 技术(XHR 对象)发起获取省份列表数据的请求&…...
【沉浸式求职学习day25】【部分网络编程知识分享】【基础概念以及简单代码】
不知道大家一直高强度学习自己是什么样的感觉,反正我现在逐渐变得麻木了,马上又要实习笔试了,每次笔试都要突击,每次突击都意识到自己有太多不会的,主打一个心累,但是又能怎样呢,自己选的路就是…...
聊聊Spring AI Alibaba的YoutubeDocumentReader
序 本文主要研究一下Spring AI Alibaba的YoutubeDocumentReader YoutubeDocumentReader community/document-readers/spring-ai-alibaba-starter-document-reader-youtube/src/main/java/com/alibaba/cloud/ai/reader/youtube/YoutubeDocumentReader.java public class You…...
常用第三方库:flutter_boost混合开发
常用第三方库:flutter_boost混合开发 前言 在移动应用开发中,混合开发是一个非常重要的话题。特别是对于已有原生应用想要引入Flutter的团队来说,如何实现Flutter页面和原生页面的无缝整合就显得尤为关键。本文将深入介绍flutter_boost这个…...
什么是 JSON?学习JSON有什么用?在springboot项目里如何实现JSON的序列化和反序列化?
作为一个学习Javaweb的新手,理解JSON的序列化和反序列化非常重要,因为它在现代Web开发,特别是Spring Boot中无处不在。 什么是 JSON? 首先,我们简单了解一下JSON (JavaScript Object Notation)。 JSON 是一种轻量级的…...
[mysql]数据类型精讲
目录 数据类型精讲: 整数类型 浮点类型 日期和时间类型 文本字符串类型 数据类型精讲: 精度问题:不能损失数据 性能问题:表的设计,范式的讲解. 表设计的时候需要设置字段,我们现在要把字段类型讲完.,细节点一点点给大家拆解. Float和double是有精度的损失的,这边推荐使用…...
WordPress AI插件能自动写高质量文章吗,如何用AI提升网站流量
WordPress AI插件能自动写高质量文章吗? 最近很多站长都在问,用wordpress AI插件真的能写出搜索引擎喜欢的好文章吗?作为一个用过10款AI写作工具的老站长,今天我就来分享真实使用体验,告诉你哪些插件好用、怎么用才能…...
【中级软件设计师】函数调用 —— 传值调用和传地址调用 (附软考真题)
【中级软件设计师】函数调用 —— 传值调用和传地址调用 (附软考真题) 目录 【中级软件设计师】函数调用 —— 传值调用和传地址调用 (附软考真题)一、历年真题二、考点:函数调用 —— 传值调用和传地址调用🔺1、传值调用🔺2、传引用(地址)调…...
ECMAScript 1(ES1):JavaScript 的开端
1. 版本背景与发布 ●发布时间:1997 年 6 月,由 ECMA International 正式发布,标准编号为 ECMA-262。 ●历史意义:ES1 是 JavaScript 的首个标准化版本,结束了 Netscape Navigator 与 Internet Explorer 浏览器间脚本语…...
C++入侵检测与网络攻防之暴力破解
目录 1.nessus扫描任务 2.漏洞信息共享平台 3.nessus扫描结果 4.漏扫报告的查看 5.暴力破解以及hydra的使用 6.crunch命令生成字典 7.其他方式获取字典 8.复习 9.关于暴力破解的防御的讨论 10.pam配置的讲解 11.pam弱密码保护 12.pam锁定账户 13.shadow文件的解析 …...
基于ssm的同城上门维修平台管理系统(源码+数据库)
54基于ssm的同城上门维修平台管理系统:前端jsp、jquery、bootstrap,后端 spring、mybatis,集成订单管理、商品管理、商品类型管理、商品浏览、购物车等功能于一体的系统。 ## 功能介绍 ### 用户 - 基本功能:登录、注册、退出、…...
力扣-hot100(和为k的子数组)
560. 和为 K 的子数组 中等 给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。 子数组是数组中元素的连续非空序列。 示例 1: 输入:nums [1,1,1], k 2 输出:2 示例 2: 输入…...
【计算机视觉】CV实战 - 基于YOLOv5的人脸检测与关键点定位系统深度解析
基于YOLOv5的人脸检测与关键点定位系统深度解析 1. 技术背景与项目意义传统方案的局限性YOLOv5多任务方案的优势 2. 核心算法原理网络架构改进关键点回归分支损失函数设计 3. 实战指南:从环境搭建到模型应用环境配置数据准备数据格式要求数据目录结构 模型训练配置文…...
HTML word属性
介绍 CSS word-spacing 属性,用于指定段字之间的空间,例如: p {word-spacing:30px; }word-spacing属性增加或减少字与字之间的空白。 注意: 负值是允许的。 浏览器支持 表格中的数字表示支持该属性的第一个浏览器版本号。 属…...
Java—ThreadLocal底层实现原理
首先,ThreadLocal 本身并不提供存储数据的功能,当我们操作 ThreadLocal 的时候,实际上操作线程对象的一个名为 threadLocals 成员变量。这个成员变量的类型是 ThreadLocal 的一个内部类 ThreadLocalMap,它是真正用来存储数据的容器…...
GTSRB德国交通标志数据集下载以及训练集划分
GTSRB德国交通标志数据集下载以及训练集划分 一、数据集下载二、数据集划分 一、数据集下载 官网地址:附含数据集说明文档点击下载:训练数据集点击下载:测试数据集 二、数据集划分 在模型训练时,将训练数据集分成训练集和验证集&…...
python 实现客户端软件许可证书签名授权 cryptography
目录 1.需求 2.cryptography介绍 3.实际代码 4.结束语 1.需求 采用pyside6开发了一款客户端软件, 为保护核心算法源码, 采用Nuitka打包python代码,这仅仅保护了核心算法代码,不能限制用户使用软件,因此需要软件许可授权签名证书ÿ…...
明远智睿SD2351核心板:以48元撬动AI视觉产业革命的“硬核引擎”
在人工智能浪潮席卷全球的今天,AI视觉作为连接虚拟与现实的“智慧之眼”,正以惊人的速度重塑着产业格局。从智慧城市中的安防监控到自动驾驶汽车的“视觉神经”,从工业产线的缺陷检测到家庭场景的智能管家,AI视觉技术的每一次突破…...
【C语言】全局变量、静态本地变量
在C语言中,变量是存储数据的基本单元。 不同类型的变量有着不同的特性和用途,其中全局变量和本地变量是比较特殊且重要的两类变量。 一、全部变量 1.1 全局变量的作用域和生存期 全局变量是在函数外部定义的变量,其作用域从定义的位置开始&…...
32.768kHz晶振详解:作用、特性及与其他晶振的区别
一、32.768kHz晶振的核心作用 实时时钟(RTC)驱动: 提供精确的1Hz时钟信号,用于计时功能(如电子表、计算机CMOS时钟)。 分频公式: 1Hz 32.768kHz / 2^15(通过15级二分频实现&#x…...
classfinal 修改过源码,支持jdk17 + spring boot 3.2.8
先贴图 使用 classfinal 修改过源码 支持jdk17 spring boot 3.3.0 使用方式: 1、springboot的jar加密 java -jar classfinal-fatjar-1.2.1.jar -file MySpringBoot.jar -libjars my-common.jar -packages cn.com.cmd -pwd 123456 -Y 得到: MySpri…...
算法训练营 Day1
努力追上那个曾经被寄予厚望的自己 —— 25.4.25 一、LeetCode_26 删除有序数组中的重复项 给你⼀个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现⼀次 ,返回删除后数组的 新⻓度。元素的 相对顺序 应该保持 ⼀致 …...
4/25 研0学习日志
Python学习 python 4个常用的数据容器 list dict tuple set list 列表中数据类型可以不一样 构造方式 mylist["xxx","xxxx"] 获取数据方式 mylist[1] mylist[:4] mylist[-1:] 添加数据 mylist.append() mylist.extern(["aaa","aaaa&…...
手机打电话时电脑坐席同时收听对方说话并插入IVR预录声音片段
手机打电话时电脑坐席同时收听对方说话并插入IVR预录声音片段 --本地AI电话机器人 前言 书接上一篇,《手机打电话通话时如何向对方播放录制的IVR引导词声音》中介绍了【蓝牙电话SDK示例App】可以实现手机app在电话通话过程中插播预先录制的开场白等语音片段的功能。…...
汽车零配件供应商如何通过EDI与主机厂生产采购流程结合
当前,全球汽车产业正经历深刻的数字化转型,供应链协同模式迎来全新变革。作为产业链核心环节,汽车零部件供应商与主机厂的高效对接已成为企业发展的战略要务。然而,面对主机厂日益严格的数字化采购要求,许多供应商在ED…...
sql server 开启cdc报事务正在执行
今天开启数据库cdc 功能的时候提示:一个dbrole 的存储过程,rolemember cdc db_ower, ,有事务正在进行,执行失败。 执行多次仍然如此,开启cdc的存储过程是sys.sp_cdc_enable_db;查询了一下网络,给出的方…...
03实战篇Redis02(优惠卷秒杀、分布式锁)
3、优惠卷秒杀 3.1 -全局唯一ID 每个店铺都可以发布优惠券: 当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题: id的规律性太明显 受单表数据量的限制 场景分析&…...