文章的目的为了记录使用QT QML开发学习的经历。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。
相关链接:
开源 C++ QT QML 开发(四)复杂控件--Listview
开源 C++ QT QML 开发(五)复杂控件--Gridview
推荐链接:
开源 C# 快速开发(十六)数据库--sqlserver增删改查
本章节主要内容是:使用qml编写了媒体播放器,使用了K-Lite Codec Pack Mega作为解码包,实现mp4的播放。
1.代码分析
2.所有源码
3.效果演示
一、代码分析1. 主窗口和属性定义
ApplicationWindow {
id: mainWindow
width: 1024
height: 720
visible: true
title: "MP4播放器 - 完全兼容版"
minimumWidth: 800
minimumHeight: 600
property string currentVideo: "" // 当前视频文件路径
property bool hasVideo: videoPlayer.hasVideo // 绑定到Video组件的hasVideo属性
property bool isSeekable: videoPlayer.seekable // 是否支持跳转
property string currentStatus: "等待选择文件" // 当前状态显示文本
}
- 核心视频播放组件
Video组件及其信号处理
Video {
id: videoPlayer
anchors.fill: parent
anchors.margins: 2
source: currentVideo // 绑定到currentVideo属性
fillMode: VideoOutput.PreserveAspectFit // 保持宽高比
volume: volumeSlider.value // 绑定音量滑块
autoPlay: true // 自动播放
// 状态变化信号处理
onStatusChanged: {
console.log("视频状态变化:", status)
updateStatusDisplay() // 更新状态显示
}
// 播放状态变化信号处理
onPlaybackStateChanged: {
console.log("播放状态变化:", playbackState)
updatePlayButton() // 更新播放按钮文本
}
// 视频时长变化信号处理
onDurationChanged: {
console.log("视频时长:", duration, "ms")
progressSlider.to = Math.max(1, duration) // 设置进度条最大值
}
// 播放位置变化信号处理
onPositionChanged: {
if (!progressSlider.pressed) { // 避免拖动时冲突
progressSlider.value = position // 更新进度条位置
}
}
}
- 主要控制函数
播放/暂停切换函数
function togglePlayPause() {
if (!hasVideo || currentVideo === "") return // 安全检查
if (videoPlayer.playbackState === MediaPlayer.PlayingState) {
videoPlayer.pause() // 如果正在播放,则暂停
} else {
videoPlayer.play() // 否则开始播放
}
}
状态显示更新函数
function updateStatusDisplay() {
var status = videoPlayer.status // 获取当前视频状态
switch(status) {
case MediaPlayer.NoMedia:
currentStatus = "无媒体文件"
break
case MediaPlayer.Loading:
currentStatus = "加载中..."
break
case MediaPlayer.Loaded:
currentStatus = "已加载"
break
case MediaPlayer.Stalling:
currentStatus = "缓冲中..."
break
case MediaPlayer.Buffering:
currentStatus = "缓冲中"
break
case MediaPlayer.Buffered:
currentStatus = "就绪"
break
case MediaPlayer.EndOfMedia:
currentStatus = "播放结束"
break
case MediaPlayer.InvalidMedia:
currentStatus = "格式不支持"
break
case MediaPlayer.UnknownStatus:
currentStatus = "未知状态"
break
default:
currentStatus = "就绪"
}
console.log("状态更新:", currentStatus)
}
播放按钮更新函数
function updatePlayButton() {
switch(videoPlayer.playbackState) {
case MediaPlayer.PlayingState:
playButton.text = "⏸️ 暂停" // 播放中显示暂停
break
case MediaPlayer.PausedState:
playButton.text = "▶️ 继续" // 暂停中显示继续
break
default:
playButton.text = "▶️ 播放" // 默认显示播放
}
}
- UI状态显示函数
状态图标获取函数
function getStatusIcon() {
if (currentVideo === "") return "📁" // 无文件
if (videoPlayer.status === MediaPlayer.InvalidMedia) return "❌" // 格式错误
if (videoPlayer.status === MediaPlayer.Loading) return "⏳" // 加载中
if (!hasVideo) return "🎬" // 无视频流
return "✅" // 正常状态
}
状态消息获取函数
function getStatusMessage() {
if (currentVideo === "") return "请选择视频文件开始播放" // 初始提示
if (videoPlayer.status === MediaPlayer.InvalidMedia) return "无法播放此文件" // 错误提示
if (videoPlayer.status === MediaPlayer.Loading) return "正在加载..." // 加载提示
if (!hasVideo) return "准备播放" // 准备状态
return "正在播放: " + getFileName(currentVideo) // 播放状态显示文件名
}
错误详情获取函数
function getErrorDetails() {
if (videoPlayer.status === MediaPlayer.InvalidMedia) {
return "此文件需要H.264解码器\n请安装K-Lite Codec Pack\n下载: https://codecguide.com/download_kl.htm"
}
return "" // 无错误时返回空字符串
}
- 工具函数
状态颜色获取函数
function getStatusColor() {
if (videoPlayer.status === MediaPlayer.InvalidMedia) return "#e74c3c" // 红色-错误
if (videoPlayer.status === MediaPlayer.Buffered) return "#2ecc71" // 绿色-就绪
if (videoPlayer.status === MediaPlayer.Loading) return "#f39c12" // 橙色-加载中
return "#3498db" // 蓝色-默认
}
文件名提取函数
function getFileName(filePath) {
var path = filePath.toString() // 转换为字符串
var fileName = path.split('/').pop() || path.split('\\').pop() // 从路径中提取文件名
return fileName || "未知文件" // 返回文件名或默认值
}
时间格式化函数
function formatTime(milliseconds) {
if (!milliseconds || isNaN(milliseconds)) return "00:00" // 无效时间处理
var totalSeconds = Math.floor(milliseconds / 1000) // 转换为秒
var hours = Math.floor(totalSeconds / 3600) // 计算小时
var minutes = Math.floor((totalSeconds % 3600) / 60) // 计算分钟
var seconds = totalSeconds % 60 // 计算秒数
if (hours > 0) {
// 有时长格式:HH:MM:SS
return hours.toString().padStart(2, '0') + ":" +
minutes.toString().padStart(2, '0') + ":" +
seconds.toString().padStart(2, '0')
} else {
// 无时长格式:MM:SS
return minutes.toString().padStart(2, '0') + ":" +
seconds.toString().padStart(2, '0')
}
}
- 进度条控制函数
进度条拖动处理
Slider {
id: progressSlider
// ... 其他属性
onMoved: {
if (isSeekable) { // 检查是否支持跳转
videoPlayer.seek(value) // 跳转到指定位置
}
}
}
- C++主程序函数
环境设置函数
int main(int argc, char *argv[])
{
// 关键环境变量设置
qputenv("QT_MEDIA_BACKEND", "windows"); // 强制使用Windows媒体后端
qputenv("QT_LOGGING_RULES", "qt.multimedia.*=true"); // 启用多媒体调试日志
qputenv("MF_DEBUG", "1"); // 启用Media Foundation调试
// 应用程序属性设置
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); // 高DPI支持
QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL); // 软件渲染
QGuiApplication app(argc, argv);
// 应用程序信息
app.setApplicationName("MP4播放器");
app.setApplicationVersion("1.0");
app.setOrganizationName("MyCompany");
}
系统诊断函数
// 详细的多媒体支持信息
qDebug() << "=== 多媒体系统诊断信息 ===";
qDebug() << "应用程序目录:" << QDir::currentPath();
qDebug() << "临时目录:" << QStandardPaths::writableLocation(QStandardPaths::TempLocation);
qDebug() << "支持的MIME类型数量:" << QMediaPlayer::supportedMimeTypes().count();
// 输出支持的格式
auto mimeTypes = QMediaPlayer::supportedMimeTypes();
for (int i = 0; i < qMin(10, mimeTypes.count()); ++i) {
qDebug() << "支持格式:" << mimeTypes[i];
}
-
文件对话框处理
FileDialog {
id: fileDialog
title: "选择视频文件"
nameFilters: [ /* 文件过滤器列表 */ ]onAccepted: { currentVideo = fileDialog.fileUrl // 获取选择的文件URL console.log("加载文件:", currentVideo) // 调试日志 }
}
二、所有源码
1.安装正确的解码器包
推荐方案:K-Lite Codec Pack Mega
下载地址:https://codecguide.com/download_kl.htm
选择版本:下载 Mega 版本(包含所有解码器)
安装步骤:运行安装程序
2.pro文件
QT += quick quickcontrols2 multimedia multimediawidgets
3.main.qml文件源码
import QtQuick 2.14
import QtQuick.Window 2.14
import QtMultimedia 5.14
import QtQuick.Controls 2.14
import QtQuick.Dialogs 1.3
import QtQuick.Layouts 1.14
ApplicationWindow {
id: mainWindow
width: 1024
height: 720
visible: true
title: "MP4播放器 - 完全兼容版"
minimumWidth: 800
minimumHeight: 600
property string currentVideo: ""
property bool hasVideo: videoPlayer.hasVideo
property bool isSeekable: videoPlayer.seekable
property string currentStatus: "等待选择文件"
// 背景渐变
Rectangle {
anchors.fill: parent
gradient: Gradient {
GradientStop { position: 0.0; color: "#2c3e50" }
GradientStop { position: 1.0; color: "#3498db" }
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
// 标题栏
Rectangle {
Layout.fillWidth: true
height: 60
color: "transparent"
RowLayout {
anchors.fill: parent
Text {
text: "🎬 MP4播放器"
color: "white"
font.pixelSize: 24
font.bold: true
}
Item { Layout.fillWidth: true }
Text {
text: "Powered by Qt5.14"
color: "lightgray"
font.pixelSize: 12
}
}
}
// 视频显示区域
Rectangle {
id: videoContainer
Layout.fillWidth: true
Layout.fillHeight: true
color: "black"
radius: 8
clip: true
Video {
id: videoPlayer
anchors.fill: parent
anchors.margins: 2
source: currentVideo
fillMode: VideoOutput.PreserveAspectFit
volume: volumeSlider.value
autoPlay: true
// 状态变化处理 - 使用正确的信号
onStatusChanged: {
console.log("视频状态变化:", status)
updateStatusDisplay()
}
onPlaybackStateChanged: {
console.log("播放状态变化:", playbackState)
updatePlayButton()
}
onDurationChanged: {
console.log("视频时长:", duration, "ms")
progressSlider.to = Math.max(1, duration)
}
onPositionChanged: {
if (!progressSlider.pressed) {
progressSlider.value = position
}
}
}
// 加载指示器
BusyIndicator {
id: loadingIndicator
anchors.centerIn: parent
running: videoPlayer.status === MediaPlayer.Loading ||
videoPlayer.status === MediaPlayer.Stalling ||
videoPlayer.status === MediaPlayer.Buffering
visible: running
}
// 居中状态信息
Column {
id: centerInfo
anchors.centerIn: parent
spacing: 10
visible: currentVideo === "" || videoPlayer.status === MediaPlayer.InvalidMedia || !hasVideo
Text {
id: statusIcon
anchors.horizontalCenter: parent.horizontalCenter
color: "white"
font.pixelSize: 48
text: getStatusIcon()
}
Text {
id: statusMessage
anchors.horizontalCenter: parent.horizontalCenter
color: "white"
font.pixelSize: 18
text: getStatusMessage()
horizontalAlignment: Text.AlignHCenter
}
Text {
id: errorDetails
anchors.horizontalCenter: parent.horizontalCenter
color: "yellow"
font.pixelSize: 12
text: getErrorDetails()
visible: text !== ""
horizontalAlignment: Text.AlignHCenter
width: 400
wrapMode: Text.WordWrap
}
}
// 点击控制播放/暂停
MouseArea {
anchors.fill: parent
onClicked: togglePlayPause()
enabled: hasVideo && currentVideo !== ""
}
}
// 进度控制区域
ColumnLayout {
Layout.fillWidth: true
spacing: 5
// 进度条
Slider {
id: progressSlider
Layout.fillWidth: true
from: 0
to: 100
value: 0
enabled: isSeekable && hasVideo
onMoved: {
if (isSeekable) {
videoPlayer.seek(value)
}
}
// 自定义样式
background: Rectangle {
implicitHeight: 6
color: "#5a5a5a"
radius: 3
}
handle: Rectangle {
x: progressSlider.leftPadding + progressSlider.visualPosition * (progressSlider.availableWidth - width)
y: progressSlider.topPadding + progressSlider.availableHeight / 2 - height / 2
implicitWidth: 16
implicitHeight: 16
radius: 8
color: progressSlider.pressed ? "#f0f0f0" : "#ffffff"
border.color: "#bdbebf"
}
}
// 时间信息
RowLayout {
Layout.fillWidth: true
Text {
color: "white"
text: formatTime(videoPlayer.position)
font.pixelSize: 12
}
Item { Layout.fillWidth: true }
Text {
color: "white"
text: formatTime(videoPlayer.duration)
font.pixelSize: 12
}
}
}
// 控制按钮区域
Rectangle {
Layout.fillWidth: true
height: 70
color: "transparent"
RowLayout {
anchors.fill: parent
spacing: 10
// 文件操作按钮
Button {
text: "📁 打开文件"
onClicked: fileDialog.open()
background: Rectangle {
color: "#27ae60"
radius: 5
}
contentItem: Text {
text: parent.text
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.bold: true
}
}
// 播放控制按钮
Button {
id: playButton
text: "▶️ 播放"
onClicked: togglePlayPause()
enabled: hasVideo && currentVideo !== ""
background: Rectangle {
color: enabled ? "#2980b9" : "#7f8c8d"
radius: 5
}
contentItem: Text {
text: parent.text
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.bold: true
}
}
Button {
text: "⏹️ 停止"
onClicked: {
videoPlayer.stop()
progressSlider.value = 0
}
enabled: hasVideo && currentVideo !== ""
background: Rectangle {
color: enabled ? "#e74c3c" : "#7f8c8d"
radius: 5
}
contentItem: Text {
text: parent.text
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
Item { Layout.fillWidth: true }
// 音量控制
RowLayout {
spacing: 5
Text {
text: "🔊"
color: "white"
font.pixelSize: 16
}
Slider {
id: volumeSlider
from: 0
to: 1
value: 0.8
stepSize: 0.1
Layout.preferredWidth: 100
background: Rectangle {
implicitHeight: 4
color: "#5a5a5a"
radius: 2
}
}
Text {
color: "white"
text: Math.round(volumeSlider.value * 100) + "%"
font.pixelSize: 12
Layout.preferredWidth: 40
}
}
}
}
// 状态信息栏
Rectangle {
Layout.fillWidth: true
height: 30
color: "#34495e"
radius: 4
RowLayout {
anchors.fill: parent
anchors.margins: 5
Text {
color: "white"
text: "文件: " + (currentVideo ? getFileName(currentVideo) : "未选择")
font.pixelSize: 11
elide: Text.ElideMiddle
Layout.fillWidth: true
}
Text {
color: getStatusColor()
text: currentStatus
font.pixelSize: 11
font.bold: true
}
Text {
color: hasVideo ? "lightgreen" : "red"
text: hasVideo ? "🎥 有视频" : "❌ 无视频"
font.pixelSize: 11
}
}
}
}
FileDialog {
id: fileDialog
title: "选择视频文件"
nameFilters: [
"MP4文件 (*.mp4)",
"AVI文件 (*.avi)",
"MKV文件 (*.mkv)",
"MOV文件 (*.mov)",
"WMV文件 (*.wmv)",
"所有文件 (*.*)"
]
onAccepted: {
currentVideo = fileDialog.fileUrl
console.log("加载文件:", currentVideo)
}
}
// 功能函数
function togglePlayPause() {
if (!hasVideo || currentVideo === "") return
if (videoPlayer.playbackState === MediaPlayer.PlayingState) {
videoPlayer.pause()
} else {
videoPlayer.play()
}
}
function updateStatusDisplay() {
var status = videoPlayer.status
switch(status) {
case MediaPlayer.NoMedia:
currentStatus = "无媒体文件"
break
case MediaPlayer.Loading:
currentStatus = "加载中..."
break
case MediaPlayer.Loaded:
currentStatus = "已加载"
break
case MediaPlayer.Stalling:
currentStatus = "缓冲中..."
break
case MediaPlayer.Buffering:
currentStatus = "缓冲中"
break
case MediaPlayer.Buffered:
currentStatus = "就绪"
break
case MediaPlayer.EndOfMedia:
currentStatus = "播放结束"
break
case MediaPlayer.InvalidMedia:
currentStatus = "格式不支持"
break
case MediaPlayer.UnknownStatus:
currentStatus = "未知状态"
break
default:
currentStatus = "就绪"
}
console.log("状态更新:", currentStatus)
}
function updatePlayButton() {
switch(videoPlayer.playbackState) {
case MediaPlayer.PlayingState:
playButton.text = "⏸️ 暂停"
break
case MediaPlayer.PausedState:
playButton.text = "▶️ 继续"
break
default:
playButton.text = "▶️ 播放"
}
}
function getStatusIcon() {
if (currentVideo === "") return "📁"
if (videoPlayer.status === MediaPlayer.InvalidMedia) return "❌"
if (videoPlayer.status === MediaPlayer.Loading) return "⏳"
if (!hasVideo) return "🎬"
return "✅"
}
function getStatusMessage() {
if (currentVideo === "") return "请选择视频文件开始播放"
if (videoPlayer.status === MediaPlayer.InvalidMedia) return "无法播放此文件"
if (videoPlayer.status === MediaPlayer.Loading) return "正在加载..."
if (!hasVideo) return "准备播放"
return "正在播放: " + getFileName(currentVideo)
}
function getErrorDetails() {
if (videoPlayer.status === MediaPlayer.InvalidMedia) {
return "此文件需要H.264解码器\n请安装K-Lite Codec Pack\n下载: https://codecguide.com/download_kl.htm"
}
return ""
}
function getStatusColor() {
if (videoPlayer.status === MediaPlayer.InvalidMedia) return "#e74c3c"
if (videoPlayer.status === MediaPlayer.Buffered) return "#2ecc71"
if (videoPlayer.status === MediaPlayer.Loading) return "#f39c12"
return "#3498db"
}
function getFileName(filePath) {
var path = filePath.toString()
var fileName = path.split('/').pop() || path.split('\\').pop()
return fileName || "未知文件"
}
function formatTime(milliseconds) {
if (!milliseconds || isNaN(milliseconds)) return "00:00"
var totalSeconds = Math.floor(milliseconds / 1000)
var hours = Math.floor(totalSeconds / 3600)
var minutes = Math.floor((totalSeconds % 3600) / 60)
var seconds = totalSeconds % 60
if (hours > 0) {
return hours.toString().padStart(2, '0') + ":" +
minutes.toString().padStart(2, '0') + ":" +
seconds.toString().padStart(2, '0')
} else {
return minutes.toString().padStart(2, '0') + ":" +
seconds.toString().padStart(2, '0')
}
}
Component.onCompleted: {
console.log("播放器初始化完成")
console.log("视频输出支持:", videoPlayer.availability)
console.log("是否有视频:", videoPlayer.hasVideo)
console.log("是否可跳转:", videoPlayer.seekable)
}
}
-
main.cpp文件源码
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QMediaPlayer>
#include <QDebug>
#include <QDir>
#include <QStandardPaths>int main(int argc, char argv[])
{
// 设置环境变量 - 在创建QGuiApplication之前
qputenv("QT_MEDIA_BACKEND", "windows");
qputenv("QT_LOGGING_RULES", "qt.multimedia.=true");
qputenv("MF_DEBUG", "1");QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL); QGuiApplication app(argc, argv); // 设置应用程序信息 app.setApplicationName("MP4播放器"); app.setApplicationVersion("1.0"); app.setOrganizationName("MyCompany"); // 详细的多媒体支持信息 qDebug() << "=== 多媒体系统诊断信息 ==="; qDebug() << "应用程序目录:" << QDir::currentPath(); qDebug() << "临时目录:" << QStandardPaths::writableLocation(QStandardPaths::TempLocation); qDebug() << "支持的MIME类型数量:" << QMediaPlayer::supportedMimeTypes().count(); // 输出前10个支持的格式 auto mimeTypes = QMediaPlayer::supportedMimeTypes(); for (int i = 0; i < qMin(10, mimeTypes.count()); ++i) { qDebug() << "支持格式:" << mimeTypes[i]; } qDebug() << "环境变量:"; qDebug() << "QT_MEDIA_BACKEND =" << qgetenv("QT_MEDIA_BACKEND"); qDebug() << "PATH =" << qgetenv("PATH"); QQmlApplicationEngine engine; // 连接加载失败信号 QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, &app, [](QObject *obj, const QUrl &objUrl) { if (!obj) { qCritical() << "QML加载失败:" << objUrl; QCoreApplication::exit(-1); } else { qDebug() << "QML加载成功"; } }, Qt::QueuedConnection); engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); return app.exec();
}
三、效果演示
选择MP4文件,进行播放,可以拖动和暂停。需要注意的是必须要安装解码器才行。
