[QtCPP]Examples使用示例-QtMultimedia、QMediaPlayer、Audio音频引擎测试mp3播放

一、Qml 音频播放器程序

说明:跨平台选择MP3播放+跨平台自定义鼠标

QmlAudioPlayer.pro

bash 复制代码
QT += quick multimedia quickcontrols2

CONFIG += c++17

# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
    main.cpp

RESOURCES += qml.qrc

#指定编译产生的文件分门别类放到对应目录
MOC_DIR     = temp/moc
RCC_DIR     = temp/rcc
UI_DIR      = temp/ui
OBJECTS_DIR = temp/obj

#指定编译生成的可执行文件放到源码上一级目录下的bin目录 一般$$PWD/bin
!android:!ios {
DESTDIR = $$PWD/bin
}

# Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH =

# Additional import path used to resolve QML modules just for Qt Quick Designer
QML_DESIGNER_IMPORT_PATH =

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

HEADERS += \
    NativeMouseFilter.h

qml.qrc

bash 复制代码
<RCC>
    <qresource prefix="/">
        <file>main.qml</file>
        <file>CustomCursor.qml</file>
    </qresource>
</RCC>

NativeMouseFilter.h

javascript 复制代码
#ifndef NATIVEMOUSEFILTER_H
#define NATIVEMOUSEFILTER_H
 
#include <QObject>
#include <QDebug>
#include <QGuiApplication>
#include <QScreen>
#include <QCursor>
#include <QWindow>
// ================= 新增注入所需的头文件 =================
#include <QMouseEvent>
#include <QCoreApplication>
// ========================================================
//QCursor::setPos(x, y); // 影响Failed to move cursor on screen HDMI1: -14报错的罪魁祸首
// ================= 跨平台头文件引入 =================
#ifdef Q_OS_WIN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <windowsx.h>
#include <QAbstractNativeEventFilter>
#elif defined(Q_OS_LINUX) || defined(Q_OS_ANDROID)
#include <QThread>
// Linux evdev 头文件
#include <linux/input.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>
#include <string.h>
#endif
 
// ================= Linux: 直接读取 evdev 的后台线程 =================
#ifdef Q_OS_LINUX
class EvdevReader : public QThread {
    Q_OBJECT
public:
    explicit EvdevReader(QObject *parent = nullptr) : QThread(parent), m_running(false) {}
 
    void startReading() {
        m_running = true;
        start(QThread::HighPriority);
    }
 
    void stopReading() {
        m_running = false;
        quit();
        wait(1000);
    }
 
signals:
    void mouseMoved(int x, int y, bool absolute);
    void mouseButtonChanged(int button, bool pressed);
    // ================= 新增:滚轮信号 =================
    void mouseWheel(int y);
    // ================================================
 
protected:
    void run() override {
        // 1. 扫描 /dev/input/ 下所有鼠标设备
        QStringList mouseDevices = findMouseDevices();
        if (mouseDevices.isEmpty()) {
            qWarning() << "[EvdevReader] 未找到任何鼠标/触摸设备!";
            return;
        }
        qDebug() << "[EvdevReader] 找到设备:" << mouseDevices;
 
        // 2. 打开所有找到的设备
        QList<int> fds;
        for (const QString &dev : mouseDevices) {
            int fd = open(dev.toUtf8().constData(), O_RDONLY | O_NONBLOCK);
            if (fd < 0) {
                qWarning() << "[EvdevReader] 无法打开" << dev << ":" << strerror(errno);
                continue;
            }
            fds.append(fd);
            qDebug() << "[EvdevReader] 已打开" << dev << "fd=" << fd;
        }
 
        if (fds.isEmpty()) {
            qWarning() << "[EvdevReader] 没有成功打开任何设备";
            return;
        }
 
        int x = 0, y = 0;
        bool hasAbs = false;
        int absMinX = 0, absMaxX = 65535;
        int absMinY = 0, absMaxY = 65535;
        int screenW = 1920, screenH = 1080;
 
        // 获取屏幕分辨率
        if (!QGuiApplication::screens().isEmpty()) {
            QScreen *screen = QGuiApplication::primaryScreen();
            screenW = screen->size().width();
            screenH = screen->size().height();
        }
 
        // 3. 轮询读取事件
        fd_set readfds;
        int maxFd = 0;
        for (int fd : fds) {
            if (fd > maxFd) maxFd = fd;
        }
 
        while (m_running) {
            FD_ZERO(&readfds);
            for (int fd : fds) {
                FD_SET(fd, &readfds);
            }
            struct timeval tv;
            tv.tv_sec = 0;
            tv.tv_usec = 10000; // 10ms 超时
 
            int ret = select(maxFd + 1, &readfds, nullptr, nullptr, &tv);
            if (ret < 0) {
                if (errno == EINTR) continue;
                qWarning() << "[EvdevReader] select 错误:" << strerror(errno);
                break;
            }
 
            for (int fd : fds) {
                if (!FD_ISSET(fd, &readfds)) continue;
 
                struct input_event ev;
                while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) {
                    if (ev.type == EV_ABS) {
                        hasAbs = true;
                        if (ev.code == ABS_X) {
                            x = mapAbs(ev.value, absMinX, absMaxX, screenW);
                        } else if (ev.code == ABS_Y) {
                            y = mapAbs(ev.value, absMinY, absMaxY, screenH);
                        }
                    } else if (ev.type == EV_REL) {
                        hasAbs = false;
                        if (ev.code == REL_X) {
                            x += ev.value;
                        } else if (ev.code == REL_Y) {
                            y += ev.value;
                        } 
                        // ================= 新增:捕获滚轮事件 =================
                        else if (ev.code == REL_WHEEL) {
                            emit mouseWheel(ev.value);
                        }
                        // ====================================================
                    } else if (ev.type == EV_SYN && ev.code == SYN_REPORT) {
                        x = qBound(0, x, screenW - 1);
                        y = qBound(0, y, screenH - 1);
                        emit mouseMoved(x, y, hasAbs);
                    } else if (ev.type == EV_KEY) {
                        if (ev.code == BTN_LEFT || ev.code == BTN_RIGHT || ev.code == BTN_MIDDLE) {
                            int btn = (ev.code == BTN_LEFT) ? 0 : (ev.code == BTN_RIGHT) ? 1 : 2;
                            emit mouseButtonChanged(btn, ev.value == 1);
                        }
                    }
                }
            }
        }
 
        // 清理
        for (int fd : fds) {
            close(fd);
        }
    }
 
