“深入浅出”系列之QT:实战篇(4)MultimediaPlayer视频播放器

通过这个项目,你将学会如何利用Qt强大的QMediaPlayerQVideoWidget等核心组件,从零开始构建一个功能完整的视频播放器。不仅会实现播放、暂停、进度控制等基础功能,还会深入探讨如何优化播放性能、支持多格式解码,甚至添加字幕、音量调节和全屏播放等进阶特性。

一:运行效果

图片

二:工程项目结构

图片

三:工程项目源码文件

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 QtQuickimport QtQuick.Controls.Fusionimport 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 QtQuickimport QtQuick.Layoutsimport QtQuick.Controls.Fusionimport 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 QtQuickimport QtQuick.Controls.Fusionimport QtQuick.Dialogsimport 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 QtQuickimport QtQuick.Controls.Fusionimport QtQuick.Dialogsimport QtQuick.Layoutsimport QtCoreimport MediaControlsimport 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 QtQuickimport QtQuick.Layoutsimport QtQuick.Controls.Fusionimport QtMultimediaimport 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 QtQuickimport QtQuick.Controls.Fusionimport MediaControlsimport 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 QtQuickimport QtQuick.Controls.Fusionimport QtQuick.Layoutsimport 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 QtQuickimport 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 QtQuickimport QtQuick.Controls.Fusionimport QtMultimediaimport MediaControlsimport 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 QtQuickimport QtQuick.Controls.Fusionimport QtQuick.Layoutsimport MediaControlsimport 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 Singletonimport 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 QtQuickimport QtQuick.Layoutsimport 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 QtQuickimport QtQuick.Controls.Fusionimport 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 QtQuickimport QtQuick.Controls.Fusionimport 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文件)

  • // 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"
            }
        }
    }
    ​​​​​​​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 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
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值