QML用ListView实现带section的GridView

news2024/11/14 16:19:53

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的宽高不是固定大小,整体为流式布局。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解决dockor安装nginx提示missing signature key的问题

问题描述 使用dockor安装nginx拉取nginx的时候提示key丢失问题 问题定位 由于dockor版本低导致 问题解决 卸载重新安装最新版本dockor 解决步骤 1. 卸载旧版本的Docker&#xff1a; sudo yum remove docker docker-common docker-selinux docker-engine 2. 安装依赖包&am…

Stable Diffusion教程——使用TensorRT GPU加速提升Stable Diffusion出图速度

概述 Diffusion 模型在生成图像时最大的瓶颈是速度过慢的问题。为了解决这个问题&#xff0c;Stable Diffusion 采用了多种方式来加速图像生成&#xff0c;使得实时图像生成成为可能。最核心的加速是Stable Diffusion 使用了编码器将图像从原始的 3512512 大小转换为更小的 46…

【杂谈】年尾做了这件事,我后悔了.......

文章目录 前言1. 花钱1.1 购买并安装幻兽帕鲁1.2 阿里云服务器 2. 配置3. 游玩4. 总结与感悟4.1 总结4.2 感悟 后记 幻兽帕鲁多人服务器部署 趁着过年放假&#xff0c;一本正经地玩游戏学习服务器部署啦&#xff01; 但是&#xff0c;部署完我就后悔了… 前言 作为一名 游戏…

shellcode

生成shellcode 在漏洞利用中&#xff0c;shellcode是不可或缺的部分&#xff0c;所以在网上有许多公开分享的 shellcode &#xff0c;在不同平台上并不通用&#xff0c;需要选择适合的shellcode 。这里推荐两个常 见公开的安全平台&#xff1a;一个为公开的漏洞库exploit-db&am…

【汇编】简单的linux汇编语言程序

一、Linux系统汇编语言 Linux系统上的汇编语言可以使用不同的语法风格&#xff0c;主要包括Intel语法和AT&T语法。这两种语法有各自的特点和风格区别&#xff0c;尽管它们表示的底层机器指令相同。下面分别对两种语法进行简要说明&#xff1a; Intel语法 Intel语法是由I…

深入理解ES的倒排索引

目录 数据写入过程 词项字典 term dictionary 倒排表 posting list FOR算法 RBM算法 ArrayContainer BitMapContainer 词项索引 term index 在Elasticsearch中&#xff0c;倒排索引的设计无疑是惊为天人的&#xff0c;下面看下倒排索引的结构。 倒排索引分为词项索引【…

【高频前端面试题--TypeScript篇】

&#x1f680; 作者 &#xff1a;“码上有前” &#x1f680; 文章简介 &#xff1a;前端高频面试题 &#x1f680; 欢迎小伙伴们 点赞&#x1f44d;、收藏⭐、留言&#x1f4ac; 高频前端面试题--Vue3.0篇 什么是TypeScript&#xff1f;TypeScript数据类型TypeScript中命名空…

[word] word 2010宏已被禁用警告关闭方法 #媒体#学习方法

word 2010宏已被禁用警告关闭方法 Word2010宏已被禁用警告关闭方法&#xff1a;在「信任中心设置」选项的宏设置中选择「禁用所有宏&#xff0c;并且不通知」即可。 每次打开Word 2010&#xff0c;都会提示「完全警告&#xff1a;宏已被禁用」提示。自从Word 2010安装完毕&am…

JavaScript鼠标移动事件

&#x1f9d1;‍&#x1f393; 个人主页&#xff1a;《爱蹦跶的大A阿》 &#x1f525;当前正在更新专栏&#xff1a;《VUE》 、《JavaScript保姆级教程》、《krpano》、《krpano中文文档》 ​ ​ ✨ 前言 鼠标移动是用户界面中非常重要的交互行为。学习区分不同的鼠标移动事…

Vue事件中如何使用 event 对象

