界面样式
音乐播放器实现歌词同步的流程
1.制作当前音乐的歌词文件;
2.通过C++模块实现对当前歌曲的歌词文件读取;
3.实现QML歌词文件的界面展示;
4.在播放歌曲时,QML歌词界面通过c++封装接口实现对当前歌曲位置的读取并更新歌词显示。
QML实现的关键代码
实现歌词的相关文件主要由三个文件构成:PlayWindow.qml,LyricsView.qml,LyricsParser.js.
歌词主界面PlayWindow.qml代码实现
import QtQuick 2.1
import QtQuick.Controls 2.1
import QtQuick.Window 2.1
import "LyricsParser.js" as LyricsParser
Item{
property var name: "示例歌曲"
property var artist: "示例歌手"
property var duration: 269
property var lyrics: "[00:00.00]歌词加载中…"
property var progress: 0
function startPlay() {
if(timer.running){
return
}
btnPlay.clicked()
}
function stopPlay() {
if(!timer.running){
return
}
btnPlay.clicked()
}
function updateCurMusic(msg){
var data = msg
console.log("title is :",String(data["title"]))
console.log("artist is :",String(data["artist"]))
console.log("duration is :",String(data["duration"]))
// console.log("lyrics :",String(data["lyrics"]));
name = String(data["title"])
artist = String(data["artist"])
duration = String(data["duration"])
lyrics = String(data["lyrics"]);
root.updateMusicInfo()
}
function updateProgress(value){
progress = value
}
Rectangle {
id:root
anchors.fill: parent
visible: true
property var currentSong: {
"artist": artist,
"title": name,
"lyrics": lyrics,
"duration": duration
}
function updateMusicInfo(){
musicInfo.formatSongInfo()
lyricsView.updateLyrics()
progressBar.totalDuration()
}
Rectangle {
anchors.fill: parent
gradient: Gradient {
GradientStop { position: 0; color: "#1e62a0" }
GradientStop { position: 1; color: "#1e62a0" }
}
Column {
anchors.fill: parent
spacing: 20
// 歌曲信息区
Item {
id:musicInfo
width: parent.width
height: 100
// 定义格式化函数
function formatSongInfo() {
return `${root.currentSong.title} – ${root.currentSong.artist}`
}
Text {
anchors.centerIn: parent
text: musicInfo.formatSongInfo()
color: "white"
font.pixelSize: 18
}
}
// 歌词显示区
LyricsView {
id: lyricsView
width: parent.width
height: parent.height – 200
function updateLyrics(){
lyricsView.lyrics = LyricsParser.parseLRC(root.currentSong.lyrics)
}
lyrics: root.currentSong.lyrics
}
// 进度控制区
Slider {
id: progressBar
width: parent.width – 40
anchors.horizontalCenter: parent.horizontalCenter
from: 0
to: totalDuration()
value: 0
visible: false
function totalDuration(){
console.log("duration :",root.currentSong.duration)
return root.currentSong.duration
}
onMoved: {
console.log("progressBar value:",value*1000)
lyricsView.updatePosition(value*1000)
}
}
// 控制按钮区
Row {
visible: false
anchors.horizontalCenter: parent.horizontalCenter
spacing: 30
Button {
icon.source: "qrc:/icons/prev.png"
flat: true
}
Button {
id:btnPlay
icon.source: "qrc:/icons/play.png"
flat: true
onClicked: {
timer.running = !timer.running
}
}
Button {
icon.source: "qrc:/icons/next.png"
flat: true
}
}
}
}
Timer {
id: timer
interval: 1000
running: false
repeat: true
function customRound(val) {
return val > 0 ? Math.floor(val) : Math.ceil(val)
}
onTriggered: {
progressBar.value = progress/1000.0//秒
console.log("value:",progressBar.value)
//获取当前播放的进度
lyricsView.updatePosition(progress)//毫秒
if(progress >= progressBar.to){
progressBar.value = 0
}
}
}
}
}
歌词显示LyricsView.qml代码实现
import QtQuick 2.1
import QtQuick.Controls 2.1
Item {
id: root
property var lyrics: []
property int currentIndex: -1
property alias contentY: flick.contentY
Flickable {
id: flick
anchors.fill: parent
contentWidth: width
contentHeight: col.height
clip: true
Column {
id: col
width: parent.width
spacing: 15
Repeater {
model: root.lyrics
delegate: Text {
width: col.width
horizontalAlignment: Text.AlignHCenter
text: modelData.text
color: index === root.currentIndex ? "#1DB954" : "white"
font {
pixelSize: index === root.currentIndex ? 24 : 18
bold: index === root.currentIndex
}
opacity: index === root.currentIndex ? 1 : 0.7
Behavior on color { ColorAnimation { duration: 200 } }
Behavior on font.pixelSize { NumberAnimation { duration: 200 } }
}
}
}
}
//更新歌词位置
function updatePosition(time) {
for (var i = 0; i < lyrics.length; i++) {
if (lyrics[i].time <= time &&
(i === lyrics.length – 1 || lyrics[i+1].time > time)) {
currentIndex = i
positionView()
break
}
}
}
//确保当前播放歌词显示到中间位置
function positionView() {
if (currentIndex < 0) return
var targetY = currentIndex * 45 – height/2
flick.contentY = Math.max(0, Math.min(targetY, flick.contentHeight – height))
}
}
歌词解析LyricsParser.js代码实现
function parseLRC(text) {
const lines = text.split('\\n')
const result = []
const timeRegex = /\\[(\\d+):(\\d+)\\.(\\d+)\\]/
lines.forEach(line => {
const match = timeRegex.exec(line)
if (match) {
const min = parseInt(match[1])
const sec = parseInt(match[2])
const ms = parseInt(match[3])
const time = min * 60000 + sec * 1000 + ms * 10
const text = line.replace(timeRegex, '').trim()
if (text) result.push({ time, text })
}
})
return result.sort((a, b) => a.time – b.time)
}
音乐播放器主界面关键实现
import QtQuick 2.1
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.1
import QtQuick.Dialogs 1.3
import MediaModel 1.0
import QtQuick.Window 2.1
import "helper.js" as Helper
ApplicationWindow {
id: root
width: 860
height: 600
visible: true
minimumHeight: 600
minimumWidth: 800
title: qsTr("音乐播放器")
onWidthChanged: console.log(width)
flags: Qt.FramelessWindowHint
gradient: Gradient {
GradientStop { position: 0; color: "#70dfe8" }
GradientStop { position: 1; color: "#1e62a0" }
}
}
menuBar: AppWindowButtons { }
property var lyricsWidget: null
Item {
Component.onCompleted: {
// 将组件注册为全局属性
Qt._musicPropertyDlg = Qt.createComponent("SetMetaInfo.qml")
if(Qt._musicPropertyDlg.status === Component.Ready){
}else{
console.error("组件SetMetaInfo.qml加载失败:", Qt._musicPropertyDlg.errorString())
}
}
Component.onDestruction: {
Qt._musicPropertyDlg.destroy()
}
}
property int previousX
property int previousY
property point dragPos: Qt.point(0,0)
ErrorDialog {
id: errorDialog
}
//自定义C++模块
MediaModel {
id: dataModel
onErrorMessage: {
errorDialog.message = message
errorDialog.visible = true
}
}
/*!
\\brief Left panel
*/
LeftPanel {
id: leftPanel
width: 100
height: parent.height
anchors.left: parent.left
topPadding: 20
}
/*!
\\brief Debug draw rect
*/
Rectangle {
height: parent.height – 50
width: parent.width – leftPanel.width – 5
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 5
radius: 20
// color: "#f4f4fe"
color: "transparent"
border {
color: "transparent"
width: 3
}
ColumnLayout {
width: parent.width
height: parent.height
anchors.fill: parent
Rectangle {
id:viewZone
Layout.fillWidth: true
Layout.preferredHeight: parent.height / 5*4
Layout.margins: 2
visible: !loader.active
color: "transparent"
property bool showTableView: true
Rectangle{
anchors.fill: parent
anchors.margins: 2
color: "transparent"
//播放列表
TableWidget {
id: tableView
anchors.fill: parent
anchors.margins: 2
model: dataModel
visible: viewZone.showTableView
}
…
//歌词显示界面
PlayerWindow{
id: playerWindow
anchors.fill: parent
anchors.margins: 2
visible: !viewZone.showTableView
opacity: 1.0
onVisibleChanged: {
if(visible === true){
playerWindow.updateCurMusic(dataModel.metaData)
playerWindow.startPlay()
lyricsTimer.start()
}else{
playerWindow.stopPlay()
lyricsTimer.stop()
}
}
Timer{
id:lyricsTimer
interval: 1000
running: false
repeat: true
onTriggered: {
var value = dataModel.position
playerWindow.updateProgress(value)
}
}
}
…
}
Button {
id:shiftBtn
text: "切换视图"
onClicked: viewZone.showTableView = !viewZone.showTableView
visible: false
}
}
Loader {
id: loader
Layout.fillWidth: true
Layout.fillHeight: true
active: false
}
/*!
\\brief Debug draw rect
*/
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: parent.height / 5
Layout.alignment: Qt.AlignBottom
color: "plum"
opacity: 0.8
visible: !loader.active
border {
color: "transparent"
width: 3
}
ColumnLayout {
width: parent.width
height: parent.height
/*!
\\brief Debug draw rect
*/
Rectangle {
Layout.alignment: Qt.AlignCenter
Layout.preferredWidth: parent.width * 0.75
Layout.preferredHeight: 50
border {
color: "transparent"
width: 3
}
RowLayout {
width: parent.width
height: parent.height
Label {
Layout.alignment: Qt.AlignCenter
Layout.leftMargin: 5
font.pixelSize: 16
text: Helper.prependZero(Math.floor(dataModel.position / 1000 / 60)) + ":"
+ Helper.prependZero(Math.floor(dataModel.position / 1000 % 60))
}
CustomSlider {
id: seekSlider
Layout.preferredWidth: parent.width * 0.75
Layout.alignment: Qt.AlignCenter
}
Label {
Layout.alignment: Qt.AlignCenter
Layout.rightMargin: 5
font.pixelSize: 16
text: Helper.prependZero(Math.floor(dataModel.duration / 1000 / 60)) + ":"
+ Helper.prependZero(Math.floor(dataModel.duration / 1000 % 60))
}
}
}
/*!
\\brief Bottom panel
*/
ControlPanel {
Layout.alignment: Qt.AlignCenter
Layout.bottomMargin: 10
}
}
}
}
}
}
C++实现的关键代码
在QML文件中,通过Q_PROPERTY属性,读取歌曲的属性信息(包含歌词信息)
/*
* 获取文件属性信息
*/
Q_PROPERTY(QVariantMap metaData READ metaData WRITE setMetaData )
QVariantMap MediaModel::metaData()
{
QVariantMap mData;
QString filePath = m_playlist->currentMedia().canonicalUrl().toLocalFile();
TagLib::FileRef f(filePath.toStdString().data());
if(f.isNull()){
qDebug() << "f is null";
return mData;
}
mData.insert("artist", QVariant::fromValue(TStringToQString(f.tag()->artist())));
mData.insert("title", QVariant::fromValue(TStringToQString(f.tag()->title())));
mData.insert("time", getMetaData(f).value("length"));
mData.insert("duration",getMetaData(f).value("duration"));
mData.insert("album", QVariant::fromValue(TStringToQString(f.tag()->album())));
//设置默认歌词文件
mData.insert("lyrics",QString(""));
{
int pos = filePath.indexOf('.');
QString lyricsFile = filePath.mid(0,pos)+".lyric";
QFile f(lyricsFile);
QByteArray lyricsContent;
if(f.exists()&&f.open(QFile::ReadOnly)){
lyricsContent = f.readAll();
mData["lyrics"] = lyricsContent;
f.close();
}else{
qDebug() << "lyricsFile path:" << lyricsFile;
}
}
// for(auto elem:mData.keys()){
// qDebug() << elem <<" :" << mData[elem] << "==" << filePath.toStdString().data();
// }
return mData;
}
需要注意的问题
QML文件中anchors与Layout两种布局比较
一、定位机制差异
- 锚点系统通过元素间的相对关系实现精确定位(如anchors.left: parent.right),需手动计算比例且依赖父容器尺寸。
- 布局系统(如RowLayout/ColumnLayout)采用流式自动排列,内置权重分配机制(Layout.fillWidth等),自动响应容器变化。
二、混合使用建议
- 对等间距排列(如按钮组)、表单输入等场景,使用RowLayout/ColumnLayout简化开发。
- 通过Layout.alignment控制子项对齐,避免直接绑定x/y坐标。
- 在布局容器内用锚点微调子项位置(如Text在Rectangle中居中)。
- 叠加层或绝对定位元素使用anchors.fill或anchors.centerIn。
三、注意事项
- 禁止混用定位属性:避免同时设置Layout和anchors,否则导致布局冲突。
- 性能优化:锚点系统在简单场景性能更优,复杂嵌套布局时推荐GridLayout等容器。
- 响应式设计:布局系统天然适配不同屏幕尺寸,锚点系统需显式绑定width/height信号。
关键结论:布局系统适合结构性界面,锚点系统擅长精准定位;实际开发中常组合使用,但需避免属性冲突。
评论前必须登录!
注册