QML用ListView实现带section的GridView

QML自带的GridView只能定义delegate,没有section,类似手机相册带时间分组标签的样式就没法做。最简单的方式就是组合ListView+GridView,或者ListView+Flow,但是嵌套View时,子级View一般是完全展开的,只显示该分组几行就得把该分组全部加载了,这样就没有了View在需要时才实例化Item的优势,所以最好还是在单层View实现最终效果。

QML的ListView支持section,可以自定义分组样式,所以可以通过ListView来实现带section的GridView。当然,你也可以直接修改GridView的C++源码给他加上section。

ListView实现GridView的效果无非就是把多行显示到一行。可以让ListView某一行撑高,其他行高度为0;也可以平均分配一行高度。因为delegate会被ListView控制位置,所以相对位置可以在内部嵌套然后设置偏移量,使之看起来在一行上。

本文完整代码:

https://github.com/gongjianbo/MyTestCode/tree/master/Qml/TestQml_20240205_SectionGrid

先实现一个不带section的GridView:

import QtQuick 2.15
import QtQuick.Controls 2.15

// ListView 实现 GridView 效果
Rectangle {
    id: control

    border.color: "black"

    // 边距
    property int padding: 10
    // Item 间隔
    property int spacing: 10
    // Item 宽
    property int itemWidth: 300
    // Item 高
    property int itemHeight: 100
    // Delegate 宽
    property int delegateWidth: itemWidth + spacing
    // Delegate 高
    property int delegateHeight: itemHeight + spacing
    // 列数根据可视宽度和 Item 宽度计算
    property int columns: (list_view.width + spacing - padding) / delegateWidth < 1
                          ? 1
                          : (list_view.width + spacing - padding) / delegateWidth

    // 套一层 Item clip 剪去 ListView 尾巴上多余的部分不显示出来
    Item {
        anchors.fill: parent
        anchors.margins: control.padding
        // 右侧留下滚动条位置,所以 columns 里 list_view.width 要减一个 padding
        anchors.rightMargin: 0
        clip: true

        ListView {
            id: list_view
            width: parent.width
            // 高度多一个 delegate 放置 footer,防止末尾的一行滑倒底部后隐藏
            // 多出来的一部分会被外部 Item clip 掉
            height: parent.height + control.delegateHeight + control.spacing
            flickableDirection: Flickable.HorizontalAndVerticalFlick
            boundsBehavior: Flickable.StopAtBounds
            headerPositioning: ListView.OverlayHeader
            // 底部多一个 footer 撑高可显示范围,防止末尾的一行滑倒底部后隐藏
            footerPositioning: ListView.OverlayFooter
            ScrollBar.vertical: ScrollBar {
                // padding 加上 ListView 多出来的一部分
                bottomPadding: padding + (control.delegateHeight + control.spacing)
                // 常驻显示只是方便调试
                policy: ScrollBar.AlwaysOn
            }
            footer: Item {
                // 竖向的 ListView 宽度无所谓
                width: control.delegateWidth
                // 高度大于等于 delegate 高度才能保证显示
                height: control.delegateHeight
            }
            // 奇数方便测试
            model: 31
            delegate: Item {
                width: control.delegateWidth
                // 每行第一个 Item 有高度,后面的没高度,这样就能排列到一行
                // 因为 0 高度 Item 在末尾,超出范围 visible 就置为 false 了,所以才需要 footer 撑高多显示一行的内容
                // delegate 高度不一致会导致滚动条滚动时长度变化
                height: (model.index % control.columns === 0) ? control.delegateHeight : 0
                // 放置真正的内容
                Rectangle {
                    // 根据列号计算 x
                    x: (model.index % control.columns) * control.delegateWidth
                    // 负高度就能和每行第一个的 y 一样
                    y: (model.index % control.columns !== 0) ? -control.delegateHeight : 0
                    width: control.itemWidth
                    height: control.itemHeight
                    border.color: "black"
                    Text {
                        anchors.centerIn: parent
                        // 显示行号列号
                        text: "(%1,%2)".arg(
                                  parseInt(model.index / control.columns)).arg(
                                  model.index % control.columns)
                    }
                }
            }
        }
    }
}

如果要带section,就得每个分组有单独的index,这样才能计算分组内的行列号,需要我们自定义一个ListModel:

#pragma once
#include <QAbstractListModel>

// 实际数据
struct DataInfo
{
    int value;
    // 本例用日期来分组
    QString date;
};

// 分组信息,如 index
struct SectionInfo
{
    int index;
};

class DataModel : public QAbstractListModel
{
    Q_OBJECT
private:
    enum ModelRole {
        ValueRole = Qt::UserRole
        , GroupNameRole
        , GroupIndexRole
    };
public:
    explicit DataModel(QObject *parent = nullptr);

    // Model 需要实现的必要接口
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    QHash<int, QByteArray> roleNames() const override;

    // 在头部添加一个数据
    Q_INVOKABLE void appendData(int value, const QString &date);
    // 根据 model.index 删除一个数据
    Q_INVOKABLE void removeData(int index);
    // 加点测试数据
    void test();

private:
    QVector<DataInfo> datas;
    QVector<SectionInfo> inners;
};

DataModel::DataModel(QObject *parent)
    : QAbstractListModel(parent)
{
    test();
}

int DataModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return 0;
    return datas.size();
}

QVariant DataModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();
    auto &&item = datas.at(index.row());
    auto &&inner = inners.at(index.row());
    switch (role)
    {
    case ValueRole: return item.value;
    case GroupNameRole: return item.date;
    case GroupIndexRole: return inner.index;
    }
    return QVariant();
}

QHash<int, QByteArray> DataModel::roleNames() const
{
    static QHash<int, QByteArray> names{
        {ValueRole, "value"}
        , {GroupNameRole, "groupName"}
        , {GroupIndexRole, "groupIndex"}
    };
    return names;
}

void DataModel::appendData(int value, const QString &date)
{
    // 先判断分组是否相同
    if (datas.isEmpty() || datas.first().date != date) {
        // 没有该组,新建一个分组
        DataInfo item;
        item.value = value;
        item.date = date;
        SectionInfo inner;
        inner.index = 0;
        beginInsertRows(QModelIndex(), 0, 0);
        datas.push_front(item);
        inners.push_front(inner);
        endInsertRows();
    } else {
        // 已有该组,插入并移动该组后面的 Item
        DataInfo item;
        item.value = value;
        item.date = date;
        SectionInfo inner;
        inner.index = 0;
        beginInsertRows(QModelIndex(), 0, 0);
        datas.push_front(item);
        inners.push_front(inner);
        endInsertRows();
        // 刷新该组
        int update_count = 0;
        // 0 是新插入,1 是旧 0
        for (int i = 1; i < inners.size(); i++) {
            auto &&inner_i = inners[i];
            if (i > 1 && inner_i.index == 0)
                break;
            inner_i.index = i;
            update_count ++;
        }
        emit dataChanged(QAbstractListModel::index(1, 0), QAbstractListModel::index(1 + update_count, 0));
    }
}

void DataModel::removeData(int index)
{
    if (index < 0 || index >= datas.size())
        return;
    beginRemoveRows(QModelIndex(), index, index);
    datas.removeAt(index);
    inners.removeAt(index);
    endRemoveRows();
    int update_count = 0;
    for (int i = index; i < inners.size(); i++) {
        auto &&inner_i = inners[i];
        if (inner_i.index == 0)
            break;
        inner_i.index -= 1;
        update_count ++;
    }
    if (update_count > 0) {
        emit dataChanged(QAbstractListModel::index(index, 0), QAbstractListModel::index(index + update_count, 0));
    }
}

void DataModel::test()
{
    DataInfo item;
    SectionInfo inner;
    item.date = "2022.2.22";
    for (int i = 0; i < 11; i++)
    {
        item.value = i + 1;
        datas.push_back(item);
        inner.index = i;
        inners.push_back(inner);
    }
    item.date = "2010.10.10";
    for (int i = 0; i < 21; i++)
    {
        item.value = i + 1;
        datas.push_back(item);
        inner.index = i;
        inners.push_back(inner);
    }
    item.date = "1999.9.9";
    for (int i = 0; i < 31; i++)
    {
        item.value = i + 1;
        datas.push_back(item);
        inner.index = i;
        inners.push_back(inner);
    }
}
import QtQuick 2.15
import QtQuick.Controls 2.15
import Test 1.0