private:
    bool m_running;
 
    QStringList findMouseDevices() {
        QStringList devices;
        DIR *dir = opendir("/dev/input");
        if (!dir) return devices;
 
        struct dirent *entry;
        while ((entry = readdir(dir)) != nullptr) {
            if (strncmp(entry->d_name, "event", 5) == 0) {
                QString path = QString("/dev/input/") + entry->d_name;
                int fd = open(path.toUtf8().constData(), O_RDONLY | O_NONBLOCK);
                if (fd < 0) continue;
 
                unsigned long evBits[EV_MAX / 8 + 1] = {0};
                unsigned long relBits[REL_MAX / 8 + 1] = {0};
                unsigned long absBits[ABS_MAX / 8 + 1] = {0};
                unsigned long keyBits[KEY_MAX / 8 + 1] = {0};
 
                ioctl(fd, EVIOCGBIT(0, sizeof(evBits)), evBits);
                ioctl(fd, EVIOCGBIT(EV_REL, sizeof(relBits)), relBits);
                ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absBits)), absBits);
                ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBits)), keyBits);
 
                bool hasRelXY = testBit(relBits, REL_X) && testBit(relBits, REL_Y);
                bool hasAbsXY = testBit(absBits, ABS_X) && testBit(absBits, ABS_Y);
                bool hasMouseBtn = testBit(keyBits, BTN_LEFT);
 
                if (hasRelXY || (hasAbsXY && hasMouseBtn)) {
                    char name[256] = {0};
                    ioctl(fd, EVIOCGNAME(sizeof(name)), name);
                    qDebug() << "[EvdevReader] 发现鼠标设备:" << path << name << "relXY=" << hasRelXY << "absXY=" << hasAbsXY;
                    devices.append(path);
                }
                close(fd);
            }
        }
        closedir(dir);
        return devices;
    }
 
    bool testBit(const unsigned long *bits, int bit) {
        return (bits[bit / 8] >> (bit % 8)) & 1;
    }
 
    int mapAbs(int value, int min, int max, int targetRange) {
        if (max <= min) return 0;
        return (int)((long long)(value - min) * targetRange / (max - min));
    }
};
#endif // Q_OS_LINUX
 
 
// ================= 跨平台鼠标过滤器 =================
#ifdef Q_OS_WIN
class NativeMouseFilter : public QObject, public QAbstractNativeEventFilter
#elif defined(Q_OS_LINUX)
class NativeMouseFilter : public QObject
#endif
{
    Q_OBJECT
    Q_PROPERTY(int cursorX READ cursorX NOTIFY cursorPositionChanged)
    Q_PROPERTY(int cursorY READ cursorY NOTIFY cursorPositionChanged)
    Q_PROPERTY(bool buttonLeft READ buttonLeft NOTIFY buttonStateChanged)
    Q_PROPERTY(bool buttonRight READ buttonRight NOTIFY buttonStateChanged)
public:
    explicit NativeMouseFilter(QObject *parent = nullptr) : QObject(parent), m_targetWindow(nullptr)
    {
        m_x = 0;
        m_y = 0;
        m_buttonLeft = false;
        m_buttonRight = false;
        QPoint pos = QCursor::pos();
        m_x = pos.x();
        m_y = pos.y();
 
#ifdef Q_OS_LINUX
        qDebug() << "====== 鼠标拦截器启动(Linux 直接 evdev 模式)======";
        qDebug() << "初始坐标:" << m_x << m_y;
 
        m_reader = new EvdevReader(this);
        
        connect(m_reader, &EvdevReader::mouseMoved, this, [this](int x, int y, bool absolute) {
            Q_UNUSED(absolute)
            m_x = x;
            m_y = y;
            emit cursorPositionChanged();
 
            // ================= 修改:注入鼠标移动事件(携带正确的 buttons 状态) =================
            QWindow *targetWindow = m_targetWindow;
            if (!targetWindow) targetWindow = QGuiApplication::topLevelWindows().isEmpty() ? nullptr : QGuiApplication::topLevelWindows().first();
            if (targetWindow) {
                //QCursor::setPos(x, y);
                QPoint globalPos(x, y);
                QPointF localPos = targetWindow->mapFromGlobal(globalPos);
                
                // 【核心修正】获取当前实际按下的按钮状态
                Qt::MouseButtons currentButtons = getCurrentButtons();
                
                QMouseEvent *moveEvent = new QMouseEvent(
                    QEvent::MouseMove, 
                    localPos, 
                    localPos, 
                    globalPos, 
                    Qt::NoButton,      // button: Move事件本身不是由某个按钮触发的
                    currentButtons,    // buttons: 【关键】当前所有持续按下的按钮
                    Qt::NoModifier
                );
                QCoreApplication::postEvent(targetWindow, moveEvent);
            }
            // ========================================================
        });
 
        connect(m_reader, &EvdevReader::mouseButtonChanged, this, [this](int btn, bool pressed) {
            // 【核心修正】先更新内部按钮状态,确保后续事件使用最新状态
            Qt::MouseButton qtButton = Qt::NoButton;
            if (btn == 0) {
                m_buttonLeft = pressed;
                qtButton = Qt::LeftButton;
            } else if (btn == 1) {
                m_buttonRight = pressed;
                qtButton = Qt::RightButton;
            } else {
                qtButton = Qt::MiddleButton;
            }
            emit buttonStateChanged();
 
            // ================= 修改:注入鼠标点击事件(携带正确的 buttons 状态) =================
            QWindow *targetWindow = m_targetWindow;
            if (!targetWindow) targetWindow = QGuiApplication::topLevelWindows().isEmpty() ? nullptr : QGuiApplication::topLevelWindows().first();
            if (targetWindow) {
                QEvent::Type eventType = pressed ? QEvent::MouseButtonPress : QEvent::MouseButtonRelease;
                
                // 【核心修正】使用更新后的状态计算当前 buttons()
                Qt::MouseButtons currentButtons = getCurrentButtons();
                
                QPoint globalPos(m_x, m_y);
                QPointF localPos = targetWindow->mapFromGlobal(globalPos);
                
                QMouseEvent *btnEvent = new QMouseEvent(
                    eventType, 
                    localPos, 
                    localPos, 
                    globalPos, 
                    qtButton,       // button: 触发此事件的按钮
                    currentButtons, // buttons: 事件发生后(按下/释放)的当前按钮状态
                    Qt::NoModifier
                );
                QCoreApplication::postEvent(targetWindow, btnEvent);
                
                // 【核心修正】按下后立刻补发一个 MouseMove,确保 Qt 拖拽状态机正确初始化
                // 因为 Qt 的 drag 检测需要在 Press 之后立即收到一个带正确 buttons 的 Move
                if (pressed) {
                    QMouseEvent *moveEvent = new QMouseEvent(
                        QEvent::MouseMove,
                        localPos,
                        localPos,
                        globalPos,
                        Qt::NoButton,
                        currentButtons, // 包含刚刚按下的按钮
                        Qt::NoModifier
                    );
                    QCoreApplication::postEvent(targetWindow, moveEvent);
                }
            }
            // ========================================================
        });
 
        // ================= 修改:注入鼠标滚轮事件(携带正确的 buttons 状态) =================
        connect(m_reader, &EvdevReader::mouseWheel, this, [this](int y) {
            QWindow *targetWindow = m_targetWindow;
            if (!targetWindow) targetWindow = QGuiApplication::topLevelWindows().isEmpty() ? nullptr : QGuiApplication::topLevelWindows().first();
            if (targetWindow) {
                QPoint globalPos(m_x, m_y);
                QPointF localPos = targetWindow->mapFromGlobal(globalPos);
                
                // 【核心修正】获取当前按钮状态
                Qt::MouseButtons currentButtons = getCurrentButtons();
                
                QWheelEvent *wheelEvent = new QWheelEvent(
                    localPos, 
                    globalPos, 
                    QPoint(0, 0), 
                    QPoint(0, y * 120),
                    currentButtons, // 【关键】使用当前按钮状态
                    Qt::NoModifier, 
                    Qt::NoScrollPhase, 
                    false
                );
                QCoreApplication::postEvent(targetWindow, wheelEvent);
            }
        });
        // ========================================================
 
        m_reader->startReading();
 
#elif defined(Q_OS_WIN)
        qDebug() << "====== 鼠标拦截器启动(Windows nativeEventFilter 模式)======";
        qDebug() << "初始坐标:" << m_x << m_y;
        // Windows: 使用 installNativeEventFilter 安装,在 nativeEventFilter 中处理
#endif
    }
 
    ~NativeMouseFilter() {
#ifdef Q_OS_LINUX
        if (m_reader) {
            m_reader->stopReading();
            m_reader->deleteLater();
        }
#endif
    }
 
    // ================= 新增:设置目标窗口方法 =================
    void setTargetWindow(QWindow *window) {
        m_targetWindow = window;
    }
    // ========================================================
 
    int cursorX() const { return m_x; }
    int cursorY() const { return m_y; }
    bool buttonLeft() const { return m_buttonLeft; }
    bool buttonRight() const { return m_buttonRight; }
 
signals:
    void cursorPositionChanged();
    void buttonStateChanged();
 
#ifdef Q_OS_WIN
protected:
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    bool nativeEventFilter(const QByteArray &eventType, void *message, long *result) override {
#else
    bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) override {
#endif
        Q_UNUSED(result)
        if (eventType == "windows_generic_MSG" || eventType == "windows_dispatcher_MSG") {
            MSG* msg = static_cast<MSG*>(message);
            if (msg->message == WM_MOUSEMOVE) {
                m_x = GET_X_LPARAM(msg->lParam);
                m_y = GET_Y_LPARAM(msg->lParam);
                emit cursorPositionChanged();
                qDebug() << "[Windows Native] X:" << m_x << "Y:" << m_y;
            } else if (msg->message == WM_LBUTTONDOWN || msg->message == WM_LBUTTONUP) {
                m_buttonLeft = (msg->message == WM_LBUTTONDOWN);
                emit buttonStateChanged();
            } else if (msg->message == WM_RBUTTONDOWN || msg->message == WM_RBUTTONUP) {
                m_buttonRight = (msg->message == WM_RBUTTONDOWN);
                emit buttonStateChanged();
            }
        }
        return false; // 不拦截,继续传递
    }
#endif
 
private:
    int m_x, m_y;
    bool m_buttonLeft, m_buttonRight;
    
    // ================= 新增:目标窗口变量 =================
    QWindow *m_targetWindow;
    // ========================================================
    
    // ================= 新增:获取当前按钮状态辅助函数 =================
    Qt::MouseButtons getCurrentButtons() const {
        Qt::MouseButtons buttons = Qt::NoButton;
        if (m_buttonLeft) buttons |= Qt::LeftButton;
        if (m_buttonRight) buttons |= Qt::RightButton;
        return buttons;
    }
    // ========================================================
    
#ifdef Q_OS_LINUX
    EvdevReader *m_reader = nullptr;
#endif
};
 
