文章的目的为了记录使用QT QML开发学习的经历。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。
相关链接:
开源 C++ QT QML 开发(四)复杂控件--Listview
开源 C++ QT QML 开发(五)复杂控件--Gridview
推荐链接:
开源 C# 快速开发(十六)数据库--sqlserver增删改查
本章节主要内容是:介绍复杂控件GridView 的使用方法,GridView 是一个用于显示网格布局数据的视图组件,非常适合显示图片库、图标集合等网格状内容。
1.代码分析
2.所有源码
3.效果演示
一、代码分析
- C++ 后端 (ImageLoader 类)
类定义分析
class ImageLoader : public QObject
{
Q_OBJECT
Q_PROPERTY(QString currentFolder READ currentFolder NOTIFY currentFolderChanged)
继承自 QObject,支持 Qt 的元对象系统
Q_PROPERTY 声明了可在 QML 中访问的 currentFolder 属性
成员函数分析
loadImages(const QString &folderPath)
void ImageLoader::loadImages(const QString &folderPath)
{
QDir directory(folderPath);
// 检查文件夹是否存在
if (!directory.exists()) {
qWarning() << "文件夹不存在:" << folderPath;
return;
}
m_currentFolder = directory.dirName();
emit currentFolderChanged(m_currentFolder);
// 设置图片过滤器
QStringList imageFilters;
imageFilters << "*.jpg" << "*.jpeg" << "*.png" << "*.bmp" << "*.gif" << "*.webp";
// 获取文件列表并按名称排序
QFileInfoList fileList = directory.entryInfoList(imageFilters, QDir::Files, QDir::Name);
// 遍历所有图片文件
for (const QFileInfo &fileInfo : fileList) {
QString name = fileInfo.fileName();
QString path = fileInfo.absoluteFilePath();
QString size = formatFileSize(fileInfo.size());
QString date = fileInfo.lastModified().toString("yyyy-MM-dd hh:mm:ss");
emit imageFound(name, path, size, date);
}
}
功能:加载指定文件夹中的所有图片文件
流程:
验证文件夹存在性
更新当前文件夹属性并发出信号
设置支持的图片格式过滤器
获取排序后的文件列表
遍历文件,提取信息并发出信号
loadSingleImage(const QString &filePath)
void ImageLoader::loadSingleImage(const QString &filePath)
{
QFileInfo fileInfo(filePath);
if (!fileInfo.exists()) {
qWarning() << "文件不存在:" << filePath;
return;
}
m_currentFolder = "单个文件";
emit currentFolderChanged(m_currentFolder);
// 提取单个文件信息并发出信号
QString name = fileInfo.fileName();
QString path = fileInfo.absoluteFilePath();
QString size = formatFileSize(fileInfo.size());
QString date = fileInfo.lastModified().toString("yyyy-MM-dd hh:mm:ss");
emit imageFound(name, path, size, date);
}
功能:加载单个图片文件
特点:设置特殊文件夹名称"单个文件"
formatFileSize(qint64 bytes)
QString ImageLoader::formatFileSize(qint64 bytes)
{
if (bytes < 1024) {
return QString::number(bytes) + " B";
} else if (bytes < 1024 * 1024) {
return QString::number(bytes / 1024.0, 'f', 1) + " KB";
} else {
return QString::number(bytes / (1024.0 * 1024.0), 'f', 1) + " MB";
}
}
功能:将字节数转换为易读的文件大小格式
转换规则:
< 1024B:显示为 B
1024B ~ 1MB:显示为 KB(保留1位小数)
≥ 1MB:显示为 MB(保留1位小数)
- QML 前端分析
主要窗口组件
图片预览窗口 (imagePreviewWindow)
Window {
id: imagePreviewWindow
// 模态对话框,阻止主窗口交互
flags: Qt.Dialog
modality: Qt.ApplicationModal
}
功能:提供大图预览界面
特性:
模态对话框
支持上一张/下一张导航
异步图片加载
网格视图 (GridView)
GridView {
id: gridView
model: imageModel
cellWidth: 200
cellHeight: 220
// 关键属性:启用裁剪,防止内容溢出
clip: true
}
布局:网格布局显示图片缩略图
性能优化:只渲染可见区域的项
关键 JavaScript 函数
folderToString(url)
function folderToString(url) {
var path = url.toString()
// 移除 file:/// 前缀
if (path.startsWith("file:///")) {
path = path.substring(8)
}
// 路径格式标准化
path = path.replace(/\\/g, "/")
return path
}
功能:将 QUrl 转换为文件系统路径
处理逻辑:
移除 file:/// 协议前缀
统一路径分隔符为 /
showPreviousImage() 和 showNextImage()
function showNextImage() {
var currentIndex = -1
// 查找当前图片在模型中的索引
for (var i = 0; i < imageModel.count; i++) {
if (imageModel.get(i).path === imagePreviewWindow.imageSource.replace("file:///", "")) {
currentIndex = i
break
}
}
// 显示下一张图片
if (currentIndex < imageModel.count - 1) {
var nextImage = imageModel.get(currentIndex + 1)
imagePreviewWindow.imageSource = "file:///" + nextImage.path
imagePreviewWindow.imageName = nextImage.name
}
}
功能:实现图片导航
算法:
遍历模型查找当前图片索引
计算相邻索引
更新预览窗口内容
交互功能
图片项委托 (delegate)
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (mouse.button === Qt.LeftButton) {
// 左键:选择/取消选择
parent.selected = !parent.selected
} else if (mouse.button === Qt.RightButton) {
// 右键:快速预览
imagePreviewWindow.imageSource = "file:///" + model.path
imagePreviewWindow.imageName = model.name
imagePreviewWindow.show()
}
}
onDoubleClicked: {
// 双击:打开预览窗口
imagePreviewWindow.show()
}
}
交互设计:
左键单击:选择图片
右键单击:快速预览
双击:打开预览窗口
悬停:缩放效果
- 数据流分析
信号-槽连接
Connections {
target: imageLoader
onImageFound: {
imageModel.append({
name: name,
path: path,
size: size,
date: date
})
}
onCurrentFolderChanged: {
imageModel.clear() // 切换文件夹时清空模型
}
}
数据流程:
用户操作 → 打开文件夹/文件
C++ 处理 → ImageLoader 扫描文件系统
信号发射 → imageFound 携带文件信息
QML 响应 → 更新 ListModel
界面更新 → GridView 重新渲染
二、所有源码
总共有4个文件