// ListView 实现带 section 分组的 GridView
Rectangle {
    id: control

    border.color: "black"

    // 边距
    property int padding: 10
    // Item 间隔
    property int spacing: 10
    // Item 宽
    property int itemWidth: 300
    // Item 高
    property int itemHeight: 100
    // Delegate 宽
    property int delegateWidth: itemWidth + spacing
    // Delegate 高
    property int delegateHeight: itemHeight + spacing
    // 列数根据可视宽度和 Item 宽度计算
    property int columns: (list_view.width + spacing - padding) / delegateWidth < 1
                          ? 1
                          : (list_view.width + spacing - padding) / delegateWidth

    // 套一层 Item clip 剪去 ListView 尾巴上多余的部分不显示出来
    Item {
        anchors.fill: parent
        anchors.margins: control.padding
        // 右侧留下滚动条位置,所以 columns 里 list_view.width 要减一个 padding
        anchors.rightMargin: 0
        clip: true

        ListView {
            id: list_view
            width: parent.width
            // 高度多一个 delegate 放置 footer,防止末尾的一行滑倒底部后隐藏
            // 多出来的一部分会被外部 Item clip 掉
            height: parent.height + control.delegateHeight + control.spacing
            flickableDirection: Flickable.HorizontalAndVerticalFlick
            boundsBehavior: Flickable.StopAtBounds
            headerPositioning: ListView.OverlayHeader
            // 底部多一个 footer 撑高可显示范围,防止末尾的一行滑倒底部后隐藏
            footerPositioning: ListView.OverlayFooter
            ScrollBar.vertical: ScrollBar {
                // padding 加上 ListView 多出来的一部分
                bottomPadding: padding + (control.delegateHeight + control.spacing)
                // 常驻显示只是方便调试
                policy: ScrollBar.AlwaysOn
            }
            footer: Item {
                // 竖向的 ListView 宽度无所谓
                width: control.delegateWidth
                // 高度大于等于 delegate 高度才能保证显示
                height: control.delegateHeight
            }
            model: DataModel {
                id: list_model
            }
            section {
                property: "groupName"
                criteria: ViewSection.FullString
                delegate: Item {
                    width: list_view.width - control.padding
                    height: 40
                    Rectangle {
                        width: parent.width
                        height: parent.height - control.spacing
                        color: "gray"
                        Text {
                            anchors.centerIn: parent
                            text: section
                            color: "white"
                        }
                    }
                }
                labelPositioning: ViewSection.InlineLabels
            }
            delegate: Item {
                width: control.delegateWidth
                // 每行第一个 Item 有高度,后面的没高度,这样就能排列到一行
                // 因为 0 高度 Item 在末尾,超出范围 visible 就置为 false 了,所以才需要 footer 撑高多显示一行的内容
                // delegate 高度不一致会导致滚动条滚动时长度变化
                height: (model.groupIndex % control.columns === 0) ? control.delegateHeight : 0
                // 放置真正的内容
                Rectangle {
                    // 根据列号计算 x
                    x: (model.groupIndex % control.columns) * control.delegateWidth
                    // 负高度就能和每行第一个的 y 一样
                    y: (model.groupIndex % control.columns !== 0) ? -control.delegateHeight : 0
                    width: control.itemWidth
                    height: control.itemHeight
                    border.color: "black"
                    Text {
                        anchors.centerIn: parent
                        // 显示行号列号
                        text: "(%1,%2) - %3".arg(
                                  parseInt(model.groupIndex / control.columns)).arg(
                                  model.groupIndex % control.columns).arg(
                                  model.value)
                    }
                    Column {
                        x: 12
                        anchors.verticalCenter: parent.verticalCenter
                        spacing: 12
                        Button {
                            width: 100
                            height: 30
                            text: "append"
                            onClicked: {
                                list_model.appendData(model.value, "2222.2.22")
                            }
                        }
                        Button {
                            width: 100
                            height: 30
                            text: "remove"
                            onClicked: {
                                list_model.removeData(model.index)
                            }
                        }
                    }
                }
            } // end delegate Item
        } // end ListView
    }
}

这里只是实现了一个简单的效果,很多细节还需要调整。