#endif // NATIVEMOUSEFILTER_H
main.cpp
bash 复制代码
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickStyle>
#include <QQuickWindow> // 修复2:必须引入此头文件
#include "NativeMouseFilter.h"

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);


#ifdef Q_OS_LINUX
    qputenv("QT_QPA_EGLFS_DISABLE_INPUT", "1");
    qputenv("QT_QPA_EGLFS_HIDECURSOR", "1");
    qputenv("QT_QPA_PLATFORM", "eglfs");
    qputenv("QT_OPENGL", "es2");
    qputenv("QT_QPA_EGLFS_INTEGRATION", "eglfs_kms");
#endif

    QGuiApplication app(argc, argv);

    NativeMouseFilter *mouseFilter = new NativeMouseFilter(&app);
#ifdef Q_OS_WIN
    app.installNativeEventFilter(mouseFilter);
#endif


    app.setOrganizationName("QmlAudioPlayer");
    app.setApplicationName("Qml Audio Player");

    // 使用 Fusion 风格,在 Linux 上表现一致
    QQuickStyle::setStyle("Fusion");

    QQmlApplicationEngine engine;

    engine.rootContext()->setContextProperty("nativeMouse", mouseFilter);

    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);

    engine.load(url);

    return app.exec();
}

main.qml

javascript 复制代码
// -*- coding: utf-8 -*-
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Dialogs 1.3 // FileDialog
import QtMultimedia 5.15

