开源 C++ QT QML 开发(五)复杂控件--Gridview

文章的目的为了记录使用QT QML开发学习的经历。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。

相关链接:

开源 C++ QT QML 开发(一)基本介绍

开源 C++ QT QML 开发(二)工程结构

开源 C++ QT QML 开发(三)常用控件

开源 C++ QT QML 开发(四)复杂控件--Listview

开源 C++ QT QML 开发(五)复杂控件--Gridview

推荐链接:

开源 C# 快速开发(一)基础知识

开源 C# 快速开发(二)基础控件

开源 C# 快速开发(三)复杂控件

开源 C# 快速开发(四)自定义控件--波形图

开源 C# 快速开发(五)自定义控件--仪表盘

开源 C# 快速开发(六)自定义控件--圆环

开源 C# 快速开发(七)通讯--串口

开源 C# 快速开发(八)通讯--Tcp服务器端

开源 C# 快速开发(九)通讯--Tcp客户端

开源 C# 快速开发(十)通讯--http客户端

开源 C# 快速开发(十一)线程

开源 C# 快速开发(十二)进程监控

开源 C# 快速开发(十三)进程--管道通讯

开源 C# 快速开发(十四)进程--内存映射

开源 C# 快速开发(十五)进程--windows消息

开源 C# 快速开发(十六)数据库--sqlserver增删改查

本章节主要内容是:介绍复杂控件GridView 的使用方法,GridView 是一个用于显示网格布局数据的视图组件,非常适合显示图片库、图标集合等网格状内容。

1.代码分析

2.所有源码

3.效果演示

一、代码分析

  1. 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位小数)

  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()
    }
}

交互设计:

左键单击:选择图片

右键单击:快速预览

双击:打开预览窗口

悬停:缩放效果

  1. 数据流分析

信号-槽连接

复制代码
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();
}

三、效果演示

可以选择打开文件夹,也可以选择打开文件。

相关推荐
扶尔魔ocy3 小时前
【QT常用技术讲解】opencv实现指定分辨率打开摄像头
qt·opencv
扶尔魔ocy3 小时前
【QT常用技术讲解】multimedia实现指定分辨率打开摄像头
图像处理·qt
玖笙&3 小时前
✨WPF编程基础【2.1】布局原则
c++·wpf·visual studio
玖笙&3 小时前
✨WPF编程基础【2.2】:布局面板实战
c++·wpf·visual studio
无敌最俊朗@4 小时前
Qt 按钮点击事件全链路解析:从系统驱动到槽函数
开发语言·qt·计算机外设
说私域5 小时前
公域流量转化困境下开源AI智能名片与链动2+1模式的S2B2C商城小程序应用研究
人工智能·小程序·开源
aitav05 小时前
⚡ arm 32位嵌入式 Linux 系统移植 QT 程序
linux·arm开发·qt
菜鸡爱玩5 小时前
Qt3D--箭头示例
c++·qt
掘根5 小时前
【Qt】多线程
java·开发语言·qt