main.qml文件源码
import QtQuick 2.14
import QtQuick.Window 2.14
import QtQuick.Controls 2.14
import QtQuick.Dialogs 1.3
import Qt.labs.platform 1.1
import QtQuick.Layouts 1.14
Window {
width: 1000
height: 700
visible: true
title: "图片浏览器"
// 图片模型
ListModel {
id: imageModel
}
// 图片预览窗口
Window {
id: imagePreviewWindow
width: 800
height: 600
title: "图片预览"
flags: Qt.Dialog
modality: Qt.ApplicationModal
visible: false
property string imageSource: ""
property string imageName: ""
Rectangle {
anchors.fill: parent
color: "#2c3e50"
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
// 标题
Text {
text: imagePreviewWindow.imageName
color: "white"
font.pixelSize: 18
font.bold: true
Layout.alignment: Qt.AlignHCenter
}
// 图片显示区域
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "#34495e"
radius: 5
Image {
id: previewImage
anchors.fill: parent
anchors.margins: 10
source: imagePreviewWindow.imageSource
fillMode: Image.PreserveAspectFit
sourceSize.width: 800
sourceSize.height: 600
// 加载中提示
BusyIndicator {
anchors.centerIn: parent
running: previewImage.status === Image.Loading
}
}
}
// 按钮区域
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 10
Button {
text: "上一张"
onClicked: showPreviousImage()
background: Rectangle {
color: "#3498db"
radius: 5
}
contentItem: Text {
text: parent.text
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
Button {
text: "关闭"
onClicked: imagePreviewWindow.close()
background: Rectangle {
color: "#e74c3c"
radius: 5
}
contentItem: Text {
text: parent.text
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
Button {
text: "下一张"
onClicked: showNextImage()
background: Rectangle {
color: "#3498db"
radius: 5
}
contentItem: Text {
text: parent.text
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
}
}
// 文件夹选择对话框
FolderDialog {
id: folderDialog
title: "选择图片文件夹"
onAccepted: {
// 将 QUrl 转换为字符串路径
var folderPath = folderToString(folderDialog.folder)
console.log("选择的文件夹路径:", folderPath)
imageLoader.loadImages(folderPath)
}
}
// 文件选择对话框(单文件)- 使用 QtQuick.Dialogs 的 FileDialog
// 替代方案:使用 Qt.labs.platform 的 FileDialog
FileDialog {
id: fileDialog
title: "选择图片文件"
nameFilters: ["图片文件 (*.jpg *.jpeg *.png *.bmp *.gif *.webp)"]
onAccepted: {
var filePath = folderToString(file)
console.log("选择的文件路径:", filePath)
imageLoader.loadSingleImage(filePath)
}
}
// 函数:将 QUrl 转换为文件路径字符串
function folderToString(url) {
var path = url.toString()
// 移除 file:/// 前缀
if (path.startsWith("file:///")) {
path = path.substring(8)
}
// 在 Windows 上,可能需要处理额外的斜杠
path = path.replace(/\\/g, "/")
return path
}
// 函数:显示上一张图片
function showPreviousImage() {
var currentIndex = -1
for (var i = 0; i < imageModel.count; i++) {
if (imageModel.get(i).path === imagePreviewWindow.imageSource.replace("file:///", "")) {
currentIndex = i
break
}
}
if (currentIndex > 0) {
var prevImage = imageModel.get(currentIndex - 1)
imagePreviewWindow.imageSource = "file:///" + prevImage.path
imagePreviewWindow.imageName = prevImage.name
}
}
// 函数:显示下一张图片
function showNextImage() {
var currentIndex = -1
for (var i = 0; i < imageModel.count; i++) {
if (imageModel.get(i).path === imagePreviewWindow.imageSource.replace("file:///", "")) {
currentIndex = i
break
}
}
if (currentIndex < imageModel.count - 1) {
var nextImage = imageModel.get(currentIndex + 1)
imagePreviewWindow.imageSource = "file:///" + nextImage.path
imagePreviewWindow.imageName = nextImage.name
}
}
Rectangle {
anchors.fill: parent
color: "#2c3e50"
// 顶部工具栏
Rectangle {
id: toolbar
width: parent.width
height: 60
color: "#34495e"
Row {
anchors.centerIn: parent
spacing: 15
Button {
text: "打开文件夹"
onClicked: folderDialog.open()
background: Rectangle {
color: "#3498db"
radius: 5
}
contentItem: Text {
text: parent.text
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: 14
}
}
Button {
text: "打开文件"
onClicked: fileDialog.open()
background: Rectangle {
color: "#9b59b6"
radius: 5
}
contentItem: Text {
text: parent.text
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: 14
}
}
Button {
text: "清除所有"
onClicked: imageModel.clear()
background: Rectangle {
color: "#e74c3c"
radius: 5
}
contentItem: Text {
text: parent.text
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: 14
}
}
Rectangle {
width: 1
height: 30
color: "#7f8c8d"
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "图片数量: " + imageModel.count
color: "white"
font.pixelSize: 14
font.bold: true
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "当前文件夹: " + (imageLoader ? imageLoader.currentFolder : "未选择")
color: "#bdc3c7"
font.pixelSize: 12
anchors.verticalCenter: parent.verticalCenter
}
}
}
// 图片网格视图
GridView {
id: gridView
anchors {
top: toolbar.bottom
left: parent.left
right: parent.right
bottom: parent.bottom
margins: 15
}
model: imageModel
cellWidth: 200
cellHeight: 220
clip: true
delegate: Rectangle {
width: gridView.cellWidth - 10
height: gridView.cellHeight - 10
color: "#34495e"
radius: 8
border.color: selected ? "#3498db" : "#7f8c8d"
border.width: selected ? 3 : 1
property bool selected: false
Column {
anchors.fill: parent
anchors.margins: 8
spacing: 8
// 图片显示
Rectangle {
width: parent.width
height: parent.height - 50
color: "#2c3e50"
radius: 5
Image {
id: img
anchors.fill: parent
anchors.margins: 3
source: "file:///" + model.path
fillMode: Image.PreserveAspectFit
sourceSize.width: 180
sourceSize.height: 150
asynchronous: true
// 图片加载失败时显示错误图标
onStatusChanged: {
if (status === Image.Error) {
console.log("图片加载失败:", model.path)
}
}
}
// 加载中提示
BusyIndicator {
anchors.centerIn: parent
running: img.status === Image.Loading
width: 25
height: 25
}
}
// 图片信息
Column {
width: parent.width
spacing: 3
Text {
text: model.name
color: "white"
font.pixelSize: 12
font.bold: true
elide: Text.ElideRight
width: parent.width
}
Text {
text: model.size
color: "#bdc3c7"
font.pixelSize: 10
width: parent.width
}
Text {
text: model.date
color: "#95a5a6"
font.pixelSize: 9
width: parent.width
}
}
}
// 鼠标交互区域
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (mouse.button === Qt.LeftButton) {
// 左键选中/取消选中图片
parent.selected = !parent.selected
console.log("选中图片: " + model.name)
} else if (mouse.button === Qt.RightButton) {
// 右键快速预览
imagePreviewWindow.imageSource = "file:///" + model.path
imagePreviewWindow.imageName = model.name
imagePreviewWindow.show()
}
}
onDoubleClicked: {
// 双击查看大图
imagePreviewWindow.imageSource = "file:///" + model.path
imagePreviewWindow.imageName = model.name
imagePreviewWindow.show()
}
onEntered: {
parent.scale = 1.03
parent.border.color = "#3498db"
}
onExited: {
parent.scale = 1.0
if (!parent.selected) {
parent.border.color = "#7f8c8d"
}
}
}
// 缩放动画
Behavior on scale {
NumberAnimation { duration: 150 }
}
// 选中标记
Rectangle {
visible: selected
width: 22
height: 22
radius: 11
color: "#3498db"
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: 6
Text {
anchors.centerIn: parent
text: "✓"
color: "white"
font.bold: true
font.pixelSize: 12
}
}
}
// 空状态提示
Label {
anchors.centerIn: parent
text: "暂无图片\n请点击上方按钮打开文件夹或选择图片文件"
color: "white"
font.pixelSize: 16
horizontalAlignment: Text.AlignHCenter
visible: imageModel.count === 0
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
background: Rectangle {
color: "#34495e"
}
contentItem: Rectangle {
color: "#3498db"
radius: 3
}
}
}
}
// 连接 C++ 信号到 QML
Connections {
target: imageLoader
onImageFound: {
imageModel.append({
name: name,
path: path,
size: size,
date: date
})
}
onCurrentFolderChanged: {
// 清除旧图片
imageModel.clear()
}
}
// 初始化时显示欢迎信息
Component.onCompleted: {
console.log("图片浏览器已启动")
}
}
ImageLoader.h文件源码
#ifndef IMAGELOADER_H
#define IMAGELOADER_H
#include <QObject>
#include <QString>
#include <QDir>
#include <QFileInfo>
#include <QFileInfoList>
#include <QDateTime>
#include <QUrl>
#include <QDebug>
class ImageLoader : public QObject
{
Q_OBJECT
Q_PROPERTY(QString currentFolder READ currentFolder NOTIFY currentFolderChanged)
public:
explicit ImageLoader(QObject *parent = nullptr) : QObject(parent) {}
QString currentFolder() const { return m_currentFolder; }
Q_INVOKABLE void loadImages(const QString &folderPath);
Q_INVOKABLE void loadSingleImage(const QString &filePath);
signals:
void imageFound(const QString &name, const QString &path, const QString &size, const QString &date);
void currentFolderChanged(const QString &folder);
private:
QString m_currentFolder;
QString formatFileSize(qint64 bytes);
};
#endif // IMAGELOADER_H
ImageLoader.cpp文件源码
#include "ImageLoader.h"
void ImageLoader::loadImages(const QString &folderPath)
{
QDir directory(folderPath);
if (!directory.exists()) {
qWarning() << "文件夹不存在:" << folderPath;
return;
}
m_currentFolder = directory.dirName();
emit currentFolderChanged(m_currentFolder);
// 支持的图片格式
QStringList imageFilters;
imageFilters << "*.jpg" << "*.jpeg" << "*.png" << "*.bmp" << "*.gif" << "*.webp";
QFileInfoList fileList = directory.entryInfoList(imageFilters, QDir::Files, QDir::Name);
qDebug() << "在文件夹中找到图片文件数量:" << fileList.count() << folderPath;
for (const QFileInfo &fileInfo : fileList) {
QString name = fileInfo.fileName();
QString path = fileInfo.absoluteFilePath();
QString size = formatFileSize(fileInfo.size());
QString date = fileInfo.lastModified().toString("yyyy-MM-dd hh:mm:ss");
qDebug() << "找到图片:" << name << "路径:" << path;
emit imageFound(name, path, size, date);
}
}
void ImageLoader::loadSingleImage(const QString &filePath)
{
QFileInfo fileInfo(filePath);
if (!fileInfo.exists()) {
qWarning() << "文件不存在:" << filePath;
return;
}
m_currentFolder = "单个文件";
emit currentFolderChanged(m_currentFolder);
QString name = fileInfo.fileName();
QString path = fileInfo.absoluteFilePath();
QString size = formatFileSize(fileInfo.size());
QString date = fileInfo.lastModified().toString("yyyy-MM-dd hh:mm:ss");
emit imageFound(name, path, size, date);
}
QString ImageLoader::formatFileSize(qint64 bytes)
{
if (bytes < 1024) {
return QString::number(bytes) + " B";
} else if (bytes < 1024 * 1024) {
return QString::number(bytes / 1024.0, 'f', 1) + " KB";
} else {
return QString::number(bytes / (1024.0 * 1024.0), 'f', 1) + " MB";
}
}
main.cpp文件源码
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "ImageLoader.h"
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
// 设置应用程序信息
app.setApplicationName("图片浏览器");
app.setApplicationVersion("1.0");
QQmlApplicationEngine engine;
// 注册 C++ 类到 QML
ImageLoader imageLoader;
engine.rootContext()->setContextProperty("imageLoader", &imageLoader);
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();
}
三、效果演示
可以选择打开文件夹,也可以选择打开文件。