通过添加更多的属性和计算,也可以实现带section的FlowView,即Item的宽高不是固定大小,整体为流式布局。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/373252.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

「递归算法」:二叉树剪枝

一、题目 给你二叉树的根结点 root &#xff0c;此外树的每个结点的值要么是 0 &#xff0c;要么是 1 。 返回移除了所有不包含 1 的子树的原二叉树。 节点 node 的子树为 node 本身加上所有 node 的后代。 示例 1&#xff1a; 输入&#xff1a;root [1,null,0,0,1] 输出&…

EasyX图形库学习(三、用easyX实现移动的小球、图片-加载、输出)

目录 图像输出 loadimage用于从文件中读取图片 putimage在当前设备上绘制指定图像。 图形界面中的小球与按钮控制 图像输出 在使用图像之前&#xff0c;需要定义一个变量(对象)&#xff0c;然后把图片加载进变量才能进行使用。 平时定义变量都是使用的基础数据类型&#x…

RISC-V工业级芯片公司匠芯创,宣布软件开发包SDK正式开源

近日&#xff0c;RISC-V芯片公司匠芯创宣布开源D21x系列工业级应用芯片软硬件开发包SDK。软件开发包涵盖了D21x开源代码、软件API库、开发手册文档、相关调试及烧录工具&#xff0c;并且提供多媒体中间件等多个SDK用例和应用 Demo示例&#xff0c;帮助企业和个人开发者快速上手…

C++ dfs 与图有关的知识(四十七)【第七篇】

今天我们接着来学习树上搜索&#xff08;dfs深度优先搜索&#xff09; 1.树的深度与子树大小 树的深度&#xff1a;规定根结点是树的第一层&#xff0c;树根的孩子结点是树的第二层&#xff0c;以此类推&#xff0c;树的深度就是结点的最大层数。 根据定义&#xff0c;如果我们…

基于深度学习算法的轴承故障自主分类

1. 要求 轴承有3种故障&#xff1a;外圈故障&#xff0c;内圈故障&#xff0c;滚珠故障&#xff0c;外加正常的工作状态。如表1所示&#xff0c;结合轴承的3种直径&#xff08;直径1,直径2,直径3&#xff09;&#xff0c;轴承的工作状态有10类&#xff1a; 表1 轴承故障类别 外…

R语言绘图教程 | 双侧条形图绘制教程

写在前面 双侧条形图在我们的文章中也是比较常见的,那么这样的图形是如何绘制的呢? 以及它使用的数据类型是什么呢? 这些都是我们在绘制图形前需要掌握的,至少我们知道绘图的数据集如何准备,这样才踏出第一步。 今天的教程,我们会从数据的准备,以及数据如何整理,以及…

亲测解决vscode的debug用不了、点了没反应

