Qml组件化编程9-Model和View



  • 简介

    本文是《Qml组件化编程》系列文章的第九篇,涛哥将教大家,Qml中Model和View的知识。

    注:文章主要发布在涛哥的博客知乎专栏-涛哥的Qt进阶之路

    界面、数据和逻辑分离

    界面架构的理念发展的非常快,主要在Web技术的驱动下,就有这么多架构:

    MVC、MVP、 MVVM、 Flux、Redux。

    涛哥并没有深入的研究过这些架构,但只要抓住一些关键点就够了:界面、数据和逻辑要分别处理,最终要能够正确处理用户输入并显示结果。

    (也可能我做的都是小项目,没有参与过大型的Web项目,眼界太低。欢迎大佬指点)

    先来看一下Qt中提供的架构:

    预览

    Model代表数据,View代表界面,这个Delegate嘛,就是用来定制View的显示方式和Controll的调用,也应该算进View里面去。

    这样看来Qt是M-V架构 ? 其实Qt算是MVC架构,这个Controll一般是自己实现的,和Model放在一起的。

    不过Qt有信号/槽机制,在QtQuick中以属性绑定的方式出现。信号/槽相当于Gof设计模式中的观察者模式,也相当于Flux中的订阅/发布模式。

    涛哥按自己的实践和理解,画了一个Qt的Model-View架构草图:

    预览

    Qt内置的Model-View

    View包括 ListView、TableView、TreeView这三种

    (ComboBox也可以算作ListView)

    预览

    对应的Model包括 ListModel、TableModel、TreeModel

    预览

    Qt提供了一些抽象的Model类,需要自己去继承并实现接口,也有一些可以直接用。

    下图是涛哥整理的Qt中model继承关系:

    预览

    其中的QStringListModel不是抽象类,可以直接用在ListView中。

    QStandardItemModel也不是抽象类,可以直接用在任意一种View中。

    在数据量大、有性能要求的地方,需要继承QAbstractItemModel类,重新实现一个model。

    对于性能要求不高的数据展示,会有一些更加方便、取巧的方式,接着往下看吧。

    (友情提示:涛哥不关心QWidget,只说QtQuick/Qml)

    整数做model

    在ListView中,一个整数作为model,就可以创建多个delegate实例。

    整数作为model,也可以用在GridView、Combobox、Repeater等需要model的地方。

    <Qml组件化编程6-进度条定制>一文中,展示渐变效果,就用的整数作为model

    预览

    import QtQuick 2.9
    import QtQuick.Controls 2.5
    
    Item {
        anchors.fill: parent
    
        GridView {
            id: g
            anchors.fill: parent
            anchors.margins: 20
            cellWidth: 160
            cellHeight: 160
            model: 180          //这里的数据Model直接给个整数180
            clip: true
            property var invalidList: [27, 39, 40, 45, 71, 74, 105, 111, 119, 130, 135, 141]    //这几个是不能用的,看过运行报错后手动列出来的。
            delegate: Item{
                width: 160
                height: 160
                Rectangle{
                    width: 150
                    height: 150
                    anchors.centerIn: parent
                    color: "white"
                    radius: 10
                    Text {
                        anchors.horizontalCenter: parent.horizontalCenter
                        anchors.top: parent.top
                        anchors.topMargin: 2
                        text: index + 1
                    }
                    Rectangle {
                        width: 100
                        height: width
                        radius:  width / 2
                        //编号在列表里的,直接渐变赋值为null,就不会在Qml运行时报警告了
                        gradient: g.invalidList.indexOf(modelData + 1) < 0 ? modelData + 1 : null
                        anchors.centerIn: parent
                        anchors.verticalCenterOffset: 10
                    }
                }
            }
        }
    }
    

    关于delegate

    简单说一下delegate:

    上面GridView的 model设置为180,表示这个View要产生180个相同的构件实例,按照Grid的方式布局排列。

    而delegate就相当于是一个模板,用来描述这180个相同的构件长啥样。当然每个实例不可能完全长得一样,我们可以通过

    绑定delegate提供的内置属性或其它属性,达到"大同小异"的目的。

    delegate中一般会提供一个index和一个modelData,详细的说明需要参考相应的View文档。

    View与Repeater的区别

    上面的GridView虽然会创建180个实例,但并不是一次创建全部的,而是只创建能看见的那几个,否则会占用很多CPU、内存和GPU资源。

    而Repeater这种就是直接生成180个,并没有做任何内置处理。

    (Repeater也可以通过自己控制visible的方式,实现部分创建,后面涛哥有个RingView特效会用这种方式)

    ListModel

    Qml提供了ListModel这样的一个封装,可以直接在Qml中定义静态的model

    静态ListModel

      import QtQuick 2.0
    
      ListModel {
          id: fruitModel
    
          ListElement {
              name: "Apple"
              cost: 2.45
          }
          ListElement {
              name: "Orange"
              cost: 3.25
          }
      }
    

    然后在ListView中使用

    ListView {
        anchors.fill: parent
        model: fruitModel
        delegate: Item {
            Text {
                text: modelData.name
            }
            Text {
                text: cost
            }
        }
    }
    

    第一个text通过modelData.name获取到name值

    第二个text直接用了cost,其实是modelData.cost省略了modelData。这种写法在静态的ListModel中是可以用的。

    动态ListModel

    ListModel还提供了一些动态修改数据的接口:

    预览

    像append、 set、insert这些,参数里的jsobject就是js环境中的Object类型,可以参考JS手册

    这里涛哥示例一下,动态添加元素

    ...
    onClicked: {
        var banana = new Object()
        
        //或者这样也行,按照js语法即可
        //var bababa = Object.create(null)
    
        banana["name"]="banana" //方括号 + key的方式设置成员
        babana.cost=15  //点+名字的方式设置成员
        
        fruitModel.append(banana)   //将创建的banana添加到model
    }
    ...
    

    更详细的用法,可以参考 涛哥两年前写过的一个Qml表格编辑器

    里面有ListModel的JSON序列化和反序列化、动态增、删、改,Ubuntu风格的查找、Redo、UnDo等大部分功能。

    TaoQuick项目的插件机制,也是通过JSON动态添加Model元素。TaoQuick

    XmlListModel

    处理xml的model,可以方便地使用XPath。

      XmlListModel {
           id: feedModel
           source: "http://rss.news.yahoo.com/rss/oceania"
           query: "/rss/channel/item"
           XmlRole { name: "title"; query: "title/string()" }
           XmlRole { name: "link"; query: "link/string()" }
           XmlRole { name: "description"; query: "description/string()" }
      }
    

    ObjectModel

    可视对象的集合,做为model,连Delegate都省了

      import QtQuick 2.0
      import QtQml.Models 2.1
    
      Rectangle {
          ObjectModel {
              id: itemModel
              Rectangle { height: 30; width: 80; color: "red" }
              Rectangle { height: 30; width: 80; color: "green" }
              Rectangle { height: 30; width: 80; color: "blue" }
          }
    
          ListView {
              anchors.fill: parent
              model: itemModel
          }
      }
    

    C++导出Model

    除了以上这些,C++中导出的一些类型也可以作为数据model。

    这里的导出包括Q_PROPERTY和 Q_INVOKABLE函数的返回值、槽函数的返回值,以及

    setContextProperty注册到上下文的可用作model的类型。

    一般使用Q_PROPERTY (本质上也是属性的get函数返回值,在js中做了转换)

    QList<T>

    QList<QString> 字符串列表,可以直接用,不用多说了。

    QList<QObject*> QObject列表,List中的任意一个QObject有一些属性变更时,都能通知到Qml。

    QJsonArray

    QJsonArray也是可以直接导出给ListView用,不过注意是只读的。

    QQmlPropertyMap

    QQmlPropertyMap 是一个Map结构, 但是这个结构注册后,Qml中可以直接用"点 + 名字"的方式访问其中的数据

      // create our data
      QQmlPropertyMap ownerData;
      ownerData.insert("name", QVariant(QString("John Smith")));
      ownerData.insert("phone", QVariant(QString("555-5555")));
    
      // expose it to the UI layer
      QQuickView view;
      QQmlContext *ctxt = view.rootContext();
      ctxt->setContextProperty("owner", &ownerData);
    
      view.setSource(QUrl::fromLocalFile("main.qml"));
      view.show();
    
    //main.qml
    Text { 
        text: owner.name + " " + owner.phone 
    }
    

    ListView缺失的灵魂

    Qml这个ListView是残缺不全的,很多功能都要自己实现。

    搜索与排序

    前面提到的QSortFilterProxyModel是一种在数据上实现排序和过滤的方法。

    还有一种在View层实现搜索和过滤的方式,即DelegateModelGroup。(已经有案例在用,后续再放出代码)

    当然Qt5.12的ListView/TableView提供了行和列 隐藏控制的功能,View层做搜索会更方便一些。(还没有实践)

    选中

    按住Ctrl 再鼠标点击,多选, 再点击一下反选。

    按住Shift再鼠标点击,连选。

    旧的QtQuick.Controls 1中也有一个ListView,带SelectonModel功能,直接支持多选、反选。

    5.12开始,QtQuick.Controls 1模块被废弃了,而Controls2中的ListView不带这功能了。只能自己记键盘按键来模拟实现。

    (顺便吐槽一下,5.12直接把Controls 1的TreeView废掉了,Controls 2又没有TreeView。Controls 1的那个虽然还能用,程序跑起来就是一堆js 异常)

    拖拽

    拖动和放置功能也得自己做。

    特效

    ListView提供过度动画,下拉刷新一类的效果很多人已经做了,涛哥就不重复了。

    (涛哥正在给TaoQuick开发高级插件TaoEffect,将会包含大量酷炫特效组件,敬请期待)

    转载声明

    文章出自涛哥的博客

    文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可, 转载请注明出处, 谢谢合作 © 涛哥

    联系方式


    作者 武威的涛哥
    开发理念 弘扬鲁班文化,传承工匠精神
    博客 https://jaredtao.github.io
    github https://github.com/jaredtao
    知乎 https://www.zhihu.com/people/wentao-jia
    邮箱 jared2020@163.com
    微信 xsd2410421
    QQ 759378563

    请放心联系我,乐于提供咨询服务,也可洽谈商务合作相关事宜。

    打赏

    weixin
    zhifubao


    如果觉得涛哥写的还不错,还请为涛哥打个赏,您的赞赏是涛哥持续创作的源泉。



