Qml界面渲染机制



  • 渲染层次:

    0_1531798264848_48a56a99-a2a7-4a1e-8d0e-f12d889d74a1-image.png

    渲染循环分三种,basic, windows, and threaded,其中Basic和Windows是单线程的,threaded在单独线程里面渲染场景,qt会根据平台自动选择一种,我电脑上已经验证是threaded

    qt.scenegraph.general: threaded render loop qt.scenegraph.general: Using sg animation driver qt.scenegraph.general: Animation Driver: using vsync: 16.67 ms

    同时可以知道画面垂直同步是16.67 ms,理论60Hz

    线程渲染循环中如何完成一帧

    0_1531798407892_935d0c1f-554a-4cbb-999a-a0966484548d-image.png

    1. A change occurs in the QML scene, causing QQuickItem::update() to be called. This can be the result of for instance an animation or user input. An event is posted to the render thread to initiate a new frame.
    2. The render thread prepares to draw a new frame and makes the OpenGL context current and initiates a block on the GUI thread.
    3. While the render thread is preparing the new frame, the GUI thread calls QQuickItem::updatePolish() to do final touch-up of items before they are rendered.
    4. GUI thread is blocked.
    5. The QQuickWindow::beforeSynchronizing() signal is emitted. Applications can make direct connections (using Qt::DirectConnection) to this signal to do any preparation required before calls to QQuickItem::updatePaintNode().
    6. Synchronization of the QML state into the scene graph. This is done by calling the QQuickItem::updatePaintNode() function on all items that have changed since the previous frame. This is the only time the QML items and the nodes in the scene graph interact.
    7. GUI thread block is released.
    8. The scene graph is rendered:
    9. The QQuickWindow::beforeRendering() signal is emitted. Applications can make direct connections (using Qt::DirectConnection) to this signal to use custom OpenGL calls which will then stack visually beneath the QML scene.
    10. Items that have specified QSGNode::UsePreprocess, will have their QSGNode::preprocess() function invoked.
    11. The renderer processes the nodes and calls OpenGL functions.
    12. The QQuickWindow::afterRendering() signal is emitted. Applications can make direct connections (using Qt::DirectConnection) to this signal to use custom OpenGL calls which will then stack visually over the QML scene.
    13. The rendered frame is swapped and QQuickWindow::frameSwapped() is emitted.
    14. While the render thread is rendering, the GUI is free to advance animations, process events, etc.
      The threaded renderer is currently used by default on Windows with opengl32.dll, Linux with non-Mesa based drivers, macOS, mobile platforms, and Embedded Linux with EGLFS but this is subject to change. It is possible to force use of the threaded renderer by setting QSG_RENDER_LOOP=threaded in the environment.

    非线程渲染

    0_1531798434302_91612780-f2cf-4fdb-b5a1-aeb004a1e7a2-image.png

    查看渲染方式:

    QLoggingCategory::setFilterRules(QStringLiteral("qt.scenegraph.general=true")); qSetMessagePattern("%{category}: %{message}");



  • @青山白云 对,Qt Quick内部实现还是根据屏幕的刷新率来的。不这么做的话,在不同平台上使用Animator做动画速度会出现不一致的现象。所以Qt的Animation Driver也是按照这样的思路规定16.67ms。


 

走马观花