Window {
    id: root
    visible: true
    width: 900
    height: 640
    minimumWidth: 700
    minimumHeight: 500
    title: qsTr("Qml 音频播放器")

    visibility: Window.FullScreen
    // ==================== 颜色主题 ====================
    readonly property color bgColor: "#1a1a2e"
    readonly property color panelColor: "#16213e"
    readonly property color accentColor: "#0f3460"
    readonly property color highlightColor:"#e94560"
    readonly property color textColor: "#eaeaea"
    readonly property color dimTextColor: "#8899aa"
    readonly property color sliderTrack: "#2a2a4a"
    color: bgColor

    CustomCursor{

        // 【保持不变】正确使用 Overlay.overlay
        // parent: Overlay.overlay
        z: 9999  // 确保在最上层
        anchors.fill: parent

        curScreenWidth: mainContainer.screenWidth
        curScreenHeight:mainContainer.screenHeight
        // 【关键】确保光标始终可见
        cursorVisible: true

        onMPositionChanged: {
            //console.log("鼠标:", mouse.x, mouse.y);
        }

    }

    // ==================== 音频引擎 ====================
    Audio {
        id: audioPlayer
        property bool manualStop: false

        // 播放结束自动下一首
        onStatusChanged: {
            if (status === Audio.EndOfMedia && !manualStop) {
                playNext();
            }
        }
        onError: {
            console.error("Audio error:", errorString);
            statusText.text = qsTr("播放错误: ") + errorString;
        }
        onPlaying: {
            statusText.text = qsTr("正在播放");
        }
        onPaused: {
            statusText.text = qsTr("已暂停");
        }
        onStopped: {
            if (manualStop) {
                statusText.text = qsTr("已停止");
                manualStop = false;
            }
        }
    }

    // ==================== 播放列表数据 ====================
    ListModel {
        id: playlistModel
    }

    // ==================== 当前播放索引 ====================
    property int currentIndex: -1
    property bool shuffleMode: false
    property int repeatMode: 0 // 0: 不重复, 1: 列表循环, 2: 单曲循环

    // ==================== 辅助函数 ====================
    function formatTime(ms) {
        if (isNaN(ms) || ms <= 0) return "00:00";
        var totalSeconds = Math.floor(ms / 1000);
        var minutes = Math.floor(totalSeconds / 60);
        var seconds = totalSeconds % 60;
        return (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
    }

    function playNext() {
        if (playlistModel.count === 0) return;
        if (repeatMode === 2) { // 单曲循环
            audioPlayer.seek(0);
            audioPlayer.play();
            return;
        }
        var nextIndex = -1;
        if (shuffleMode) {
            nextIndex = Math.floor(Math.random() * playlistModel.count);
        } else {
            nextIndex = (currentIndex + 1) % playlistModel.count;
        }
        if (nextIndex === 0 && repeatMode === 0 && !shuffleMode) {
            // 不重复模式,播放到列表末尾停止
            audioPlayer.stop();
            return;
        }
        currentIndex = nextIndex;
        playCurrentTrack();
    }

    function playPrevious() {
        if (playlistModel.count === 0) return;
        // 如果已播放超过3秒,则重新播放当前曲目
        if (audioPlayer.position > 3000) {
            audioPlayer.seek(0);
            return;
        }
        var prevIndex = -1;
        if (shuffleMode) {
            prevIndex = Math.floor(Math.random() * playlistModel.count);
        } else {
            prevIndex = (currentIndex - 1 + playlistModel.count) % playlistModel.count;
        }
        currentIndex = prevIndex;
        playCurrentTrack();
    }

    function playCurrentTrack() {
        if (currentIndex < 0 || currentIndex >= playlistModel.count) return;
        var track = playlistModel.get(currentIndex);
        audioPlayer.source = track.filePath;
        audioPlayer.play();
        statusText.text = qsTr("正在播放: ") + track.fileName;
    }

    function addToPlaylist(fileUrl, fileName) {
        // 检查是否已存在
        for (var i = 0; i < playlistModel.count; i++) {
            if (playlistModel.get(i).filePath === fileUrl) {
                return;
            }
        }
        playlistModel.append({ "filePath": fileUrl, "fileName": fileName });
    }

    function removeFromPlaylist(index) {
        if (index < 0 || index >= playlistModel.count) return;
        // 如果删除的是当前播放的曲目
        if (index === currentIndex) {
            audioPlayer.stop();
            audioPlayer.manualStop = true;
            if (playlistModel.count > 1) {
                playlistModel.remove(index);
                if (currentIndex >= playlistModel.count) {
                    currentIndex = 0;
                }
                playCurrentTrack();
            } else {
                playlistModel.remove(index);
                currentIndex = -1;
                audioPlayer.source = "";
                statusText.text = qsTr("播放列表为空");
            }
        } else {
            playlistModel.remove(index);
            // 调整当前索引
            if (index < currentIndex) {
                currentIndex--;
            }
        }
    }

    function clearPlaylist() {
        audioPlayer.stop();
        audioPlayer.manualStop = true;
        audioPlayer.source = "";
        playlistModel.clear();
        currentIndex = -1;
        statusText.text = qsTr("播放列表已清空");
    }

    // ==================== 文件选择对话框 ====================
    FileDialog {
        id: fileDialog
        title: qsTr("选择音频文件")
        folder: shortcuts.home
        selectMultiple: true
        nameFilters: [
            "音频文件 (*.mp3 *.wav *.ogg *.flac *.aac *.wma *.m4a)",
            "MP3 文件 (*.mp3)",
            "WAV 文件 (*.wav)",
            "OGG 文件 (*.ogg)",
            "FLAC 文件 (*.flac)",
            "所有文件 (*)"
        ]
        onAccepted: {
            var urls = fileDialog.fileUrls;
            for (var i = 0; i < urls.length; i++) {
                var path = urls[i].toString();
                // 提取文件名
                var parts = path.split("/");
                var name = decodeURIComponent(parts[parts.length - 1]);
                addToPlaylist(path, name);
            }
            // 如果当前没有播放,自动开始播放第一个
            if (currentIndex === -1 && playlistModel.count > 0) {
                currentIndex = 0;
                playCurrentTrack();
            }
        }
    }

    // ==================== 主布局 ====================
    ColumnLayout {
        anchors.fill: parent
        spacing: 0

        // ========== 顶部标题栏 ==========
        Rectangle {
            Layout.fillWidth: true
            Layout.preferredHeight: 50
            color: panelColor

            RowLayout {
                anchors.fill: parent
                anchors.leftMargin: 20
                anchors.rightMargin: 20

                Text {
                    text: "🎵 Qml 音频播放器"
                    color: highlightColor
                    font.pixelSize: 20
                    font.bold: true
                }

                Item { Layout.fillWidth: true }

                Text {
                    id: statusText
                    text: qsTr("就绪")
                    color: dimTextColor
                    font.pixelSize: 13
                }
            }
        }

        // ========== 中间内容区 ==========
        SplitView {
            Layout.fillWidth: true
            Layout.fillHeight: true
            orientation: Qt.Horizontal

            // ===== 左侧:播放列表 =====
            Rectangle {
                SplitView.minimumWidth: 250
                SplitView.preferredWidth: 320
                color: panelColor

                ColumnLayout {
                    anchors.fill: parent
                    anchors.margins: 10
                    spacing: 8

                    // 播放列表标题和操作按钮
                    RowLayout {
                        Layout.fillWidth: true

                        Text {
                            text: qsTr("播放列表")
                            color: textColor
                            font.pixelSize: 16
                            font.bold: true
                        }

                        Item { Layout.fillWidth: true }

                        Button {
                            text: qsTr("添加")
                            font.pixelSize: 12
                            onClicked: fileDialog.open()
                            background: Rectangle {
                                color: parent.pressed ? highlightColor : accentColor
                                radius: 4
                            }
                            contentItem: Text {
                                text: parent.text
                                color: "white"
                                font: parent.font
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                            }
                        }

                        Button {
                            text: qsTr("清空")
                            font.pixelSize: 12
                            onClicked: clearPlaylist()
                            background: Rectangle {
                                color: parent.pressed ? highlightColor : accentColor
                                radius: 4
                            }
                            contentItem: Text {
                                text: parent.text
                                color: "white"
                                font: parent.font
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                            }
                        }
                    }

                    // 播放列表
                    ListView {
                        id: playlistView
                        Layout.fillWidth: true
                        Layout.fillHeight: true
                        clip: true
                        model: playlistModel
                        highlight: Rectangle { color: accentColor; radius: 4 }
                        highlightMoveDuration: 200

                        delegate: Rectangle {
                            width: playlistView.width
                            height: 44
                            color: index === currentIndex ? accentColor : (mouseArea.containsMouse ? "#1e2d4a" : "transparent")
                            radius: 4

                            RowLayout {
                                anchors.fill: parent
                                anchors.leftMargin: 10
                                anchors.rightMargin: 10
                                spacing: 8

                                // 播放指示器
                                Text {
                                    text: index === currentIndex && audioPlayer.playing ? "▶" : ""
                                    color: highlightColor
                                    font.pixelSize: 14
                                    Layout.preferredWidth: 16
                                }
                                // 序号
                                Text {
                                    text: (index + 1) + "."
                                    color: dimTextColor
                                    font.pixelSize: 12
                                    Layout.preferredWidth: 28
                                }
                                // 文件名
                                Text {
                                    text: fileName
                                    color: index === currentIndex ? highlightColor : textColor
                                    font.pixelSize: 13
                                    elide: Text.ElideRight
                                    Layout.fillWidth: true
                                }
                                // 删除按钮
                                Button {
                                    text: "✕"
                                    font.pixelSize: 11
                                    Layout.preferredWidth: 24
                                    Layout.preferredHeight: 24
                                    background: Rectangle { color: "transparent" }
                                    contentItem: Text {
                                        text: parent.text
                                        color: dimTextColor
                                        font: parent.font
                                        horizontalAlignment: Text.AlignHCenter
                                        verticalAlignment: Text.AlignVCenter
                                    }
                                    onClicked: removeFromPlaylist(index)
                                }
                            }

                            MouseArea {
                                id: mouseArea
                                anchors.fill: parent
                                hoverEnabled: true
                                onDoubleClicked: {
                                    currentIndex = index;
                                    playCurrentTrack();
                                }
                            }
                        }

                        // 空列表提示
                        Text {
                            anchors.centerIn: parent
                            text: qsTr("点击「添加」按钮\n选择音频文件")
                            color: dimTextColor
                            font.pixelSize: 14
                            horizontalAlignment: Text.AlignHCenter
                            visible: playlistModel.count === 0
                        }
                    }
                }
            }

            // ===== 右侧:可视化区域 =====
            Rectangle {
                SplitView.minimumWidth: 300
                color: bgColor

                ColumnLayout {
                    anchors.fill: parent
                    anchors.margins: 20
                    spacing: 16

                    // 专辑/封面占位区域
                    Rectangle {
                        Layout.fillWidth: true
                        Layout.fillHeight: true
                        color: "#0d1b2a"
                        radius: 12
                        border.color: accentColor
                        border.width: 1

                        ColumnLayout {
                            anchors.centerIn: parent
                            spacing: 12

                            // 旋转的音符图标
                            Text {
                                text: "♪"
                                color: highlightColor
                                font.pixelSize: 80
                                Layout.alignment: Qt.AlignHCenter

                                RotationAnimation on rotation {
                                    from: 0
                                    to: 360
                                    duration: 4000
                                    loops: Animation.Infinite
                                    running: audioPlayer.playing
                                }
                            }

                            Text {
                                text: currentIndex >= 0 ? playlistModel.get(currentIndex).fileName : qsTr("未选择文件")
                                color: textColor
                                font.pixelSize: 16
                                font.bold: true
                                Layout.alignment: Qt.AlignHCenter
                                Layout.maximumWidth: parent.parent.width - 40
                                elide: Text.ElideMiddle
                                horizontalAlignment: Text.AlignHCenter
                            }

                            Text {
                                text: formatTime(audioPlayer.position) + " / " + formatTime(audioPlayer.duration)
                                color: dimTextColor
                                font.pixelSize: 14
                                Layout.alignment: Qt.AlignHCenter
                            }
                        }
                    }

                    // 频谱可视化占位(简单柱状动画)
                    Row {
                        Layout.fillWidth: true
                        Layout.preferredHeight: 60
                        Layout.alignment: Qt.AlignHCenter
                        spacing: 3

                        Repeater {
                            model: 32
                            Rectangle {
                                width: (root.width - 640) / 32 - 3 > 8 ? 8 : (root.width - 640) / 32 - 3
                                height: audioPlayer.playing ? (10 + Math.random() * 50) : 5
                                color: highlightColor
                                radius: 2
                                opacity: 0.7 + Math.random() * 0.3

                                Behavior on height { NumberAnimation { duration: 150 } }

                                Timer {
                                    interval: 150
                                    running: audioPlayer.playing
                                    repeat: true
                                    onTriggered: parent.height = 10 + Math.random() * 50
                                }
                            }
                        }
                    }
                }
            }
        }

        // ========== 底部控制栏 ==========
        Rectangle {
            Layout.fillWidth: true
            Layout.preferredHeight: 120
            color: panelColor

            ColumnLayout {
                anchors.fill: parent
                anchors.leftMargin: 20
                anchors.rightMargin: 20
                anchors.topMargin: 8
                anchors.bottomMargin: 8
                spacing: 6

                // ===== 进度条 =====
                RowLayout {
                    Layout.fillWidth: true
                    spacing: 10

                    Text {
                        text: formatTime(audioPlayer.position)
                        color: dimTextColor
                        font.pixelSize: 12
                        Layout.preferredWidth: 45
                    }

                    Slider {
                        id: progressSlider
                        Layout.fillWidth: true
                        from: 0
                        to: audioPlayer.duration > 0 ? audioPlayer.duration : 1
                        value: audioPlayer.position
                        onMoved: {
                            audioPlayer.seek(value);
                        }
                        background: Rectangle {
                            x: progressSlider.leftPadding
                            y: progressSlider.topPadding + progressSlider.availableHeight / 2 - height / 2
                            width: progressSlider.availableWidth
                            height: 4
                            radius: 2
                            color: sliderTrack

                            Rectangle {
                                width: progressSlider.visualPosition * parent.width
                                height: parent.height
                                radius: 2
                                color: highlightColor
                            }
                        }
                        handle: Rectangle {
                            x: progressSlider.leftPadding + progressSlider.visualPosition * (progressSlider.availableWidth - width)
                            y: progressSlider.topPadding + progressSlider.availableHeight / 2 - height / 2
                            width: 14
                            height: 14
                            radius: 7
                            color: progressSlider.pressed ? "#ff6b81" : highlightColor
                        }
                    }

                    Text {
                        text: formatTime(audioPlayer.duration)
                        color: dimTextColor
                        font.pixelSize: 12
                        Layout.preferredWidth: 45
                        horizontalAlignment: Text.AlignRight
                    }
                }

                // ===== 控制按钮行 =====
                RowLayout {
                    Layout.fillWidth: true
                    spacing: 6

                    // 左侧:模式按钮
                    RowLayout {
                        spacing: 4

                        // 随机播放
                        Button {
                            id: shuffleBtn
                            Layout.preferredWidth: 36
                            Layout.preferredHeight: 36
                            background: Rectangle { color: "transparent"; radius: 4 }
                            contentItem: Text {
                                text: "🔀"
                                font.pixelSize: 16
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                                opacity: shuffleMode ? 1.0 : 0.4
                            }
                            onClicked: shuffleMode = !shuffleMode
                            ToolTip.visible: pressed
                            ToolTip.text: shuffleMode ? qsTr("关闭随机播放") : qsTr("开启随机播放")
                        }

                        // 循环模式
                        Button {
                            id: repeatBtn
                            Layout.preferredWidth: 36
                            Layout.preferredHeight: 36
                            background: Rectangle { color: "transparent"; radius: 4 }
                            contentItem: Text {
                                text: repeatMode === 2 ? "🔂" : "🔁"
                                font.pixelSize: 16
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                                opacity: repeatMode > 0 ? 1.0 : 0.4
                            }
                            onClicked: repeatMode = (repeatMode + 1) % 3
                            ToolTip.visible: pressed
                            ToolTip.text: repeatMode === 0 ? qsTr("不重复") : (repeatMode === 1 ? qsTr("列表循环") : qsTr("单曲循环"))
                        }
                    }

                    Item { Layout.fillWidth: true }

                    // 中间:播放控制
                    RowLayout {
                        spacing: 8
                        Layout.alignment: Qt.AlignHCenter

                        // 上一首
                        Button {
                            Layout.preferredWidth: 44
                            Layout.preferredHeight: 44
                            background: Rectangle {
                                color: parent.pressed ? accentColor : "transparent"
                                radius: 22
                            }
                            contentItem: Text {
                                text: "⏮"
                                font.pixelSize: 22
                                color: textColor
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                            }
                            onClicked: playPrevious()
                        }

                        // 播放/暂停
                        Button {
                            Layout.preferredWidth: 56
                            Layout.preferredHeight: 56
                            background: Rectangle {
                                color: highlightColor
                                radius: 28
                                scale: parent.pressed ? 0.95 : 1.0
                            }
                            contentItem: Text {
                                text: audioPlayer.playing ? "⏸" : "▶"
                                font.pixelSize: 26
                                color: "white"
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                            }
                            onClicked: {
                                if (audioPlayer.playing) {
                                    audioPlayer.pause();
                                } else if (playlistModel.count > 0) {
                                    if (currentIndex === -1) currentIndex = 0;
                                    if (audioPlayer.source.toString() === "") {
                                        playCurrentTrack();
                                    } else {
                                        audioPlayer.play();
                                    }
                                }
                            }
                        }

                        // 下一首
                        Button {
                            Layout.preferredWidth: 44
                            Layout.preferredHeight: 44
                            background: Rectangle {
                                color: parent.pressed ? accentColor : "transparent"
                                radius: 22
                            }
                            contentItem: Text {
                                text: "⏭"
                                font.pixelSize: 22
                                color: textColor
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                            }
                            onClicked: playNext()
                        }

                        // 停止
                        Button {
                            Layout.preferredWidth: 44
                            Layout.preferredHeight: 44
                            background: Rectangle {
                                color: parent.pressed ? accentColor : "transparent"
                                radius: 22
                            }
                            contentItem: Text {
                                text: "⏹"
                                font.pixelSize: 22
                                color: textColor
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                            }
                            onClicked: {
                                audioPlayer.manualStop = true;
                                audioPlayer.stop();
                            }
                        }
                    }

                    Item { Layout.fillWidth: true }

                    // 右侧:音量控制
                    RowLayout {
                        spacing: 6

                        Button {
                            Layout.preferredWidth: 36
                            Layout.preferredHeight: 36
                            background: Rectangle { color: "transparent"; radius: 4 }
                            contentItem: Text {
                                text: volumeSlider.value === 0 ? "🔇" : (volumeSlider.value < 0.5 ? "🔉" : "🔊")
                                font.pixelSize: 16
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                            }
                            onClicked: {
                                if (volumeSlider.value > 0) {
                                    volumeSlider._prevValue = volumeSlider.value;
                                    volumeSlider.value = 0;
                                } else {
                                    volumeSlider.value = volumeSlider._prevValue || 0.7;
                                }
                            }
                        }

                        Slider {
                            id: volumeSlider
                            Layout.preferredWidth: 110
                            from: 0
                            to: 1.0
                            value: 0.7
                            property real _prevValue: 0.7
                            onValueChanged: {
                                audioPlayer.volume = value;
                            }
                            background: Rectangle {
                                x: volumeSlider.leftPadding
                                y: volumeSlider.topPadding + volumeSlider.availableHeight / 2 - height / 2
                                width: volumeSlider.availableWidth
                                height: 4
                                radius: 2
                                color: sliderTrack

                                Rectangle {
                                    width: volumeSlider.visualPosition * parent.width
                                    height: parent.height
                                    radius: 2
                                    color: highlightColor
                                }
                            }
                            handle: Rectangle {
                                x: volumeSlider.leftPadding + volumeSlider.visualPosition * (volumeSlider.availableWidth - width)
                                y: volumeSlider.topPadding + volumeSlider.availableHeight / 2 - height / 2
                                width: 12
                                height: 12
                                radius: 6
                                color: volumeSlider.pressed ? "#ff6b81" : highlightColor
                            }
                        }
                    }
                }
            }
        }
    }

    // ==================== 键盘快捷键 ====================
    Shortcut {
        sequence: "Space"
        onActivated: {
            if (audioPlayer.playing) {
                audioPlayer.pause();
            } else {
                if (currentIndex === -1 && playlistModel.count > 0) currentIndex = 0;
                if (audioPlayer.source.toString() === "" && playlistModel.count > 0) {
                    playCurrentTrack();
                } else {
                    audioPlayer.play();
                }
            }
        }
    }

    Shortcut {
        sequence: "Right"
        onActivated: {
            if (audioPlayer.seekable) {
                audioPlayer.seek(Math.min(audioPlayer.position + 5000, audioPlayer.duration));
            }
        }
    }

    Shortcut {
        sequence: "Left"
        onActivated: {
            if (audioPlayer.seekable) {
                audioPlayer.seek(Math.max(audioPlayer.position - 5000, 0));
            }
        }
    }

    Shortcut {
        sequence: "Ctrl+O"
        onActivated: fileDialog.open()
    }

    Shortcut {
        sequence: "Ctrl+Q"
        onActivated: Qt.quit()
    }
}

CustomCursor.qml

javascript 复制代码
// CustomCursor.qml
import QtQuick 2.15
 
Item {
    id: root
    /* 公共属性 */
    property url source: "qrc:/images/mouseIcon.png"
    property point hotSpot: Qt.point(image.width / 2, image.height / 2)
    property bool cursorVisible: true
    property int cursorZ: 10000
 
    property int curScreenWidth:1920
    property int curScreenHeight:1080
 
    /* 信号 */
    signal mPressed(var mouse)
    signal mReleased(var mouse)
    signal mClicked(var mouse)
    signal mDoubleClicked(var mouse)
    signal mPositionChanged(var mouse)
    signal mEntered()
    signal mExited()
 
    z: cursorZ - 1
 
    // --- 光标图片 ---
    Image {
        id: image
        source: root.source
        // 关键:使用 hoverHandler 的状态控制显示,不再依赖 MouseArea
        //visible: root.cursorVisible && hoverHandler.hovered
        z: root.cursorZ
        width: 16
        height: 16
 
        // 关键:中心对齐
        x: nativeMouse.cursorX //- root.curScreenWidth/ 2             //width
        y: nativeMouse.cursorY //- root.curScreenHeight/ 2
 
        // 点击时的缩放反馈
        scale: nativeMouse.buttonLeft ? 0.8 : 1.0
        Behavior on scale {
            NumberAnimation { duration: 50 }
        }
        // 占位矩形
        Rectangle {
            anchors.fill: parent
            color: "#00FF00"
            border.color: "#FFFFFF"
            border.width: 2
            radius: 8
            visible: parent.status === Image.Error
        }
        layer.enabled: true
        layer.smooth: true
    }
 
    // --- 鼠标事件转发层 ---
    // 职责简化:仅用于隐藏系统光标 和转发点击事件
    MouseArea {
        id: mouseArea
        anchors.fill: parent
        acceptedButtons: Qt.AllButtons
        z: root.cursorZ
 
        //cursorShape: Qt.BlankCursor // 注释掉  隐藏系统光标
        propagateComposedEvents: true
 
        // 【关键修改】关闭 hoverEnabled,避免拦截 HoverHandler 的事件
        // 同时防止在移动时因 MouseArea 状态更新不及时导致的光标闪烁
        hoverEnabled: false
 
        // 【关键修改】确保不窃取鼠标 Grab,让下层 MouseArea 的 drag 正常工作
        preventStealing: false
 
        onPressed: (mouse) => {
            mouse.accepted = false
            root.mPressed(mouse)
        }
        onReleased: (mouse) => {
            mouse.accepted = false
            root.mReleased(mouse)
        }
        onClicked: (mouse) => {
            mouse.accepted = false
            root.mClicked(mouse)
        }
        onDoubleClicked: (mouse) => {
            mouse.accepted = false
            root.mDoubleClicked(mouse)
        }
        // 【核心新增】显式透传 positionChanged,确保拖拽期间的 Move 事件能到达下层滑块
        onPositionChanged: (mouse) => {
            mouse.accepted = false
            root.mPositionChanged(mouse)
        }
    }
}

二、音频文件播放测试程序

说明,扫描bin下的MP3音频文件并循环播放,并且调试信息显示时间和文件名称

1.分成.h.cpp:

Mp3Player.pro
bash 复制代码
QT += core multimedia 
QT -= gui 

CONFIG += c++11 console 
CONFIG -= app_bundle 

TARGET = Mp3Player 
TEMPLATE = app 

MOC_DIR = temp/moc 
RCC_DIR = temp/rcc 
UI_DIR = temp/ui 
OBJECTS_DIR = temp/obj 
DESTDIR = $$PWD/bin 

# 明确列出三个源文件
SOURCES += \
    main.cpp \
    audioplayer.cpp

# 明确列出头文件
HEADERS += \
    audioplayer.h
main.cpp
cpp 复制代码
#include <QCoreApplication>
#include "audioplayer.h" // 引入我们的类头文件

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    
    AudioPlayer player;
    player.start();
    
    return a.exec();
}
audioplayer.cpp
cpp 复制代码
#include "audioplayer.h"
#include <QCoreApplication>
#include <QDir>
#include <QUrl>
#include <QDebug>