在Vue中&#xff0c;事件处理函数常常需要获取事件触发时的相关信息&#xff0c;比如鼠标位置、按键信息等。而要获取这些信息&#xff0c;就需要使用event对象。那么在Vue的事件中如何正确使用event对象呢&#xff1f;接下来就来详细介绍一下。 首先&#xff0c;在Vue的事件中…

ChatGLM2-6B模型的win10测试笔记

ChatGLM2-6B介绍&#xff1a; 介绍 ChatGLM2-6B 是开源中英双语对话模型 ChatGLM-6B 的第二代版本&#xff0c;在保留了初代模型对话流畅、部署门槛较低等众多优秀特性的基础之上&#xff0c;ChatGLM2-6B 引入了如下新特性&#xff1a; 更强大的性能&#xff1a;基于 ChatGLM 初…

#Js篇:js里面递归的理解

定义&#xff1a; 递归是一种编程技术&#xff0c;它是指一个函数在其定义内部调用自身的过程。 特点&#xff1a; 一个问题可以分解为更小的问题用同样的方法解决&#xff1b;分解后的子问题求解方式一样&#xff0c;不同的是数据规模变小&#xff1b;存在递归终止条件 作…

前端JavaScript篇之原型链的终点是什么?如何打印出原型链的终点?

目录 原型链的终点是什么&#xff1f;如何打印出原型链的终点&#xff1f; 原型链的终点是什么&#xff1f;如何打印出原型链的终点&#xff1f; 在 JavaScript 中&#xff0c;原型链是由对象和原型对象组成的链式结构。每个对象都有一个原型对象&#xff0c;并通过 __proto__…

ruoyi若依框架SpringSecurity实现分析

系列文章 ruoyi若依框架学习笔记-01 ruoyi若依框架分页实现分析 ruoyi若依框架SpringSecurity实现分析 文章目录 系列文章前言具体分析一、项目中的SpringSecurity版本二、登录认证流程分析三、权限鉴定四、退出登录五、SpringSecurity配置类 总结 前言 在ruoyi-vue若依框…

JavaScript综合练习2

JavaScript 综合练习 2 1. 案例演示 2. 代码实现 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title&…

docker 基于容器创建本地web容器化镜像

一、docker 基于容器创建本地web容器化镜像 1、启动指定buysbox 镜像 docker run --name b1 -it busybox:latest 2、创建目录&#xff0c;并创建html mkdir -p /data/html vi index.html 内容自定义例如&#xff1a;<h1>welcome to busybox<h1> 3、新增窗口&am…

Kubernetes实战(二十七)-HPA实战

1 HPA简介 HPA 全称是 Horizontal Pod Autoscaler&#xff0c;用于POD 水平自动伸缩&#xff0c; HPA 可以 基于 POD CPU 利用率对 deployment 中的 pod 数量进行自动扩缩容&#xff08;除了 CPU 也可以基于自定义的指标进行自动扩缩容&#xff09;。pod 自动缩放不适用于无法…

ubuntu22.04@laptop OpenCV Get Started: 005_rotate_and_translate_image

ubuntu22.04laptop OpenCV Get Started: 005_rotate_and_translate_image 1. 源由2. translate/rotate应用Demo3 translate_image3.1 C应用Demo3.2 Python应用Demo3.3 平移图像过程 4. rotate_image4.1 C应用Demo4.2 Python应用Demo4.3 旋转图像过程 5. 总结6. 参考资料 1. 源由…

Seurat - 聚类教程 (1)

设置 Seurat 对象 在本教程[1]中&#xff0c;我们将分析 10X Genomics 免费提供的外周血单核细胞 (PBMC) 数据集。在 Illumina NextSeq 500 上对 2,700 个单细胞进行了测序。可以在此处[2]找到原始数据。 我们首先读取数据。 Read10X() 函数从 10X 读取 cellranger 管道的输出&…

详细分析Redis性能监控指标 附参数解释(全)

目录 前言1. 基本指标2. 监控命令3. 实战演示 前言 对于Redis的相关知识推荐阅读&#xff1a; Redis框架从入门到学精&#xff08;全&#xff09;Python操作Redis从入门到精通附代码&#xff08;全&#xff09;Redis相关知识 1. 基本指标 Redis 是一个高性能的键值存储系统…