这个问题在小虎登录vscode同步了设置后出现,原因是launch文件被修改或删除。解决方法是重新添加launch。 坏境配置 win11 + vscode 解决方法 Ctrl + shift + P,搜索debug添加配置: 选择python debugger。 结果生成了一个文件在当前路径: launch内容: {// Use Int…

ubuntu系统下c++ cmakelist vscode debug(带传参的debug)的详细示例

c和cmake的debug&#xff0c;网上很多都需要配置launch.json&#xff0c;cpp.json啥的&#xff0c;记不住也太复杂了&#xff0c;我这里使用cmake插件带有的设置&#xff0c;各位可以看一看啊✌(不知不觉&#xff0c;竟然了解了vscode中配置文件的生效逻辑&#x1f923;) 克隆…

Unity3D判断屏幕中某个坐标点的位置是否在指定UI区域内

系列文章目录 unity工具 文章目录 系列文章目录前言一、使用rect.Contains()判断1-1、转换坐标1-2、代码如下&#xff1a;1-3、注意事项1-3、测试效果如下 二、使用坐标计算在不在区域内2-1、方法如下&#xff1a;2-2、注意事项 三、使用RectTransformUtility.ScreenPointToLo…

使用maven对springboot项目进行瘦身

目录 一、什么是Maven 二、springboot 项目 三、springboot 项目瘦身 一、什么是Maven Maven是一个基于Java的项目管理和构建工具。它通过提供一个一致的项目结构、自动化构建脚本和依赖管理系统&#xff0c;简化了Java项目的构建过程。 Maven使用一种称为POM&#xff08;…

数据结构_找环,破环题-2.5

一. 判断单链表有无环 a. 错误的思路&#xff1a;遍历陷入死循环 1&#xff09;和相交的遍历思路一样&#xff0c;找指向相同。 错误点 一直在死循环。 思考点&#xff1a;如何破环 b. 个人思路&#xff1a;反转链表回首结点 1&#xff09;目前的经验&#xff0c;无非就…

macOS Sonoma 14系统安装包

macOS Sonoma 14是苹果公司最新推出的操作系统&#xff0c;为Mac用户带来了全新的使用体验。Sonoma是苹果继Catalina之后的又一重要更新&#xff0c;它在改善系统性能、增加新功能、优化用户界面等方面做出了显著贡献。 macOS Sonoma 14系统有许多令人兴奋的新功能和改进&…

【LangChain-04】利用权重和偏差跟踪和检查LangChain代理的提示

利用权重和偏差跟踪和检查LangChain代理的提示 一、说明 考虑到&#xff08;生成&#xff09;人工智能空间&#xff0c;&#xff08;自主&#xff09;代理现在无处不在&#xff01;除了更强大且幸运的是开放的大型语言模型&#xff08;LLM&#xff09;之外&#xff0c;LangCh…

JavaScript运行机制

在web前端开发中&#xff0c;JavaScript无疑是一种非常重要的编程语言。它能够为网页添加动态交互功能&#xff0c;提升用户体验。然而&#xff0c;要充分发挥JavaScript的威力&#xff0c;我们需要对它的运行机制有一定的了解。 JavaScript是一种解释执行的脚本语言&#xff…

Goland控制台日志打印错位

现象&#xff1a;Goland控制台打印日志&#xff0c;调整控制台界面大小后偶发性的日志内容错位 原因&#xff1a;未知&#xff08;大概是bug&#xff09; 解决方案&#xff1a; shift shift 进入Registry&#xff0c;取消go.run.process.with.pty勾选即可

AI助力农作物自动采摘,基于YOLOv3全系列【yolov3tiny/yolov3/yolov3spp】参数模型开发构建作物生产场景下番茄采摘检测计数分析系统

去年十一那会无意间刷到一个视频展示的就是德国机械收割机非常高效自动化地24小时不间断地在超广阔的土地上采摘各种作物&#xff0c;专家设计出来了很多用于采摘不同农作物的大型机械&#xff0c;看着非常震撼&#xff0c;但是我们国内农业的发展还是相对比较滞后的&#xff0…

K8S之Namespace的介绍和使用

Namespace的理论和实操 Namespace理论说明Namespace实操创建、查看命名空间使用ResouceQuota 对Namespace做资源限额更多ResouceQuota 的使用 Namespace理论说明 命名空间定义 K8s支持多个虚拟集群&#xff0c;它们底层依赖于同一个物理集群。 这些虚拟集群被称为命名空间&…

教授LLM思考和行动:ReAct提示词工程

ReAct&#xff1a;论文主页 原文链接&#xff1a;Teaching LLMs to Think and Act: ReAct Prompt Engineering 在人类从事一项需要多个步骤的任务时&#xff0c;而步骤和步骤之间&#xff0c;或者说动作和动作之间&#xff0c;往往会有一个推理过程。让LLM把内心独白说出来&am…

Flink 动态表 (Dynamic Table) 解读

博主历时三年精心创作的《大数据平台架构与原型实现&#xff1a;数据中台建设实战》一书现已由知名IT图书品牌电子工业出版社博文视点出版发行&#xff0c;点击《重磅推荐&#xff1a;建大数据平台太难了&#xff01;给我发个工程原型吧&#xff01;》了解图书详情&#xff0c;…

Linux服务器安装Jenkins

1、安装Jenkins前必须先安装jdk与maven 2、下载Jenkins 安装包地址 linux jenkins 链接: 百度网盘 请输入提取码 提取码: zfyq 3、解压压缩包 rpm -ivh jenkins-2.174-1.1.noarch.rpm 4、解压完成后查看Jenkins安装路径 whereis jenkins 5、启动报错 &#xff0c;这是因为Jenki…