AudioPlayer::AudioPlayer(QObject *parent) : QObject(parent)
{
    m_player = new QMediaPlayer(this);
    m_playlist = new QMediaPlaylist(this);

    // ★ 设置播放模式为循环播放
    m_playlist->setPlaybackMode(QMediaPlaylist::Loop);
    m_player->setPlaylist(m_playlist);

    // 连接状态变化信号
    connect(m_player, &QMediaPlayer::currentMediaChanged, this, &AudioPlayer::onCurrentMediaChanged);

    // 使用定时器每秒输出一次播放时间进度
    m_progressTimer = new QTimer(this);
    m_progressTimer->setInterval(1000);
    connect(m_progressTimer, &QTimer::timeout, this, &AudioPlayer::printProgress);

    // 播放状态变化时控制定时器
    connect(m_player, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state){
        if (state == QMediaPlayer::PlayingState) {
            m_progressTimer->start();
        } else {
            m_progressTimer->stop();
        }
    });
}

void AudioPlayer::start()
{
    QDir dir(QCoreApplication::applicationDirPath());
    QStringList mp3Files = dir.entryList(QStringList() << "*.mp3", QDir::Files);

    if (mp3Files.isEmpty()) {
        qWarning() << "未找到MP3文件!请将mp3文件放入程序所在目录:" << dir.absolutePath();
        QTimer::singleShot(2000, [](){ QCoreApplication::quit(); });
        return;
    }

    qDebug() << "==== 扫描到 MP3 文件 ====";
    for (const QString &file : mp3Files) {  // 注意这里加了 const 修了之前的警告
        QUrl url = QUrl::fromLocalFile(dir.absoluteFilePath(file));
        m_playlist->addMedia(url);
        qDebug() << "添加:" << file;
    }
    qDebug() << "=========================";

    m_playlist->setCurrentIndex(0);
    m_player->play();
}