最近的回复

  • 简介 qmake简介 添加第三方库 示例1 - 直接链接库的全路径 示例2 - 路径中包含空格等特殊字符,用引号括起来。 示例3 - 分别指定路径和库 示例4 - 分平台条件链接 原理 影子构建 指定目标路径 指定中间件生成路径 拷贝资源 拷贝资源示例 编译前拷贝 安装 结束语 简介

    本文是《Qt实用技能》系列文章的第二篇,涛哥将教大家,一些qmake的实用技巧。部分地方也会说一下原理,让大家知其然,知其所以然。

    工欲善其事,必先利其器。

    这个系列,全是干货!

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

    qmake简介

    qmake是Qt的构建工具,主要作用是解析pro格式的项目文件、生成编译规则(Makefiles或其它)。

    qmake是一个比较古老的工具,很多功能使用perl脚本实现,涛哥在其它地方就没怎么见过使用perl脚本的代码/项目。

    Qt官方之前开发的Qbs,后来又宣布不再更新,现在又大力支持CMake。。。

    在这样的背景下,qmake依然是当下主要的构建工具,所以qmake的一些技巧还是有必要掌握的。

    qmake本身作为一个可执行程序,也是有一些参数的,但这不是本文的重点,本文的重点都在pro文件里。

    pro文件中,除了常规的组织项目结构外,还可以做很多事情, 比如 指定编译选项、链接选项、制定目标生成规则、扩展编译规则 等等。

    pro文件中的qmake语法,包括 变量声明和使用、内建变量、替换函数、测试函数等,帮助文档都有详细的介绍。

    搜索关键词为qmake, 或者和普通的类查看帮助文档方式一样,光标放在pro文件要查看的变量上,按F1就能看到相应的说明。

    预览

    涛哥就不赘述了,后面用到的会单独说明。

    添加第三方库

    c++开发,使用第三方库也是家常便饭了,这是一个必备的技能。

    这里首选的方法,是使用QtCreator提供的添加库UI。在pro文件里(或者项目文件夹), 鼠标右键->添加库,然后根据自己的需要下一步、下一步点一下即可。

    预览

    熟练的人也可以直接按pro语法(perl语法)写,给LIBS变量赋值。

    下面给几个示例,至于动态库/静态库的差异,大家自己实践吧。

    示例1 - 直接链接库的全路径 LIBS += c:/mylibs/math.lib

    我们都知道windows系统默认的路径分割符是'',但在qmake中要写成'\'才行。qmake也支持写成'/',其它unix系统又都是'/',

    所以干脆都写成'/',方便处理。

    示例2 - 路径中包含空格等特殊字符,用引号括起来。 LIBS += "C:/mylibs/extra libs/extra.lib" 示例3 - 分别指定路径和库 LIBS += "C:/mylibs/extra libs" -lextra

    这里的LIBS指定要链接的库,'-L'是指定链接库的路径,'-l'指定要链接的库名称

    名称可以省略lib前缀和 扩展名后缀,Qt会自动处理。 后缀包括 '.so' '.dll' '.dylib' 等。

    示例4 - 分平台条件链接 win32:LIBS += "C:/mylibs/extra libs/extra.lib" unix:LIBS += "-L/home/user/extra libs" -lextra

    条件链接可以很方便地实现不同平台链接不同的库。

    这里的 win32 unix 是在选择了不同的编译器环境时,qmake分别预置的变量。

    (比如win32平台相关的变量,可以参考msvc的配置文件: [QTDIR]/mkspecs/win32-msvc/qmake.conf 和 [QTDIR]/mkspecs/common/msvc-desktop.conf)

    原理

    Qt内置了一些perl脚本,在执行qmake解析时会包含这些脚本。其中一些脚本会来处理这个LIBS变量,将其转换成编译器/链接器的参数。

    内置的脚本路径在[QTDIR]/mkspecs/features文件夹下,扩展名为prf。

    预览

    后续的很多变量,也是一样的原理, 只是处理方式各不相同。

    很多pro文件的语法、功能实现,都可以参考这些prf来实现。

    (注意:不熟悉的同学,不要乱改prf,容易改坏)

    Qt程序员都知道的一件事:有时候修改了信号/槽相关的代码,不能正常运行,要重新qmake一下,才会生效。

    本质上就是在重新触发[QTDIR]/mkspecs/features/moc.prf这个脚本。

    (多少年了,都没有修好Moc生成问题,可见qmake的古老...)

    影子构建

    影子构建,就是编译生成的产物和源代码在不同的文件夹。这样可以防止源代码文件夹被污染。

    QtCreator默认导入pro工程时,就会生成一个影子构建路径。比如这样:

    预览

    F:\Dev\Qt\build-HelloTaoQuick-Desktop_Qt_5_12_3_MSVC2017_64bit-Debug

    之后编译项目时生成的中间文件及目标文件,都在这个文件夹中。

    这个路径很长,而且编译器或者编译选项不同时都有可能不一样。

    有时候要做一些特定的操作 比如目标exe生成到特定目录、拷贝资源文件等等,直接用这个路径会不太方便/不太可靠,我们需要一些定制。

    指定目标路径 DESTDIR = $$PWD/bin

    通过给DESTDIR变量赋值, 可以指定生成的lib/exe放在哪个目录下

    'PWD'是qmake内置变量,'$$'是内置变量取值的写法。'/bin'是字符串拼接在变量后面。

    更多内置变量可以参考qmake帮助文档,以及这篇文档隐藏的qmake文档

    当然也可以参考那一堆prf和conf文件。

    指定中间件生成路径

    可以通过这几个变量指定中间件生成的路径

    config(debug, debug|release) { OBJECTS_DIR = build/debug/obj MOC_DIR = build/debug/moc RCC_DIR = build/debug/rcc UI_DIR = build/debug/uic } else { OBJECTS_DIR = build/release/obj MOC_DIR = build/release/moc RCC_DIR = build/release/rcc UI_DIR = build/release/uic }

    config(debug, debug|release) 是一个条件表达式,可以理解为

    if (debug === true) { } else if (release == true) { }

    注意: 按照perl语法,那个左大括号'{'不能换行,要和前面的表达式在同一行。(有人自作聪明换行,被坑了呢😄)

    上面这种指定中间件路径的方式,在QtCreator中有默认路径所以没有太大意义,不过在命令行编译时这样写却很有用。

    拷贝资源

    pro可以实现,在编译代码时,拷贝一些文件到指定的路径下

    拷贝资源示例

    这里以TaoQuick为例,来说明:

    我在TaoQuick库目录下,有个叫qmldir的文件,需要在编译代码时自动拷贝到bin目录下。(先别管这个文件干嘛的,下一篇文章会说)

    预览

    关键目录结构如下:

    TaoQuick TaoQuick.pro - bin -TaoQuick - TaoQuickCore TaoQuickCore.pro - Qml qmldir

    那么我在TaoQuickCore.pro文件中的写法如下:

    !equals(_PRO_FILE_PWD_, $$DESTDIR) { copy_qmldir.target = $$DESTDIR/qmldir copy_qmldir.depends = $$_PRO_FILE_PWD_/qmldir win32 { copy_qmldir.target ~= s,/,\\\\,g copy_qmldir.depends ~= s,/,\\\\,g } copy_qmldir.commands = $${QMAKE_COPY_FILE} $${copy_qmldir.depends} $${copy_qmldir.target} QMAKE_EXTRA_TARGETS += copy_qmldir }

    ‘!equals(PRO_FILE_PWD, $$DESTDIR)’ 这一句是执行条件,即: 目标路径不等于pro文件所在路径时 执行下面的操作。

    剩下的事情就是在创建一个"编译目标"(Target),将这个编译目标添加到QMAKE_EXTRA_TARGETS变量中就行了。

    熟悉MakeFiles的同学应该都清楚什么是"目标"。不懂MakeFiles也没关系,这里的目标就理解为自己声明的一个变量即可。

    这个变量有三个很重要的"子变量":

    copy_qmldir.target 指定目标文件所在的路径 (这里理解成要拷贝到哪去)
    copy_qmldir.depends 指定依赖文件所在的路径 (这里理解成从哪里拷贝)
    copy_qmldir.commands 指定拷贝操作的执行命令 (就是怎么拷贝)

    QMAKE_COPY_FILE 这个变量来自前面说过的隐藏的qmake文档

    qmake会在解析pro文件时,自动替换成平台相应的拷贝命令。 windows 平台就是 copy /y

    注意windows的copy指令,路径分隔符得写成 '\'才行。所以有了下面的特殊处理:

    win32 { copy_qmldir.target ~= s,/,\\\\,g copy_qmldir.depends ~= s,/,\\\\,g }

    ‘s,/,\\,g’ 是一个正则表达式,作用是把‘/’替换成‘\’ 。s表示开头,g表示结尾。

    VAR~= REGEXP 是对变量VAR执行REGEXP这个正则表达式

    编译前拷贝

    如果想在编译之前,先把资源拷贝完成,只需要前面的基础上,添加一句

    PRE_TARGETDEPS += $$copy_qmldir.target

    也就是把"目标"加到 PRE_TARGETDEPS变量

    !equals(_PRO_FILE_PWD_, $$DESTDIR) { copy_qmldir.target = $$DESTDIR/qmldir copy_qmldir.depends = $$_PRO_FILE_PWD_/qmldir win32 { copy_qmldir.target ~= s,/,\\\\,g copy_qmldir.depends ~= s,/,\\\\,g } copy_qmldir.commands = $${QMAKE_COPY_FILE} $${copy_qmldir.depends} $${copy_qmldir.target} QMAKE_EXTRA_TARGETS += copy_qmldir PRE_TARGETDEPS += $$copy_qmldir.target } 安装

    pro中还有一种INSTALL功能,可以执行文件拷贝。

    和编译期拷贝 类似,INSTALL用起来更简单无脑一些,而且INSTALL只在执行make install指令时,才会拷贝资源。

    还是以TaoQuick为例, 我有一堆文件,需要在make install时,安装到Qt的Qml路径中

    预览

    如上图所示所有的文件, 除了TaoQuickDesigner.pri, 都要按照这个结构拷贝。

    (这个pri文件是pro文件的一小部分,可以直接在pro中通过include引入。

    pri和pro语法一样,但是qmake不直接识别pri,只识别pro

    pri一般用来写一些公用的部分,让多个pro公用)

    拷贝整个文件夹是一种做法, 当然为了精确地控制要拷贝的内容,可以写成下面这样:

    taoquick_designer.files = $$PWD/designer/TaoQuick.metainfo taoquick_designer.path = $$[QT_INSTALL_QML]/$${uri}/designer toaquick_qmldir.files = $$PWD/qmldir toaquick_qmldir.path = $$[QT_INSTALL_QML]/$${uri} taoquick_qml_buttons.files = $$PWD/BasicComponent/Button/*.qml taoquick_qml_buttons.path = $$[QT_INSTALL_QML]/$${uri}/BasicComponent/Button taoquick_qml_mouse.files = $$PWD/BasicComponent/Mouse/*.qml taoquick_qml_mouse.path = $$[QT_INSTALL_QML]/$${uri}/BasicComponent/Mouse taoquick_qml_others.files = $$PWD/BasicComponent/Others/*.qml taoquick_qml_others.path = $$[QT_INSTALL_QML]/$${uri}/BasicComponent/Others taoquick_qml_progress.files = $$PWD/BasicComponent/Progress/*.qml taoquick_qml_progress.path = $$[QT_INSTALL_QML]/$${uri}/BasicComponent/Progress taoquick_degisner_images.files = $$PWD/designer/images/*.png taoquick_degisner_images.path = $$[QT_INSTALL_QML]/$${uri}/designer/images INSTALLS += taoquick_designer toaquick_qmldir taoquick_qml_buttons taoquick_qml_mouse taoquick_qml_others taoquick_qml_progress taoquick_degisner_images

    自定义一个变量,然后其子变量files指定要拷贝的文件,子变量path指定目标路径。

    把自定义变量加入INSTALLS变量就行了。

    QT_INSTALL_QML也是一个内置变量,默认值为[QTDIR]/qml。

    之后只要执行以下命令,就能完成资源拷贝。

    qmake make make install

    当然QtCreator中也能执行make install

    如下图所示:

    预览

    任意编译器kit都可以,项目->构建步骤->添加build步骤->Make,添加之后在make参数中输入install。最后重新构建工程,即可完成安装。

    结束语

    以上案例,大部分都在TaoQuick项目中实践过,可以去Github参考。

    TaoQuick

    转载声明

    文章出自涛哥的博客

    文章采用 知识共享署名-非商业性使用-相同方式共享 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

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

    read more
  • 简介 界面、数据和逻辑分离 Qt内置的Model-View 整数做model 关于delegate View与Repeater的区别 ListModel 静态ListModel 动态ListModel XmlListModel ObjectModel C++导出Model QList<T> QJsonArray QQmlPropertyMap ListView缺失的灵魂 搜索与排序 选中 拖拽 特效 简介

    本文是《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

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

    read more
  • 简介 Qml内置类型 简单类型 枚举 list var var数组 var回调函数 Qml模块扩展类型 Qml属性 属性的change信号 属性绑定 动态解绑、动态绑定 条件绑定 只读属性 默认属性 属性别名 QQmlProperty 简介

    本文是《Qml组件化编程》系列文章的第八篇,涛哥将教大家,一些Qml中属性的知识和使用技巧。

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

    说Qml属性之前,先来看看Qml中都有哪些类型吧

    Qml内置类型

    Qml本身支持的类型如下图:

    预览

    一共9个,可以在Qml中实用。

    简单类型 Item { property bool doorIsOpened: true property int doorCount: 1 + 2 * 3 property double PI: 3.1415926 property real PI: 3.1415926 property string name: "JaredTao" property url address: "https://jaredtao.github.io" }

    bool double int real string url 这6个简单的类型,C++中也分别有对应的类型,其中string对应QString,url对应QUrl,就不用多说了。

    这里提一下,"1 + 2 * 3" 这种可以在编译期间确定的简单数值表达式,

    Qml引擎会自动帮你计算成7。编译进二进制文件的时候就是“7”,不是“1 + 2 * 3” (就好比C++ 中的constexpr)

    枚举

    枚举可以通过C++注册给Qml使用。5.10以上的版本还可以直接在Qml中定义枚举。

    这里分别示例一下:

    C++注册枚举给Qml使用
    预览

    C++11的作用域枚举,也是可以的:

    预览

    5.12的版本已经不用写Q_DECLARE_METATYPE(BrotherTao::Country)这一句了,旧一点的版本可能需要写上。

    5.10以上版本,Qml中定义枚举:

    预览

    注意 使用枚举必须带上首字母大写的Qml文件名

    (这里枚举可能没有语法高亮,但是能正常用,不要担心,那是QtCreator的问题, 可以不管它)

    list

    list就是一个列表,但是一般用来存Qml的扩展类型,不能存基础类型。基础类型想要存List,应该用下面的var。

    (大部分人用不到这个list,可以跳过)

    这里看一下list的用法:

    Item { states: [ State { name: "activated" }, State { name: "deactivated" } ] }

    这种list的实现方式,是在C++中导出了一个特殊类型的属性,即QQmlListProperty。

    你也可以自己定义一个这样的属性:

    Q_PROPERTY(QQmlListProperty<Fruit> fruit READ fruit)

    C++里面按它的规则实现几个函数,并注册类型后,就可以在Qml中这样用:

    fruit: [ Apple {}, Orange{}, Banana{} ]

    这个大部分人用不到,就不深入讲解了。

    var

    var就相当于js中的var,什么类型都可以存。

    Item { property var aNumber: 100 property var aBool: false property var aString: "Hello world!" property var anotherString: String("#FF008800") property var aColor: Qt.rgba(0.2, 0.3, 0.4, 0.5) property var aRect: Qt.rect(10, 10, 10, 10) property var aPoint: Qt.point(10, 10) property var aSize: Qt.size(10, 10) property var aVector3d: Qt.vector3d(100, 100, 100) property var anArray: [1, 2, 3, "four", "five", (function() { return "six"; })] property var anObject: { "foo": 10, "bar": 20 } property var aFunction: (function() { return "one"; }) }

    这种坑人的东西也可以(涛哥我是坚决不会这么用的):

    Item { property var first: {} // nothing = undefined property var second: {{}} // empty expression block = undefined property var third: ({}) // empty object }

    C++自定义类型、js的内置类型,都可以用var。你可以在Qt的帮助文档中找到js内置类型:

    预览

    这个文档可能不全面,你可以参考第三方js手册,比如mozilla-js-reference

    注意,一般将var换成确切的类型会更好一些,Qml引擎处理var有一个转换的过程,会慢一些。

    接下来涛哥说几个var的典型用法:

    var数组

    预览

    如上图,数组的操作基本和js里的Array一致,参考js的手册就行了。

    但是要注意的一个问题,无论是改变数组中的单个元素的值,还是增加、删除数组中某个元素,都不会触发数组本身的change信号,绑定其length属性也没有用。

    没有change信号,Qml中其它与这个数组关联的地方都没法刷新了。那怎么才能让这个数组刷新呢?

    答案是 修改过后,赋值为自己

    names = names

    预览

    原理很简单,拷贝了一个副本,又放回那个地址了,数组本身变了,触发change信号。

    (不得不吐槽,js真挫,居然还拷贝了一份。性能肯定好不到哪去)

    var回调函数

    这个用案例来说:

    如果你使用过Qml性能探查器(Profiler),就会发现FileDialog这玩意占很多启动时间、占很多内存。

    (涛哥说的是QtQuick.Dialogs里面那个,Qt.labs.platform 那里的实验品从来不用)

    好的做法是,Qml工程中只创建一个,复用它。

    那么问题来了,某个按钮调用了FileDialog, FileDialog按下确定的时候,怎么把结果传回按钮那里?

    这里就要用到回调函数了。按钮调用FileDialog的同时给它一个回调函数,等确定后直接执行回调函数即可。

    下面是涛哥封装的TDialog组件,同时支持创建文件、打开文件、打开多个文件、打开文件夹五种用法。

    //TDialog.qml import QtQuick 2.0 import QtQuick.Dialogs 1.2 Item { //顶层使用Item,不用FileDialog,屏蔽FileDialog内部属性和函数 enum Type { CreateFile, OpenFile, OpenFiles, OpenFolder, } property int __type //参考Qml源码,人为约定 双下划线开头的属性当作私有属性使用,外部不能用。 //点击确定后的回调函数 property var __acceptCallback: function(file) {} FileDialog { id: d folder: shortcuts.home onAccepted: { switch(__type) { case TDialog.Type.CreateFile: __acceptCallback(d.fileUrl) break case TDialog.Type.OpenFile: __acceptCallback(d.fileUrl) break case TDialog.Type.OpenFiles: __acceptCallback(d.fileUrls) break case TDialog.Type.OpenFolder: __acceptCallback(d.folder) break } } } function createFile(title, nameFilters, callback) { __type = TDialog.Type.CreateFile d.selectExisting = false d.selectFolder = false d.selectMultiple = false d.title = title d.nameFilters = nameFilters __acceptCallback = callback d.open() } function openFile(title, nameFilters, callback) { __type = TDialog.Type.OpenFile d.selectExisting = true d.selectFolder = false d.selectMultiple = false d.title = title d.nameFilters = nameFilters __acceptCallback = callback d.open() } function openFiles(title, nameFilters, callback) { __type = TDialog.Type.OpenFiles d.selectExisting = true d.selectFolder = false d.selectMultiple = true d.title = title d.nameFilters = nameFilters __acceptCallback = callback d.open() } function openFolder(title, callback) { __type = TDialog.Type.OpenFolder d.selectExisting = true d.selectFolder = true d.selectMultiple = false d.title = title __acceptCallback = callback d.open() } }

    下面是使用的示例:

    import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 2.5 Window { visible: true width: 640 height: 480 title: qsTr("Hello Dialog") TDialog { id: globalDialog } Row { spacing: 10 Button { text: "create file" onClicked: { globalDialog.createFile("create", ["All files (*.*)"], function(file){ console.log("create file", file) }) } } Button { text: "open Image" onClicked: { globalDialog.openFile("Open one image", ["Image files (*.png *.jpg *.bmp)"], function(file){ console.log("Open one image", file) }) } } Button { text: "open Image" onClicked: { globalDialog.openFiles("Open mulit images", ["Image files (*.png *.jpg *.bmp)"], function(files){ console.log("Open mulit images", files) }) } } Button { text: "open folder" onClicked: { globalDialog.openFolder("Open one folder", function(file){ console.log("Open one folder", file) }) } } } }

    TDialog功能收录在最新的TaoQuick项目中,

    可以参考源代码,或者到github Release页面下载发布包进行体验。(之前MacOS不能用的问题已经修复)

    Qml模块扩展类型

    Qml扩展类型有很多,比如QtQuick模块提供的类型如下:

    date Date value point Value with x and y attributes rect Value with x, y, width and height attributes size Value with width and height attributes

    还有一个非常有用的类型,是QtQml模块提供的Qt:

    预览

    这个Qt自带了很多方法,前面几章提到的Qt.darker和Qt.lighter都是来自这里。

    还有这几个也很实用:

    Qt.openUrlExternally 可以直接打开一个网址,会自动调用系统的默认浏览器,并跳转到相应的界面

    也可以打开一个本地的文件或文件夹,会自动调用系统程序。比如打开一个.txt文件会自动用记事本打开,打开一个文件夹会自动用文件管理器。

    Qt.callLater 可以延迟执行,延迟到Qml引擎的事件循环返回(可以用来规避一些隐藏的bug)。

    Qml属性

    前面介绍类型的过程中,其实已经用了简单的属性定义。

    这里再补充一些前面没有提到的。

    属性的change信号

    写一个普通的属性,隐含的自动生成了一个change信号,信号名字一般是onXxxChanged

    Item { property int value: 12 //这里定义一个属性,并赋初值 //隐含的已经定义了一个change信号: onValueChanged } 属性绑定 Item { id: root property int value1: 12 Item { property int value2: root.value1 * 10 + 4 //属性初值,依赖另一个属性。这就形成了一种绑定关系 } }

    这里的value2依赖于value1的值,就产生了绑定

    所谓的绑定,就是Qml引擎自动做了一个 信号-槽 连接:当onValue1Changed信号发出时,执行 value2 = value1 * 10 + 4。

    就是说value1变了,value2会自动跟着变。

    动态解绑、动态绑定

    有时候并不希望两个属性之间,一直是这种绑定状态,需要暂时断开一下绑定关系(解绑)。

    这时候只要重新赋值即可解绑,比如

    Item { id: item1 property int value1: 12 Item { id: item2 property int value2: item1.value1 * 10 + 4 //属性初值,依赖另一个属性。这就形成了一种绑定关系 } Button { onClicked: { item2.value = 1024; //重新赋值,绑定关系被破坏,不会再随着value1的改变而改变。 } } }

    解绑后,又需要再次绑定,是不是重新赋值回item1.value1就行了呢?

    答案是不对的,再次绑定要用绑定表达式Qt.binding()

    Item { id: item1 property int value1: 12 Item { id: item2 property int value2: item1.value1 * 10 + 4 //属性初值,依赖另一个属性。这就形成了一种绑定关系 } Button { onClicked: { item2.value = 1024; //重新赋值,绑定关系被破坏,不会再随着value1的改变而改变。 } } Button { onClicked: { item2.value = item1.value1 * 10 + 4 //这个是赋值表达式,只执行一次,不是绑定表达式。 item2.value = Qt.binding(function() { return item1.value1 * 10 + 4;}) //这个是绑定表达式。 } } } 条件绑定

    Qml中还提供了一种功能,叫条件绑定 Binding。前面的动态解绑、再绑定可以用下面的方式实现:

    Item { id: item1 property int value1: 12 Item { id: item2 property int value2 } Binding { target: item2 property: "value2" value: item1.value1 * 10 + 4 when: needBind } property bool needBind: true Button { onClicked: { needBind = false; //条件绑定 关闭 item2.value = 1024; //重新赋值。 } } Button { onClicked: { needBind = true; //条件绑定打开 } } } 只读属性

    只读属性就是只能读,不能修改,不产出Change信号。只要在前面写上readonly即可

    readonly property int maxCPUCount: 8 默认属性

    默认属性一般用在组件封装中,比如要封装一个TaoLabel的组件,这个组件有个Text类型的默认属性叫someText

    // TaoLabel.qml import QtQuick 2.0 Text { default property Text someText text: "Hello, " + someText.text }

    那么在外面实例化组件的时候,就可以在TaoLabel内部放一个子Text组件,它会自动关联到someText。

    Item { TaoLable { Text {text: "world"} } } 属性别名

    别名属性一般用来导出组件内部的属性给外部直接修改

    Item { property alias text: t.text Text { id: t } }

    效果与下面的等价,但是别名少一层变量的声明和绑定,效率更高一些。

    Item { id: root property string text Text { id: t text: root.text } } QQmlProperty

    《Qml组件化编程5-Qml与C++交互》一文中,提到了C++访问Qml的两种方式,这里再补充第三种,就是QQmlProperty

    假如有这样的Qml文件,

    // MyItem.qml import QtQuick 2.0 Text { text: "A bit of text" }

    那么在c++种,就可以通过QQmlProperty访问其属性

    #include <QQmlProperty> #include <QGraphicsObject> ... QQuickView view(QUrl::fromLocalFile("MyItem.qml")); QQmlProperty property(view.rootObject(), "font.pixelSize"); qWarning() << "Current pixel size:" << property.read().toInt(); property.write(24); qWarning() << "Pixel size should now be 24:" << property.read().toInt();

    QQmlProperty不仅可以用来访问Qml中的属性,还可以调用其信号、js函数。

    转载声明

    文章出自涛哥的博客

    文章采用 知识共享署名-非商业性使用-相同方式共享 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

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

    read more
  • W

    测试一下,测试一下

    read more

关注我们

微博
QQ群











召唤伊斯特瓦尔