通过这个项目,你将学会如何利用Qt强大的QMediaPlayer和QVideoWidget等核心组件,从零开始构建一个功能完整的视频播放器。不仅会实现播放、暂停、进度控制等基础功能,还会深入探讨如何优化播放性能、支持多格式解码,甚至添加字幕、音量调节和全屏播放等进阶特性。
一:运行效果
二:工程项目结构
三:工程项目源码文件
1:CMakeList.txt文件
cmake_minimum_required(VERSION 3.16)
project(MediaPlayerApp LANGUAGES CXX)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
if(NOT DEFINED INSTALL_EXAMPLESDIR)
set(INSTALL_EXAMPLESDIR "examples")
endif()
set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/demos/mediaplayer")
find_package(Qt6 6.5 REQUIRED COMPONENTS Core Quick QuickControls2 Svg)
qt_standard_project_setup(REQUIRES 6.5)
qt_add_executable(MediaPlayerApp
main.cpp
)
set(qml_files
"Main.qml"
"MetadataInfo.qml"
"PlayerMenuBar.qml"
"TracksInfo.qml"
"TracksOptions.qml"
"PlaylistInfo.qml"
"UrlPopup.qml"
"SettingsInfo.qml"
"ThemeInfo.qml"
"ErrorPopup.qml"
"TouchMenu.qml"
)
qt_add_qml_module(MediaPlayerApp
URI MediaPlayerModule
QML_FILES ${qml_files})
add_subdirectory(MediaControls)
add_subdirectory(Config)
target_link_libraries(MediaPlayerApp PRIVATE
Qt6::Core
Qt6::Svg
Qt6::Quick
MediaControlsplugin
Configplugin
)
install(TARGETS MediaPlayerApp
RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
)
2:MediaPlayerApp(ErrorPopup.qml文件)
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
import Config
Popup {
id: errorPopup
anchors.centerIn: Overlay.overlay
padding: 30
property alias errorMsg: label.text
onOpened: closeTimer.restart()
background: Rectangle {
color: "transparent"
}
Column {
spacing: 15
Image {
source: Config.iconSource("Error", false)
anchors.horizontalCenter: parent.horizontalCenter
}
Label {
id: label
color: "#FFE353"
font.pixelSize: 24
}
}
Timer {
id: closeTimer
interval: 5000
onTriggered: errorPopup.close()
}
}
3:MediaPlayerApp(MetadataInfo.qml文件)
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Fusion
import Config
Item {
id: root
function clear() {
elements.clear()
}
function read(metadata) {
if (metadata) {
for (var key of metadata.keys()) {
if (metadata.stringValue(key)) {
elements.append({
name: metadata.metaDataKeyToString(key),
value: metadata.stringValue(key)
})
}
}
}
}
ListModel {
id: elements
}
Item {
anchors.fill: parent
anchors.margins: 15
ListView {
id: metadataList
anchors.fill: parent
model: elements
delegate: RowLayout {
id: row
width: metadataList.width
required property string name
required property string value
Label {
text: row.name + ":"
font.pixelSize: 16
color: Config.secondaryColor
Layout.preferredWidth: root.width / 2
}
Label {
text: row.value
font.pixelSize: 16
wrapMode: Text.WrapAnywhere
color: Config.secondaryColor
Layout.fillWidth: true
}
}
}
Label {
visible: !elements.count
font.pixelSize: 16
text: qsTr("No metadata present")
anchors.centerIn: parent
color: Config.secondaryColor
}
}
}
4:MediaPlayerApp(PlayerMenuBar.qml文件)
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Dialogs
import Config
Item {
id: root
implicitHeight: menuBar.height
signal fileOpened(path: url)
property alias openFileMenu: fileDialog
property alias openUrlPopup: urlPopup
FileDialog {
id: fileDialog
title: qsTr("Please choose a file")
onAccepted: root.fileOpened(fileDialog.selectedFile)
}
UrlPopup {
id: urlPopup
onPathChanged: root.fileOpened(urlPopup.path)
}
MenuBar {
id: menuBar
visible: !Config.isMobileTarget
anchors.left: root.left
leftPadding: 10
topPadding: 10
palette.base: Config.mainColor
palette.text: Config.secondaryColor
palette.highlightedText: "#41CD52"
palette.window: "transparent"
palette.highlight: Config.mainColor
Menu {
title: qsTr("&File")
palette.text: Config.secondaryColor
palette.window: Config.mainColor
palette.highlightedText: "#41CD52"
MenuItem {
text: qsTr("Open &File")
onTriggered: fileDialog.open()
}
MenuItem {
text: qsTr("Open &URL")
onTriggered: urlPopup.open()
}
}
}
}
5:MediaPlayerApp(PlaylistInfo.qml文件)
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Dialogs
import QtQuick.Layouts
import QtCore
import MediaControls
import Config
Rectangle {
id: root
implicitWidth: 380
color: Config.mainColor
border.color: "lightgrey"
radius: 10
property int currentIndex: -1
property bool isShuffled: false
property alias mediaCount: files.count
signal playlistUpdated()
signal currentFileRemoved()
function getSource() {
if (isShuffled) {
let randomIndex = Math.floor(Math.random() * mediaCount)
while (randomIndex == currentIndex) {
randomIndex = Math.floor(Math.random() * mediaCount)
}
currentIndex = randomIndex
}
return files.get(currentIndex).path
}
function addFiles(index, selectedFiles) {
selectedFiles.forEach(function (file){
const url = new URL(file)
files.insert(index,
{
path: url,
isMovie: isMovie(url.toString())
})
})
playlistUpdated()
}
function addFile(index, selectedFile) {
if (index > mediaCount || index < 0) {
index = 0
currentIndex = 0
}
files.insert(index,
{
path: selectedFile,
isMovie: isMovie(selectedFile.toString())
})
}
function isMovie(path) {
const paths = path.split('.')
const extension = paths[paths.length - 1]
const musicFormats = ["mp3", "wav", "aac"]
for (const format of musicFormats) {
if (format === extension) {
return false
}
}
return true
}
MouseArea {
anchors.fill: root
preventStealing: true
}
FileDialog {
id: folderView
title: qsTr("Add files to playlist")
currentFolder: StandardPaths.standardLocations(StandardPaths.MoviesLocation)[0]
fileMode: FileDialog.OpenFiles
onAccepted: {
root.addFiles(files.count, folderView.selectedFiles)
close()
}
}
ListModel {
id: files
}
Item {
id: playlist
anchors.fill: root
anchors.margins: 30
RowLayout {
id: header
width: playlist.width
Label {
font.bold: true
font.pixelSize: 20
text: qsTr("Playlist")
color: Config.secondaryColor
Layout.fillWidth: true
}
CustomButton {
icon.source: Config.iconSource("Add_file")
onClicked: folderView.open()
}
}
ListView {
id: listView
model: files
anchors.fill: playlist
anchors.topMargin: header.height + 30
spacing: 20
delegate: RowLayout {
id: row
width: listView.width
spacing: 15
required property string path
required property int index
required property bool isMovie
Image {
id: mediaIcon
states: [
State {
name: "activeMovie"
when: root.currentIndex === row.index && row.isMovie
PropertyChanges {
mediaIcon.source: Config.iconSource("Movie_Active", false)
}
},
State {
name: "inactiveMovie"
when: root.currentIndex !== row.index && row.isMovie
PropertyChanges {
mediaIcon.source: Config.iconSource("Movie_Icon")
}
},
State {
name: "activeMusic"
when: root.currentIndex === row.index && !row.isMovie
PropertyChanges {
mediaIcon.source: Config.iconSource("Music_Active", false)
}
},
State {
name: "inactiveMusic"
when: root.currentIndex !== row.index && !row.isMovie
PropertyChanges {
mediaIcon.source: Config.iconSource("Music_Icon")
}
}
]
}
Label {
Layout.fillWidth: true
elide: Text.ElideRight
font.bold: root.currentIndex === row.index
color: root.currentIndex === row.index ? "#41CD52" : Config.secondaryColor
font.pixelSize: 18
text: {
const paths = row.path.split('/')
return paths[paths.length - 1]
}
}
CustomButton {
icon.source: Config.iconSource("Trash_Icon")
onClicked: {
const removedIndex = row.index
files.remove(row.index)
if (root.currentIndex === removedIndex) {
root.currentFileRemoved()
} else if (root.currentIndex > removedIndex) {
--root.currentIndex
}
}
}
}
remove: Transition {
NumberAnimation {
property: "opacity"
from: 1.0
to: 0.0
duration: 400
}
}
add: Transition {
NumberAnimation {
property: "opacity"
from: 0.0
to: 1.0
duration: 400
}
NumberAnimation {
property: "scale"
from: 0.5
to: 1.0
duration: 400
}
}
displaced: Transition {
NumberAnimation {
properties: "y"
duration: 600
easing.type: Easing.OutBounce
}
}
}
}
}
6:MediaPlayerApp(SettingsInfo.qml.qml文件)
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Fusion
import QtMultimedia
import Config
Rectangle {
id: root
implicitWidth: 380
color: Config.mainColor
border.color: "lightgrey"
radius: 10
property alias tracksInfo: tracksInfo
property alias metadataInfo: metadataInfo
required property MediaPlayer mediaPlayer
required property int selectedAudioTrack
required property int selectedVideoTrack
required property int selectedSubtitleTrack
MouseArea {
anchors.fill: root
preventStealing: true
}
TabBar {
id: bar
width: root.width
contentHeight: 60
Repeater {
model: [qsTr("Metadata"), qsTr("Tracks"), qsTr("Theme")]
TabButton {
id: tab
required property int index
required property string modelData
property color shadowColor: bar.currentIndex === index ? "#41CD52" : "black"
property color textColor: bar.currentIndex === index ? "#41CD52" : Config.secondaryColor
background: Rectangle {
opacity: 0.15
gradient: Gradient {
GradientStop { position: 0.0; color: "transparent" }
GradientStop { position: 0.5; color: "transparent" }
GradientStop { position: 1.0; color: tab.shadowColor }
}
}
contentItem: Label {
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
text: tab.modelData
font.pixelSize: 20
color: tab.textColor
}
}
}
}
StackLayout {
width: root.width
anchors.top: bar.bottom
anchors.bottom: root.bottom
currentIndex: bar.currentIndex
MetadataInfo { id: metadataInfo }
TracksInfo {
id: tracksInfo
mediaPlayer: root.mediaPlayer
selectedAudioTrack: root.selectedAudioTrack
selectedVideoTrack: root.selectedVideoTrack
selectedSubtitleTrack: root.selectedSubtitleTrack
}
ThemeInfo { id: themeInfo }
}
}
7:MediaPlayerApp(ThemeInfo.qml文件)
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls.Fusion
import MediaControls
import Config
Item {
id: root
Item {
anchors.fill: parent
Column {
padding: 15
spacing: 20
ButtonGroup { id: group }
CustomRadioButton {
checked: Config.Theme.Light === Config.activeTheme
text: qsTr("Light theme")
ButtonGroup.group: group
onClicked: Config.activeTheme = Config.Theme.Light
}
CustomRadioButton {
checked: Config.Theme.Dark === Config.activeTheme
text: qsTr("Dark theme")
ButtonGroup.group: group
onClicked: Config.activeTheme = Config.Theme.Dark
}
}
}
}
8:MediaPlayerApp(TouchMenu.qml文件)
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Layouts
import Config
Menu {
id: menuPopup
padding: 0
verticalPadding: 15
property alias openUrlMenuItem: openUrlMenuItem
property alias openFileMenuItem: openFileMenuItem
background: Rectangle {
color: Config.mainColor
radius: 15
border.color: "#41CD52"
}
component MenuItemLabel: Label {
font.pixelSize: 24
color: "#41CD52"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
topPadding: 4
bottomPadding: 4
}
component CustomMenuItem: MenuItem {
id: menuItem
property bool bold
text: qsTr("File")
contentItem: MenuItemLabel {
text: menuItem.text
font.bold: menuItem.bold
}
background: Rectangle {
color: menuItem.pressed ? "#41CD52" : "transparent"
opacity: 0.25
}
}
MenuItemLabel {
text: qsTr("Load media file from:")
color: Config.secondaryColor
bottomPadding: 12
width: parent.width
}
Rectangle {
width: parent.width
implicitHeight: 1
color: "#41CD52"
opacity: 0.25
}
CustomMenuItem {
id: openFileMenuItem
text: qsTr("File")
bold: true
}
Rectangle {
width: parent.width
implicitHeight: 1
color: "#41CD52"
opacity: 0.25
}
CustomMenuItem {
id: openUrlMenuItem
text: qsTr("URL")
bold: true
}
Rectangle {
implicitHeight: 1
color: "#41CD52"
Layout.fillWidth: true
}
CustomMenuItem {
id: cancelButtonBackground
text: qsTr("Cancel")
}
}
9:MediaPlayerApp(TracksInfo.qml文件)
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtMultimedia
Item {
id: root
required property int selectedAudioTrack
required property int selectedVideoTrack
required property int selectedSubtitleTrack
required property MediaPlayer mediaPlayer
Flickable {
anchors.fill: parent
contentWidth: column.implicitWidth
contentHeight: column.implicitHeight
boundsBehavior: Flickable.DragAndOvershootBounds
flickableDirection: Flickable.VerticalFlick
clip: true
Column {
id: column
anchors.fill: parent
clip: true
padding: 15
spacing: 20
TracksOptions {
id: audioTracks
headerText: qsTr("Audio Tracks")
selectedTrack: root.selectedVideoTrack
metaData: root.mediaPlayer.audioTracks
onSelectedTrackChanged: root.mediaPlayer.activeAudioTrack = audioTracks.selectedTrack
}
TracksOptions {
id: videoTracks
headerText: qsTr("Video Tracks")
selectedTrack: root.selectedVideoTrack
metaData: root.mediaPlayer.videoTracks
onSelectedTrackChanged: root.mediaPlayer.activeVideoTrack = videoTracks.selectedTrack
}
TracksOptions {
id: subtitleTracks
headerText: qsTr("Subtitle Tracks")
selectedTrack: root.selectedSubtitleTrack
metaData: root.mediaPlayer.subtitleTracks
onSelectedTrackChanged: root.mediaPlayer.activeSubtitleTrack = subtitleTracks.selectedTrack
}
}
}
}
10:MediaPlayerApp(TracksOptions.qml文件)
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls.Fusion
import QtMultimedia
import MediaControls
import Config
Item {
id: root
implicitWidth: 380
implicitHeight: elements.count ? (elements.count + 1) * 40 : 20
required property int selectedTrack
required property list<mediaMetaData> metaData
property string headerText: ""
function readTracks(metadataList : list<mediaMetaData>) {
const LanguageKey = 6
elements.clear()
if (!metadataList || !metadataList.length)
return
elements.append({
language: "No Track",
trackNumber: -1
})
metadataList.forEach(function (metadata, index) {
const language = metadata.stringValue(LanguageKey)
const label = language ? language : "Track " + (index + 1)
elements.append({
language: label,
trackNumber: index
})
});
}
ListModel { id: elements }
ButtonGroup { id: group }
Column {
spacing: 16
anchors.fill: root
Label {
id: header
text: elements.count ? qsTr(root.headerText) : qsTr("No " + root.headerText + " present")
font.pixelSize: 18
font.bold: true
color: Config.secondaryColor
}
ListView {
id: trackList
model: elements
spacing: 18
height: root.implicitHeight - header.height
width: parent.width
clip: true
delegate: CustomRadioButton {
checked: trackNumber === root.selectedTrack
text: language
required property int trackNumber
required property string language
ButtonGroup.group: group
onClicked: root.selectedTrack = trackNumber
}
}
}
onMetaDataChanged: readTracks(root.metaData)
}
11:MediaPlayerApp(UrlPopup.qml文件)
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Layouts
import MediaControls
import Config
Popup {
id: urlPopup
anchors.centerIn: Overlay.overlay
padding: 30
width: 500
height: column.height + 60
property url path: ""
readonly property color borderColor: urlText.text ? (!errorMsg.visible ? "#41CD52" : "red") : Config.secondaryColor
background: Rectangle {
color: Config.mainColor
opacity: 0.9
radius: 15
border.color: "grey"
}
function setUrl(urlPath: url) {
path = urlPath
urlPopup.close()
}
function validateUrl(urlText: string) {
const urlPattern = /^((http)|(https)|(rtp)|(rtsp)|(udp)):\/\//
return urlPattern.test(urlText)
}
Column {
id: column
spacing: 20
Label {
text: qsTr("Load from URL")
font.pixelSize: 18
anchors.horizontalCenter: parent.horizontalCenter
color: Config.secondaryColor
}
ColumnLayout {
spacing: 0
TextField {
id: urlText
leftPadding: 15
verticalAlignment: TextInput.AlignVCenter
font.pixelSize: 16
placeholderText: qsTr("URL:")
placeholderTextColor: Config.secondaryColor
color: Config.secondaryColor
text: "https://download.qt.io/learning/videos/media-player-example/Qt_LogoMergeEffect.mp4"
Layout.preferredHeight: 40
Layout.preferredWidth: 440
background: Rectangle {
color: Config.mainColor
border.color: urlPopup.borderColor
}
}
Rectangle {
id: errorMsg
visible: false
color: "#FF3A3A"
Layout.minimumHeight: 40
Layout.minimumWidth: 130
Layout.alignment: Qt.AlignLeft
Row {
anchors.centerIn: parent
spacing: 10
Image {
source: Config.iconSource("Warning_Icon", false)
}
Label {
text: qsTr("Wrong URL")
font.pixelSize: 16
color: "white"
}
}
onVisibleChanged: showError.start()
NumberAnimation {
id: showError
target: errorMsg
properties: "opacity"
from: 0
to: 1
duration: 1000
}
}
}
RowLayout {
spacing: 20
anchors.horizontalCenter: parent.horizontalCenter
CustomButton {
icon.source: Config.iconSource("Cancel_Button", false)
onClicked: {
urlText.text = ""
urlPopup.close()
}
}
CustomButton {
icon.source: Config.iconSource("Load_Button", false)
enabled: urlText.text
opacity: urlText.text ? 1 : 0.5
onClicked: {
if (urlPopup.validateUrl(urlText.text)) {
urlPopup.setUrl(new URL(urlText.text))
} else {
errorMsg.visible = true
}
}
}
}
}
onOpened: urlPopup.forceActiveFocus()
onClosed: {
urlText.text = ""
errorMsg.visible = false
}
}
12:Config(CMakeLists.txt文件)
qt_add_library(Config STATIC)
set_source_files_properties(Config.qml
PROPERTIES
QT_QML_SINGLETON_TYPE true
)
qt_add_qml_module(Config
URI "Config"
OUTPUT_DIRECTORY Config
QML_FILES
"Config.qml"
)
13:Config(Config.qml文件)
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma Singleton
import QtQuick
QtObject {
enum Theme {
Light,
Dark
}
property int activeTheme : Config.Theme.Dark
readonly property bool isMobileTarget : Qt.platform.os === "android" || Qt.platform.os === "ios"
readonly property color mainColor : activeTheme ? "#09102B" : "#FFFFFF"
readonly property color secondaryColor : activeTheme ? "#FFFFFF" : "#09102B"
function iconSource(fileName, addSuffix = true) {
return `qrc:/qt/qml/MediaControls/icons/${fileName}${activeTheme === Config.Theme.Dark && addSuffix ? "_Dark.svg" : ".svg"}`
}
}
14:MediaControls(CMakeLists.txt文件)
qt_add_library(MediaControls STATIC)
target_link_libraries(MediaControls PRIVATE Qt6::Quick)
qt_add_qml_module(MediaControls
URI "MediaControls"
OUTPUT_DIRECTORY MediaControls
QML_FILES
"AudioControl.qml"
"PlaybackSeekControl.qml"
"PlaybackRateControl.qml"
"PlaybackControl.qml"
"CustomSlider.qml"
"CustomButton.qml"
"CustomRadioButton.qml"
RESOURCES
"../icons/Rate_Icon.svg"
"../icons/Rate_Icon_Dark.svg"
"../icons/Loop_Icon.svg"
"../icons/Loop_Icon_Dark.svg"
"../icons/Play_Icon.svg"
"../icons/Previous_Icon.svg"
"../icons/Previous_Icon_Dark.svg"
"../icons/Next_Icon.svg"
"../icons/Next_Icon_Dark.svg"
"../icons/Shuffle_Icon.svg"
"../icons/Volume_Icon.svg"
"../icons/Volume_Icon_Dark.svg"
"../icons/Playlist_Icon.svg"
"../icons/Playlist_Icon_Dark.svg"
"../icons/Settings_Icon.svg"
"../icons/Settings_Icon_Dark.svg"
"../icons/FullScreen_Icon.svg"
"../icons/FullScreen_Icon_Dark.svg"
"../icons/Stop_Icon.svg"
"../icons/Loop_Playlist.svg"
"../icons/Single_Loop.svg"
"../icons/Playlist_Active.svg"
"../icons/Add_file.svg"
"../icons/Add_file_Dark.svg"
"../icons/Movie_Icon.svg"
"../icons/Movie_Icon_Dark.svg"
"../icons/Trash_Icon.svg"
"../icons/Trash_Icon_Dark.svg"
"../icons/Music_Icon.svg"
"../icons/Music_Icon_Dark.svg"
"../icons/Music_Active.svg"
"../icons/Movie_Active.svg"
"../icons/Shuffle_Icon_Dark.svg"
"../icons/Shuffle_Active.svg"
"../icons/Default_CoverArt.svg"
"../icons/Cancel_Button.svg"
"../icons/Load_Button.svg"
"../icons/Shadow.png"
"../icons/Warning_Icon.svg"
"../icons/Menu_Icon.svg"
"../icons/Menu_Icon_Dark.svg"
"../icons/Mute_Icon.svg"
"../icons/Mute_Icon_Dark.svg"
"../icons/Error.svg"
)
15:MediaControls(AudioControl.qml文件)
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Layouts
import Config
Item {
id: root
property real volume: volumeSlider.value / 100
Layout.minimumWidth: 100
Layout.maximumWidth: 200
RowLayout {
anchors.fill: root
spacing: 10
Image {
source: Config.iconSource("Mute_Icon")
}
CustomSlider {
id: volumeSlider
enabled: true
to: 100.0
value: 100.0
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
Image {
source: Config.iconSource("Volume_Icon")
}
}
}
16:MediaControls(CustomButton.qml文件)
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Effects
Button {
id: control
flat: true
contentItem: Image {
id: image
source: control.icon.source
}
background: MultiEffect {
source: image
anchors.fill: control
visible: control.down
opacity: 0.5
shadowEnabled: true
blurEnabled: true
blur: 0.5
}
}
17:MediaControls(CustomRadioButton.qml)
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
import Config
RadioButton {
id: control
height: 20
indicator: Rectangle {
width: 20
height: 20
radius: 10
x: control.leftPadding
y: control.height / 2 - height / 2
color: "transparent"
border.color: Config.secondaryColor
border.width: 2
Rectangle {
width: 10
height: 10
radius: 5
color: Config.secondaryColor
anchors.centerIn: parent
visible: control.checked
}
}
contentItem: Label {
text: control.text
font.pixelSize: 18
verticalAlignment: Text.AlignVCenter
leftPadding: control.indicator.width + control.spacing
color: Config.secondaryColor
}
}
18:MediaControls(CustomSlider.qml文件)
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
Slider {
id: slider
property alias backgroundColor: backgroundRec.color
property alias backgroundOpacity: backgroundRec.opacity
background: Rectangle {
id: backgroundRec
x: slider.leftPadding
y: slider.topPadding + slider.availableHeight / 2 - height / 2
implicitWidth: 120
implicitHeight: 8
width: slider.availableWidth
height: implicitHeight
radius: 10
color: "#41CD52"
opacity: 0.2
border.color: "#41CD52"
border.width: 1
}
handle: Rectangle {
x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width)
y: slider.topPadding + slider.availableHeight / 2 - height / 2
implicitWidth: 8
implicitHeight: 8
color: "transparent"
}
Rectangle {
width: slider.visualPosition * slider.availableWidth
x: slider.leftPadding
y: slider.topPadding + slider.availableHeight / 2 - height / 2
height: 8
color: "#41CD52"
radius: 10
}
}
19:MediaControls(PlaybackControl.qml文件)
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Layouts
import QtMultimedia
import Config
Item {
id: root
implicitHeight: 100
required property MediaPlayer mediaPlayer
readonly property int mediaPlayerState: root.mediaPlayer.playbackState
property bool isPlaylistShuffled: false
property bool isPlaylistLooped: false
property bool isPlaylistVisible: false
property url playlistIcon: !root.isPlaylistVisible ? Config.iconSource("Playlist_Icon") : Config.iconSource("Playlist_Active", false)
property url shuffleIcon: !root.isPlaylistShuffled ? Config.iconSource("Shuffle_Icon") : Config.iconSource("Shuffle_Active", false)
property alias volume: audio.volume
property alias playbackRate: rate.playbackRate
property alias playlistButton: playlistButton
property alias menuButton: menuButton
signal playNextFile()
signal playPreviousFile()
function changeLoopMode() {
if (root.mediaPlayer.loops === 1 && !root.isPlaylistLooped) {
root.mediaPlayer.loops = MediaPlayer.Infinite
} else if (root.mediaPlayer.loops === MediaPlayer.Infinite) {
root.mediaPlayer.loops = 1
root.isPlaylistLooped = true
} else {
root.mediaPlayer.loops = 1
root.isPlaylistLooped = false
}
}
Item {
anchors.fill: root
RowLayout {
id: playerButtons
anchors.fill: parent
Item {
CustomButton {
id: menuButton
icon.source: Config.iconSource("Menu_Icon")
visible: Config.isMobileTarget
anchors.centerIn: parent
}
Layout.fillWidth: true
Layout.minimumWidth: 40
Layout.maximumWidth: 95
}
PlaybackRateControl {
id: rate
Layout.fillHeight: true
Layout.fillWidth: true
}
Item {
Layout.fillWidth: true
}
RowLayout {
id: controlButtons
spacing: Screen.primaryOrientation === Qt.LandscapeOrientation ? 25 : 1
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
CustomButton {
id: shuffleButton
icon.source: root.shuffleIcon
visible: Screen.primaryOrientation === Qt.LandscapeOrientation
onClicked: root.isPlaylistShuffled = !root.isPlaylistShuffled
}
CustomButton {
id: previousButton
icon.source: Config.iconSource("Previous_Icon")
onClicked: root.playPreviousFile()
}
CustomButton {
id: playButton
icon.source: Config.iconSource("Play_Icon", false)
onClicked: root.mediaPlayer.play()
}
CustomButton {
id: pausedButton
icon.source: Config.iconSource("Stop_Icon", false)
onClicked: root.mediaPlayer.pause()
}
CustomButton {
id: nextButton
icon.source: Config.iconSource("Next_Icon")
onClicked: root.playNextFile()
}
CustomButton {
id: loopButton
icon.source: Config.iconSource("Loop_Icon")
visible: Screen.primaryOrientation === Qt.LandscapeOrientation
onClicked: root.changeLoopMode()
states: [
State {
name: "noActiveLooping"
when: root.mediaPlayer.loops === 1 && !root.isPlaylistLooped
PropertyChanges {
loopButton.icon.source: Config.iconSource("Loop_Icon")
}
},
State {
name: "singleLoop"
when: root.mediaPlayer.loops === MediaPlayer.Infinite
PropertyChanges {
loopButton.icon.source: Config.iconSource("Single_Loop", false)
}
},
State {
name: "playlistLoop"
when: root.isPlaylistLooped
PropertyChanges {
loopButton.icon.source: Config.iconSource("Loop_Playlist", false)
}
}
]
}
}
Item {
Layout.fillWidth: true
}
AudioControl {
id: audio
Layout.fillHeight: true
Layout.fillWidth: true
}
Item {
Layout.fillWidth: true
Layout.minimumWidth:40
Layout.maximumWidth: 95
CustomButton {
id: playlistButton
anchors.centerIn: parent
icon.source: root.playlistIcon
}
}
}
}
states: [
State {
name: "playing"
when: root.mediaPlayerState == MediaPlayer.PlayingState
PropertyChanges {
playButton.visible: false
}
PropertyChanges {
pausedButton.visible: true
}
},
State {
name: "paused"
when: root.mediaPlayerState == MediaPlayer.PausedState || root.mediaPlayerState == MediaPlayer.StoppedState
PropertyChanges {
playButton.visible: true
}
PropertyChanges {
pausedButton.visible: false
}
}
]
}
20:MediaControls(PlaybackRateControl.qml文件)
-
21:MediaControls(PlaybackSeekControl.qml)// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import QtQuick import QtQuick.Controls.Fusion import QtQuick.Layouts import Config Item { id: root property alias playbackRate: slider.value Layout.minimumWidth: 100 Layout.maximumWidth: 200 RowLayout { anchors.fill: root spacing: 10 Image { source: Config.iconSource("Rate_Icon") } CustomSlider { id: slider Layout.fillWidth: true snapMode: Slider.SnapOnRelease from: 0.5 to: 2.5 stepSize: 0.5 value: 1.0 } Label { text: slider.value.toFixed(1) + "x" color: "#41CD52" } } }
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Layouts
import QtMultimedia
import Config
Item {
id: root
implicitHeight: 40
required property MediaPlayer mediaPlayer
property alias fullScreenButton: fullScreenButton
property alias settingsButton: settingsButton
property alias isMediaSliderPressed: mediaSlider.pressed
property alias showSeeker: showSeekerAnim
property alias hideSeeker: hideSeekerAnim
function getTime(time : int) {
const h = Math.floor(time / 3600000).toString()
const m = Math.floor(time / 60000).toString()
const s = Math.floor(time / 1000 - m * 60).toString()
return `${h.padStart(2,'0')}:${m.padStart(2,'0')}:${s.padStart(2, '0')}`
}
RowLayout {
anchors.fill: root
anchors.leftMargin: 10
anchors.rightMargin: 10
Label {
id: mediaTime
color: Config.secondaryColor
font.bold: true
text: root.getTime(root.mediaPlayer.position)
}
CustomSlider {
id: mediaSlider
backgroundColor: !Config.activeTheme ? "white" : "#41CD52"
backgroundOpacity: !Config.activeTheme ? 0.8 : 0.2
enabled: root.mediaPlayer.seekable
to: 1.0
value: root.mediaPlayer.position / root.mediaPlayer.duration
Layout.fillWidth: true
onMoved: root.mediaPlayer.setPosition(value * root.mediaPlayer.duration)
}
Label {
id: durationTime
color: Config.secondaryColor
font.bold: true
text: root.getTime(root.mediaPlayer.duration)
}
CustomButton {
id: settingsButton
icon.source: Config.iconSource("Settings_Icon")
}
CustomButton {
id: fullScreenButton
icon.source: Config.iconSource("FullScreen_Icon")
}
}
ParallelAnimation {
id: hideSeekerAnim
NumberAnimation {
target: root
properties: "opacity"
to: 0
duration: 1000
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: root
properties: "anchors.bottomMargin"
to: -root.height
duration: 1000
easing.type: Easing.InOutQuad
}
}
ParallelAnimation {
id: showSeekerAnim
PropertyAnimation {
target: root
properties: "opacity"
to: 1
duration: 1000
easing.type: Easing.InOutQuad
}
PropertyAnimation {
target: root
properties: "anchors.bottomMargin"
to: 0
duration: 500
easing.type: Easing.InOutQuad
}
}
}