void AudioPlayer::onCurrentMediaChanged(const QMediaContent &media)
{
    if (!media.isNull()) {
        qDebug() << "\n🎵 切换播放文件:" << media.request().url().fileName();
    }
}

void AudioPlayer::printProgress()
{
    if (m_player->state() == QMediaPlayer::PlayingState) {
        qint64 position = m_player->position();
        qint64 duration = m_player->duration();
        if (duration > 0) {
            qDebug() << "正在播放:" << m_player->currentMedia().request().url().fileName()
                     << "| 时间:" << formatTime(position) << "/" << formatTime(duration);
        }
    }
}

QString AudioPlayer::formatTime(qint64 milliseconds)
{
    qint64 seconds = milliseconds / 1000;
    qint64 mins = seconds / 60;
    qint64 secs = seconds % 60;
    return QString("%1:%2")
            .arg(mins, 2, 10, QChar('0'))
            .arg(secs, 2, 10, QChar('0'));
}
audioplayer.h
cpp 复制代码
#ifndef AUDIOPLAYER_H
#define AUDIOPLAYER_H

#include <QObject>
#include <QMediaPlayer>
#include <QMediaPlaylist>
#include <QMediaContent>
#include <QTimer>

class AudioPlayer : public QObject
{
    Q_OBJECT

public:
    explicit AudioPlayer(QObject *parent = nullptr);