Log in to reply
 

走马观花

最近的回复

  • C

    Qt for MCU需要商业授权的

    read more
  • Qt for MCUs

    搭建Qt for MCUs PC端开发环境。qt for mcus提供了一个完整的图形框架和工具包,包含了在MCUs上设计、开发和部署gui所需的一切。它允许您在裸机或实时操作系统上运行应用程序。

    先决条件

    开发主机环境支持仅限于Windows 10

    MSVC compiler v19.16 (Visual Studio 2017 15.9.9 or newer) x64

    CMake v3.13 or newer (you can install it using the Qt Online installer) x64

    使用Qt联机安装程序安装Qt for MCUs,该安装程序可通过Qt帐户下载

    安装Qt 5.14和Qt Creator 4.11 or higher

    安装链接

    › Qt: https://account.qt.io/downloads
    › CMake: https://cmake.org/download/
    › Python 2.7 32-bit: https://www.python.org/downloads/release/python-2716/
    › Arm GCC: https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnutoolchain/gnu-rm/downloads
    › J-Link Software Pack: https://www.segger.com/downloads/jlink/JLink_Windows.exe
    › J-Link OpenSDA Firmware: https://www.segger.com/downloads/jlink/OpenSDA_MIMXRT1050-EVKHyperflash
    › STM32CubeProgrammer: https://www.st.com/en/development-tools/stm32cubeprog.html
    › STM32 ST-LINK Utility: https://www.st.com/en/development-tools/stsw-link004.html​​​​​​​

    Qt Creator设置 启用Qt Creator插件 选择“帮助>关于插件”,然后从列表中选择“MCU支持(实验性)”插件,重新启动Qt Creator以应用更改
    替代文字 为MCU创建Qt工具包

    选择工具>选项>设备>MCU

    选择Qt for MCUs-Desktop 32bpp作为目标

    如果尚未设置,请提供Qt for MCUs安装目录的路径。

    单击Apply应用。

    替代文字

    替代文字
    替代文字

    注意:

    编译器要选X64,Qt版本要选64bit,CMake Tool选x64

    打开恒温器项目demo

    选择文件>打开文件或项目。。。

    打开CMakefiles.txt文件来自thermo文件夹的文件。

    选择Qt作为MCU-桌面32bpp套件。

    单击“配置项目”以完成。

    替代文字

    问题

    开发主机环境支持仅限于Windows 10

    C++编译失败,文本大字体.pixelSize.

    文本类型无法正确呈现需要复杂文本布局的unicode序列。对复杂文本使用StaticText

    read more
  • H

    hi 有问题请教你,方便加个联系方式吗

    read more
  • boost.asio是一个很棒的网络库,这回儿我也开始系统地学习起来了。想想当年接触boost,也有八年多了。这次开始接触boost,觉得既熟悉又陌生。熟悉的是小写字母+下划线的命名方式、晦涩的模板、很慢的编译速度以及较大的程序体积,陌生的是asio的各种概念:io服务、接收器、套接字等等:我之前对网络编程不是非常了解。

    于是根据我的理解,参考《Boost.Asio C++网络编程》实现了这样一个简单的客户端和服务端通信的例子,例子非常简单,还不完善,但是幸运的是,可以在本机上互通了。
    下面是客户端的代码:

    #include <iostream> #include <boost/asio.hpp> #include <boost/proto/detail/ignore_unused.hpp> using namespace std; using namespace boost::asio; using namespace boost::system; using namespace boost::proto::detail;// 提供ignore_unused方法 void writeHandler( const boost::system::error_code& ec, size_t bytesTransferred ) { if ( ec ) { cout << "Write data error, code: " << ec.value( ) << "transferred: " << bytesTransferred << endl; } else { cout << "OK! " << bytesTransferred << "bytes written. " << endl; } } int main(int argc, char *argv[]) { ignore_unused( argc ); ignore_unused( argv ); io_service service; ip::tcp::socket sock( service ); ip::tcp::endpoint ep( ip::address::from_string( "127.0.0.1" ), 6545 ); boost::system::error_code ec; sock.connect( ep, ec ); if ( ec ) { cout << "Connect error, code: " << ec.value( ) << ", We will exit." << endl; return ec.value( ); } else { char buf[1024] = "Hello world!"; sock.async_write_some( buffer( buf ), writeHandler ); sock.close( ); } return service.run( ); }

    下面是服务端的代码:

    #include <iostream> #include <boost/asio.hpp> #include <boost/proto/detail/ignore_unused.hpp> using namespace std; using namespace boost::asio; using namespace boost::system; using namespace boost::proto::detail;// 提供ignore_unused方法 void acceptHandle( const boost::system::error_code& code ) { cout << "Accepted." << endl; } int main(int argc, char *argv[]) { ignore_unused( argc ); ignore_unused( argv ); io_service service; ip::tcp::endpoint ep( ip::address::from_string( "127.0.0.1" ), 6545 ); boost::system::error_code ec; ip::tcp::socket sock( service ); ip::tcp::acceptor acceptor( service, ep ); acceptor.async_accept( sock, acceptHandle ); if ( ec ) { cout << "There is an error in server. code: " << ec.value( ) << endl; } return service.run( );// 阻塞运行 }

    运行结果是这样的:
    78448d7b-b3ae-42fc-9e2e-4dd2fbdac2c2-image.png

    我对boost.asio中几个概念的理解:

    io_service,这就是一个类似事件循环的东西,它为io设备提供服务,故名。不管是套接字、文件还是串口设备,都要使用它的服务。它的run()函数相当于启动了一个事件循环。一旦有消息了,即进行响应。这也是实现异步编程的重要基础。 socket,这个类则是套接字,可以处理TCP或者是UDP请求。有同步以及异步的处理方式,也有带异常以及不带异常的处理方式。 acceptor,接收器,仅仅是服务端使用。相当于其余框架中的listener,作接收用的。

    比较浅显,如果有不当之处,敬请指正。

    read more

关注我们

微博
QQ群