    // 扫描同目录下的 mp3 文件并开始播放
    void start();

private slots:
    void onCurrentMediaChanged(const QMediaContent &media);

private:
    void printProgress();
    QString formatTime(qint64 milliseconds);

private:
    QMediaPlayer *m_player;
    QMediaPlaylist *m_playlist;
    QTimer *m_progressTimer;
};

#endif // AUDIOPLAYER_H

2.极简测试:

#include "main.moc"

Mp3Player.pro
javascript 复制代码
QT += core multimedia
QT -= gui

CONFIG += c++11 console
CONFIG -= app_bundle

TARGET = Mp3Player
TEMPLATE = app

MOC_DIR     = temp/moc
RCC_DIR     = temp/rcc
UI_DIR      = temp/ui
OBJECTS_DIR = temp/obj

DESTDIR =$$PWD/bin

SOURCES += main.cpp
main.cpp
cpp 复制代码
#include <QCoreApplication>
#include <QMediaPlayer>
#include <QMediaPlaylist>
#include <QMediaContent>
#include <QDir>
#include <QUrl>
#include <QDebug>
#include <QTimer>

class AudioPlayer : public QObject
{
    Q_OBJECT
public:
    explicit AudioPlayer(QObject *parent = nullptr) : QObject(parent)
    {
        m_player = new QMediaPlayer(this);
        m_playlist = new QMediaPlaylist(this);

        // ★ 设置播放模式为循环播放(播完列表最后一个后重新回到第一个)
        m_playlist->setPlaybackMode(QMediaPlaylist::Loop);

        m_player->setPlaylist(m_playlist);

        // 连接状态变化信号,用于输出文件名
        connect(m_player, &QMediaPlayer::currentMediaChanged, this, &AudioPlayer::onCurrentMediaChanged);

        // 使用定时器每秒输出一次播放时间进度,避免控制台疯狂刷屏
        m_progressTimer = new QTimer(this);
        m_progressTimer->setInterval(1000);
        connect(m_progressTimer, &QTimer::timeout, this, &AudioPlayer::printProgress);
        
        // 播放状态变化时控制定时器
        connect(m_player, &QMediaPlayer::stateChanged, this, [this](QMediaPlayer::State state){
            if (state == QMediaPlayer::PlayingState) {
                m_progressTimer->start();
            } else {
                m_progressTimer->stop();
            }
        });
    }

    // 扫描同目录下的 mp3 文件并开始播放
    void start()
    {
        QDir dir(QCoreApplication::applicationDirPath());
        QStringList mp3Files = dir.entryList(QStringList() << "*.mp3", QDir::Files);

        if (mp3Files.isEmpty()) {
            qWarning() << "未找到MP3文件!请将mp3文件放入程序所在目录:" << dir.absolutePath();
            // 延迟2秒退出,保证控制台信息可见
            QTimer::singleShot(2000, [](){ QCoreApplication::quit(); });
            return;
        }

        qDebug() << "==== 扫描到 MP3 文件 ====";
        for (const QString &file : mp3Files) {
            QUrl url = QUrl::fromLocalFile(dir.absoluteFilePath(file));
            m_playlist->addMedia(url);
            qDebug() << "添加:" << file;
        }
        qDebug() << "=========================";

        // 设置从第一个文件开始播放
        m_playlist->setCurrentIndex(0);
        m_player->play();
    }

private slots:
    void onCurrentMediaChanged(const QMediaContent &media)
    {
        if (!media.isNull()) {
            qDebug() << "\n🎵 切换播放文件:" << media.request().url().fileName(); // 新接口
        }
    }



private:
    void printProgress()
    {
        if (m_player->state() == QMediaPlayer::PlayingState) {
            qint64 position = m_player->position();
            qint64 duration = m_player->duration();

            if (duration > 0) {
                qDebug() << "正在播放:" << m_player->currentMedia().request().url().fileName() // 新接口
                         << "| 时间:" << formatTime(position) << "/" << formatTime(duration);
            }
        }
    }


    // 格式化毫秒为 mm:ss 格式
    QString formatTime(qint64 milliseconds)
    {
        qint64 seconds = milliseconds / 1000;
        qint64 mins = seconds / 60;
        qint64 secs = seconds % 60;
        return QString("%1:%2")
            .arg(mins, 2, 10, QChar('0'))
            .arg(secs, 2, 10, QChar('0'));
    }

private:
    QMediaPlayer *m_player;
    QMediaPlaylist *m_playlist;
    QTimer *m_progressTimer;
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    AudioPlayer player;
    player.start();

    return a.exec();
}

#include "main.moc"
相关推荐
用户8055336980314 小时前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner14 小时前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz6 天前
QML Hello World 入门示例
qt
xcyxiner9 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner9 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner10 天前
DicomViewer (添加模型类)3
qt
xcyxiner10 天前
DicomViewer (目录调整) 2
qt
xcyxiner10 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
RTC实战笔记11 天前
Android 实时音视频接入教程:媒体补充增强信息(SEI)
音视频·媒体·rtc
潜创微科技12 天前
HDMI1.3 无线传输芯片方案 空旷 150 米量产级音视频方案
音视频