├── Concepts ├── QML.md ├── Qt Quick.md └── Signal&Handler.md ├── Containers ├── Loader.md └── Views.md ├── Controls ├── Controls.md ├── Customizing Controls.md ├── Handlers.md └── UI Forms.md ├── Integrating ├── Context-Properties.md ├── Integrating-QML-and-C++.md ├── Register-QML-Type.md └── Using-C++-Models.md ├── Others ├── DPI-Scaling.md ├── Global-Styles.md └── Translation.md ├── Positioning ├── Anchors.md └── Layouts.md ├── README.md ├── Windows ├── Create-Component-Dynamically.md ├── Inside-Window.md └── Singleton-Objects.md └── images ├── 2020-09-26_16-46-13.png ├── 2020-09-26_17-25-53.png ├── 2020-09-26_17-30-10.png ├── 2020-09-26_17-34-00.png ├── 2020-09-26_17-35-09.png ├── 2020-09-26_23-59-51.png ├── 2020-09-26_23-59-59.png ├── 2020-09-27_00-05-29.png ├── 2020-10-04_21-15-44.png ├── 2020-10-04_21-29-17.png ├── 2020-10-04_22-11-31.png ├── 2020-10-04_22-23-20.png ├── 2020-10-04_22-30-09.png ├── 2020-10-04_22-33-03.png ├── 2020-10-04_22-38-34.png ├── 2020-10-04_22-44-15.png ├── 2020-10-04_22-56-15.png ├── 2020-10-04_23-00-17.png ├── 2020-10-04_23-03-18.png ├── 2020-10-04_23-08-59.png ├── 2020-10-04_23-27-57.png ├── 2020-10-05_14-34-07.png ├── 2020-10-05_14-34-24.png ├── 2020-10-05_14-34-38.png ├── 2020-10-05_14-34-56.png ├── 2020-10-05_14-36-27.png ├── 2020-10-05_15-17-22.png ├── 2020-10-05_15-18-33.png ├── 2020-10-05_20-01-56.png ├── 2020-10-05_20-02-02.png ├── 2020-10-05_20-04-12.png ├── 2020-10-05_20-05-39.png ├── 2020-10-05_20-16-55.png ├── 2020-10-05_20-19-09.png ├── 2020-10-05_23-58-53.png ├── 2020-10-06_14-58-46.png ├── 2020-10-06_15-06-45.png ├── 2020-10-06_15-10-36.png ├── 2020-10-06_15-28-02.png ├── 2020-10-07_11-32-19.png ├── 2020-10-07_11-32-45.png ├── Containers └── modelview-overview.png ├── Controls ├── customizing-button.gif ├── hoverhandler.gif ├── listview-delegate.gif ├── loader-different-page.gif ├── mousearea.gif ├── page-loader.gif └── qtquickcontrols2-busyindicator.png ├── Integrating ├── clipboard.gif ├── cpp-qml-integration-flowchart-cn.jpg ├── cpp-qml-integration-flowchart.png └── loggedIn-property.gif └── Windows ├── child-window.gif ├── dynamic-create.gif └── galbal-toast.gif /Concepts/QML.md: -------------------------------------------------------------------------------- 1 | # What's QML 2 | 3 | 在上一篇介绍中,我们了解到 Qt Quick 项目的 UI 资源是通过 QML 描述的,它与 Duilib 的 xml 起到同样的作用,描述 UI 是如何展示的。但他不仅仅能描述一个窗口或者控件的样式,它还有一些更强大的功能允许我们通过前端即可控制一些业务逻辑。 4 | 5 | ## 项目(Items) 6 | 7 | 除了上文中我们看到的 Window 和我们尚未接触到的 ApplicationWindow 以外,QML 几乎所有其他组件都是基于一个基础 Item 衍生而来的。这里有一幅网友整理的关系图,来自 [https://github.com/CodeBees/QMLClassDiagram](https://github.com/CodeBees/QMLClassDiagram),此图清晰的展示了 QML 所有 Items 的关系 8 | 9 | 10 | 11 | 可以看到,几乎所有 QML 可视的资源几乎都派生于 Item,在 C++ 中实际都是基于 QObject。包括布局(Layouts)、常用控件(Controls),后面我们提到 Item 将泛指这些 Item 派生的子对象。 12 | 13 | ## 组件([Components](https://doc.qt.io/qt-5/qml-qtqml-component.html)) 14 | 15 | QML 是一个全新的描述型语言,书写风格与 Json 较像,但并不是纯 Json 格式。像上一篇文章我们看到的窗口描述文件,它允许我们创建一个根,在根的基础之上再去嵌入其他的内容(Qt Quick 新版本中已经允许使用内联组件,更多请参考 Qt 官方文档)。 16 | 17 | Qt Quick 官方文档中是这样描述 Component 的,`Components are reusable, encapsulated QML types with well-defined interfaces.`。它是一个具有良好的接口、封装好的、可重用的 QML 类型。它最终是一个 `.qml` 文件的形式存在(或者编写内联代码被包含在一个 .qml 文件中以 Component 为根。),下面是一个 Qt Quick 官方文档中的简单示例。 18 | 19 | ```QML 20 | import QtQuick 2.0 21 | 22 | Item { 23 | width: 100; height: 100 24 | } 25 | ``` 26 | 27 | 将以上代码保存为一个单独的 `MyItem.qml` 文件后,我们就可以把它当做一个组件(Component)来引用: 28 | 29 | ```QML 30 | import QtQuick 2.12 31 | import QtQuick.Window 2.12 32 | 33 | Window { 34 | visible: true 35 | width: 640 36 | height: 480 37 | title: qsTr("Hello World") 38 | 39 | MyItem { 40 | 41 | } 42 | } 43 | ``` 44 | 45 | 此时我们的 Window 就具有了一个宽度为 100、高度为 100 的 Item 组件,它只是一个简单的示例,并没有展示任何内容。 46 | 47 | 组件是具有自己的生命周期的,在 MyItem.qml 文件中,我们写下如下代码然后运行应用: 48 | 49 | ```QML 50 | import QtQuick 2.0 51 | 52 | Item { 53 | width: 100; height: 100 54 | 55 | Component.onCompleted: { 56 | console.log('MyItem component onCompleted') 57 | } 58 | 59 | Component.onDestruction: { 60 | console.log('MyItem component onDestruction') 61 | } 62 | } 63 | ``` 64 | 65 | 我们会看到控制台打印了如下内容: 66 | 67 | ``` 68 | qml: MyItem component onCompleted 69 | ``` 70 | 71 | 这代表了组件已经加载完成,我们可以根据外部传入的一些参数对组件中要展示的界面进行初始化。当我们关闭程序时,这个组件随之销毁,在控制台会看到如下打印: 72 | 73 | ``` 74 | qml: MyItem component onDestruction 75 | ``` 76 | 77 | 生命周期的管理很实用,我们经常通过组件的创建完成和销毁机制来对组件内部的一些资源做初始化或与 C++ 层做相应的通讯来保证时序。美中不足的是它并不像 React、Vue 这类前端框架一样,可以更细颗粒度的在组件创建前、创建后、销毁前、销毁后等情况下做不同的操作。 78 | 79 | 以上介绍到的是编译前声明式创建组件,我们还可以通过动态创建组件的方式来满足不同场景的需求 `Qt.createComponent()`,这些我们在后面将会介绍。 80 | 81 | ## 属性绑定([Binding](https://doc.qt.io/qt-5/qml-qtqml-binding.html)) 82 | 83 | 与 Duilib 中 XML 纯描述性语言不同,QML 允许不同的组件属性进行绑定,如下示例: 84 | 85 | ```QML 86 | import QtQuick 2.12 87 | import QtQuick.Window 2.12 88 | 89 | Window { 90 | visible: true 91 | width: 640 92 | height: 480 93 | title: qsTr("Hello World") 94 | 95 | Rectangle { 96 | id: rect1 97 | width: 100 98 | height: 200 99 | color: 'red' 100 | } 101 | 102 | Rectangle { 103 | id: rect2 104 | width: rect1.width 105 | height: rect1.height 106 | color: 'black' 107 | } 108 | } 109 | ``` 110 | 111 | 我们让 rect2 的宽和高与 rect1 保持一致,当 rect1 的宽高变更时,rect2 会跟随变更。我们还没有接触更多的组件,这里仅介绍简单的属性绑定原理,当随着了解的越来越多时,您将了解多更多关于属性绑定的细节。 112 | 113 | ## 信号连接([Connections](https://doc.qt.io/qt-5/qml-qtqml-connections.html)) 114 | 115 | Qt Widgets 中经常提到的就是信号和槽,同样在 QML 中,这个特性被延续了下来。一个简单的示例如下: 116 | 117 | ```QML 118 | MouseArea { 119 | onClicked: { foo(parameters) } 120 | } 121 | ``` 122 | 123 | MouseArea 是一个描述了鼠标区域的组件,它可以响应鼠标的单击事件,MouseArea 有一个默认的 clicked() 信号,如果我们要响应 clicked() 信号,则在信号前加 on 并将信号首字母大写后连接到一起即可,像上面的例子 `on + Clicked` 组成了一个概念上的槽函数。 124 | 125 | 除了这些预定义的信号以外,组件的任意一个属性(Properties)的变更我们都可以通过这种方式检测到,比如在 MouseArea 中有一个属性是 hoverEnabled,设置为 true 表示该鼠标区域接受鼠标悬浮事件,反之则不接受。我们希望检测这个属性的变更可以这样写: 126 | 127 | ```QML 128 | MouseArea { 129 | hoverEnabled: false 130 | onHoverEnabledChanged: { 131 | 132 | } 133 | } 134 | ``` 135 | 136 | 类似的一些组件的 visible、height、width 等属性,都可以通过这种方式检测到。但是某些场景下这样单一的信号连接可能无法满足我们的需求,比如: 137 | 138 | - 需要多次连接到一个信号,当信号发出时多个连接同时响应 139 | - 发出的信号可能并不在同一个文件或区域 140 | - 这些连接目标可能并不是定义在 QML 中 141 | 142 | 这些问题我们可以通过 Connections 来解决,首先我们要给 MouseArea 定义一个 ID: 143 | 144 | ```QML 145 | MouseArea { 146 | id: area 147 | } 148 | ``` 149 | 150 | 随后我们可以在任意可以访问到该 id 的文件或者组件中使用 Connections 连接该信号,如下所示: 151 | 152 | ```QML 153 | Connections { 154 | target: area 155 | onClicked: { 156 | 157 | } 158 | } 159 | ``` 160 | 161 | Qt 5.15 中的新写法 162 | 163 | ```QML 164 | Connections { 165 | target: area 166 | function onClicked(mouse) { 167 | foo(mouse) 168 | } 169 | } 170 | ``` 171 | 172 | ## 状态(Stats) 173 | 174 | 待补充 175 | 176 | ## 动画(Transitions) 177 | 178 | 待补充 179 | 180 | ## QML 中使用 JavaScript 181 | 182 | 在上面的一些示例中,细心的读者不难发现,像 Qt5.15 中推荐的信号连接函数写法是一个 JavaScript 风格的函数。没错,在 QML 中允许你将各种表达式和方法定义为 JavaScript 函数,并且你可以将单独的一个 JavaScript 工具文件导入到工程中使用。先看一个简单的例子: 183 | 184 | ```QML 185 | import QtQuick 2.12 186 | import QtQuick.Window 2.12 187 | 188 | Window { 189 | id: mainWindow 190 | visible: true 191 | width: 640 192 | height: 480 193 | title: qsTr("Hello World") 194 | 195 | Rectangle { 196 | id: rect 197 | width: 100 198 | height: 100 199 | color: mainWindow.width > 640 ? 'red' : 'black' 200 | } 201 | } 202 | ``` 203 | 204 | 上面例子中 Rectangle 的 color 属性代表该矩形区域的背景色,我们使用了 JavaScript 的三目运算符来判断,当主窗口的宽度 width 属性大于 640 的时候,color 属性为 red,否则 color 属性为 black。你可以通过拖动窗口大小来查看一下左上角矩形区域的颜色变化。 205 | 206 | 207 | 208 | 窗口宽度大于 640 后矩形区域颜色变色红色。 209 | 210 | 211 | 212 | 你同样可以定义一个 JavaScript 函数来帮助你处理 QML 语法处理不了的事情,比如做一个数据的排序、比如使用 JavaScript 中的 Date() 来处理时间等等。 213 | 214 | ```QML 215 | import QtQuick 2.12 216 | 217 | Item { 218 | function fibonacci(n){ 219 | var arr = [0, 1]; 220 | for (var i = 2; i < n + 1; i++) 221 | arr.push(arr[i - 2] + arr[i -1]); 222 | 223 | return arr; 224 | } 225 | TapHandler { 226 | onTapped: console.log(fibonacci(10)) 227 | } 228 | } 229 | ``` 230 | 231 | 上面例子中,`fibonacci` 函数就是一个纯 JavaScript 函数,当 onTapped 响应时调用了该方法打印一个结果。 232 | 233 | 有时你希望将 JavaScript 相关的方法都存放到一个文件中,或者可能团队中有专人在负责 JavaScript 相关的开发,你可以把他们写好的 JavaScript 文件通过导入的方式导入到自己工程中使用。要使用这个单独的文件,你需要先将 js 文件导入到工程中,如下所示: 234 | 235 | 236 | 237 | 选择你自己的 js 文件后导入,此时在你需要使用该 js 文件的位置使用 import 语法导入该 js 文件到 QML 中。 238 | 239 | ```QML 240 | import QtQuick 2.12 241 | import QtQuick.Window 2.12 242 | 243 | import 'calc.js' as MathFunction 244 | 245 | Window { 246 | id: mainWindow 247 | visible: true 248 | width: 640 249 | height: 480 250 | title: qsTr("Hello World") 251 | 252 | Rectangle { 253 | id: rect 254 | width: 100 255 | height: 100 256 | color: MathFunction.add(width, height) > 200 ? 'red' : 'black' 257 | } 258 | } 259 | ``` 260 | 261 | 上面例子中我们将同级目录下的 calc.js 导入到 main.qml 文件中并重命名为 MathFunction,其实里面就一个 add 方法提供我们做一个简单的加法运算。在 Rectangle 的 color 属性中,我们使用了该方法来计算 Rectangle 的宽高之和是否超过 200 以决定显示为什么颜色。 262 | 263 | 以上是一些 JavaScript 在 QML 中的应用示例,有了 JavaScript 以后,QML 变得异常强大,好像没有什么做不到的事情了,但实际不是的,这里有一些 JavaScript 环境的限制,请参考官方文档:[JavaScript Environment Restrictions](https://doc.qt.io/qt-5/qtqml-javascript-hostenvironment.html#javascript-environment-restrictions) 264 | 265 | ## 编码风格 266 | 267 | 以上为 QML 的一些基础语法和特性,在本章最后推荐大家参考 Qt 官方对于 QML 的代码风格的推荐 [QML Coding Conventions](https://doc.qt.io/qt-5/qml-codingconventions.html) -------------------------------------------------------------------------------- /Concepts/Qt Quick.md: -------------------------------------------------------------------------------- 1 | # What's Qt Quick 2 | 3 | Qt Quick 与 Qt Widgets 属于同级事物,但使用方式有明显差异,Qt Widgets 更像 MFC,他使用 C++ 实现所有 UI 的展示,而 Qt Quick 使用 QML 描述所有 UI 的展示效果。三者均可以通过设计器进行布局、控件的拖拽安置,只不过设计器略有不同。 4 | 5 | ## 通过导航创建一个 Qt Quick 工程 6 | 7 | 打开 Qt Creator,点击左上角文件->新建文件或项目,选择 Application 菜单即可根据需要选择要创建的 Qt Quick 项目。 8 | 9 | 10 | 11 | 以一个 Qt Quick Application - Empty 工程为例,我们查看一下其目录结构。 12 | 13 | 14 | 15 | 像常规 C++ 项目一样,会生成一个 main.cpp 的入口文件,这个文件里面的内容同样包含了 main 函数入口,如下所示: 16 | 17 | ```C++ 18 | #include 19 | #include 20 | 21 | int main(int argc, char *argv[]) 22 | { 23 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 24 | 25 | QGuiApplication app(argc, argv); 26 | 27 | QQmlApplicationEngine engine; 28 | const QUrl url(QStringLiteral("qrc:/main.qml")); 29 | QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, 30 | &app, [url](QObject *obj, const QUrl &objUrl) { 31 | if (!obj && url == objUrl) 32 | QCoreApplication::exit(-1); 33 | }, Qt::QueuedConnection); 34 | engine.load(url); 35 | 36 | return app.exec(); 37 | } 38 | ``` 39 | 40 | 与传统 Qt Widgets 不同的是,这里并不是创建一个 Widget Window,而是使用 QQmlApplicationEngine 对象加载了一个 `main.qml` 的 UI 资源描述文件。这个 `main.qml` 文件就是上面图中我们看到的使用 QML 语法描述的窗口信息。 41 | 42 | ```QML 43 | import QtQuick 2.12 44 | import QtQuick.Window 2.12 45 | 46 | Window { 47 | visible: true 48 | width: 640 49 | height: 480 50 | title: qsTr("Hello World") 51 | } 52 | ``` 53 | 54 | 这个文件被放到了 qrc 资源文件中的根目录,所以使用 `qrc:/main.qml` 就可以索引到该文件。想快速获得该文件的路径,可以在该文件上点右键复制 URL 地址: 55 | 56 | 57 | 58 | 运行该项目后,你就可以看到一个宽度为 640、高度为 480、窗口名称为 Hello world 的窗口,这就是最简单的一个 Qt Quick 构建的窗口。 59 | 60 | 61 | 62 | 这个窗口全部使用 QML 语言来进行描述,而 QML 语言又是什么,它的编写规则又是如何,请参考 [QML](QML.md) 63 | -------------------------------------------------------------------------------- /Concepts/Signal&Handler.md: -------------------------------------------------------------------------------- 1 | # Signal and Handler Event System 2 | 3 | Qt Quick 像传统 Qt Widgets 一样将信号与槽的能力沿用了下来,虽然我们可以这样理解,但是实现机制是不同的。Qt Quick 中使用 QML 描述 UI 对象和一些非可见的组件,当它们需要响应不同的事件时同样是触发一个信号,而响应信号的位置我们通常叫做信号处理程序(类似槽函数) 4 | 5 | ## 使用处理程序接收信号 6 | 7 | 通常情况下,一个按钮会有单击信号,这个信号的名字叫做 clicked,当我们需要响应这个信号的时候可以通过在信号名称前 + on 并在后面紧跟首字母大写的信号名,即可响应。如下所示: 8 | 9 | ```QML 10 | import QtQuick 2.15 11 | import QtQuick.Controls 2.15 12 | 13 | Rectangle { 14 | id: rect 15 | width: 250; height: 250 16 | 17 | Button { 18 | anchors.bottom: parent.bottom 19 | anchors.horizontalCenter: parent.horizontalCenter 20 | text: "Change color!" 21 | onClicked: { 22 | rect.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1); 23 | } 24 | } 25 | } 26 | ``` 27 | 28 | 这个例子中当按钮被点击,我们将其父容器的矩形颜色设置了一个随机值。 29 | 30 | ### 属性变更信号处理程序 31 | 32 | 除了传统的信号以外,QML 中所有 Item 子项都具有自己的属性,每一个属性的变更我们都可以通过 on + Property + Changed 关键字来响应属性的变更,下面的例子中描述了当 TapHandler 的 pressed 属性变更时我们打印一个日志到控制台。 33 | 34 | ```QML 35 | import QtQuick 2.15 36 | 37 | Rectangle { 38 | id: rect 39 | width: 100; height: 100 40 | 41 | TapHandler { 42 | onPressedChanged: console.log("taphandler pressed?", pressed) 43 | } 44 | } 45 | ``` 46 | 47 | ### 使用 Connections 处理信号 48 | 49 | 同样的,我们可以在外部响应一个 Item 子项的信号或者属性变更,使用 Connections 关键字,设置其 target 目标即可关注其各种属性或信号的变更,将需要关注的变更以 on + Signal or Property + Changed 关键字响应即可,如下所示: 50 | 51 | ```QML 52 | import QtQuick 2.15 53 | import QtQuick.Controls 2.15 54 | 55 | Rectangle { 56 | id: rect 57 | width: 250; height: 250 58 | 59 | Button { 60 | id: button 61 | anchors.bottom: parent.bottom 62 | anchors.horizontalCenter: parent.horizontalCenter 63 | text: "Change color!" 64 | } 65 | 66 | Connections { 67 | target: button 68 | function onClicked(): { 69 | rect.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1); 70 | } 71 | } 72 | } 73 | ``` 74 | 75 | ### 附加信号处理程序 76 | 77 | 除了控件自有信号和属性的变更通知,每一个组件都有自己的加载完成和销毁的通知,Qt 文档中以 Attached signal 表示他们。他们以 Component.on* 开头,如下所示: 78 | 79 | ```QML 80 | import QtQuick 2.15 81 | 82 | Rectangle { 83 | width: 200; height: 200 84 | color: Qt.rgba(Qt.random(), Qt.random(), Qt.random(), 1) 85 | 86 | Component.onCompleted: { 87 | console.log("The rectangle's color is", color) 88 | } 89 | } 90 | ``` 91 | 92 | ## 给自定义组件添加信号 93 | 94 | 通常情况下一个 Qt Quick 自带的组件已经包含了丰富的信号和属性,响应他们已经满足大部分场景的需求,但当我们自己封装的组件需要一个信号让外接根据变更连接时,我们可能需要自己来自定义信号。通过 `signal` 关键字来声明自己的信号,需要触发信号时只需要像调用一个函数一样直接调用信号名称即可。信号可以携带参数,下面的例子中携带了两个 x 和 y 的位置信息参数供外部获取: 95 | 96 | ```QML 97 | // SquareButton.qml 98 | import QtQuick 2.15 99 | 100 | Rectangle { 101 | id: root 102 | 103 | signal activated(real xPosition, real yPosition) 104 | property point mouseXY 105 | property int side: 100 106 | width: side; height: side 107 | 108 | TapHandler { 109 | id: handler 110 | onTapped: root.activated(mouseXY.x, mouseXY.y) 111 | onPressedChanged: mouseXY = handler.point.position 112 | } 113 | } 114 | ``` 115 | 116 | 当外部使用这个组件的时候,我们可以像处理组件自带信号一样处理我们添加的自定义信号,同样格式也是 on + SignalName 117 | 118 | ```QML 119 | // myapplication.qml 120 | SquareButton { 121 | onActivated: console.log("Activated at " + xPosition + "," + yPosition) 122 | } 123 | ``` 124 | 125 | ## 连接一个信号到指定的方法 126 | 127 | 有时我们可能无法对一个动态创建的组件去写信号连接的处理程序,因为他们还没有创建出来,我们还不知道组件的名称,该如何链接? 128 | 129 | 其实除了信号处理程序以外,我们还可以将信号与一个函数进行连接,通过信号自带的 `connect` 方法我们可以连接该信号到一个函数上,当信号触发时该函数自动执行。这种方法在动态创建组件时非常实用。如下所示: 130 | 131 | ```QML 132 | import QtQuick 2.15 133 | 134 | Rectangle { 135 | id: relay 136 | 137 | signal messageReceived(string person, string notice) 138 | 139 | Component.onCompleted: { 140 | relay.messageReceived.connect(sendToPost) 141 | relay.messageReceived.connect(sendToTelegraph) 142 | relay.messageReceived.connect(sendToEmail) 143 | relay.messageReceived("Tom", "Happy Birthday") 144 | } 145 | 146 | function sendToPost(person, notice) { 147 | console.log("Sending to post: " + person + ", " + notice) 148 | } 149 | function sendToTelegraph(person, notice) { 150 | console.log("Sending to telegraph: " + person + ", " + notice) 151 | } 152 | function sendToEmail(person, notice) { 153 | console.log("Sending to email: " + person + ", " + notice) 154 | } 155 | } 156 | ``` 157 | 158 | 当我们不在需要这些函数响应信号时,你需要将信号与函数断开链接,使用 `disconnect` 关键字来断开信号与方法的连接: 159 | 160 | ```QML 161 | Rectangle { 162 | id: relay 163 | //... 164 | 165 | function removeTelegraphSignal() { 166 | relay.messageReceived.disconnect(sendToTelegraph) 167 | } 168 | } 169 | ``` 170 | 171 | ### 信号与信号之间连接 172 | 173 | 信号可以连接一个信号处理程序(槽函数)或一个具体的方法(JavaScript function),像 Qt Widgets 一样,我们也可以将两个信号连接到一起,让信号 A 触发信号 B,这种场景不是很常用,以下为具体的示例: 174 | 175 | ```QML 176 | import QtQuick 2.15 177 | 178 | Rectangle { 179 | id: forwarder 180 | width: 100; height: 100 181 | 182 | signal send() 183 | onSend: console.log("Send clicked") 184 | 185 | TapHandler { 186 | id: mousearea 187 | anchors.fill: parent 188 | onTapped: console.log("Mouse clicked") 189 | } 190 | 191 | Component.onCompleted: { 192 | mousearea.tapped.connect(send) 193 | } 194 | } 195 | ``` 196 | 197 | 我们将 TapHandler 组件的 tapped 信号与父节点 Rectangle 的 send 信号进行了连接,这样当 tapped 信号触发,也同时回触发 send 信号。 198 | 199 | -------------------------------------------------------------------------------- /Containers/Loader.md: -------------------------------------------------------------------------------- 1 | # Loader 2 | 3 | 到目前为止,我们一直在使用声明式方式来加载一个组件,当有些组件需要被动态替换时声明式的方式就无法满足我们的需求了,Loader 可以帮忙我们解决这个问题。它可以动态的从文件或者内联组件中加载一个子项。这在一些单页面应用中非常实用,它类似一些前端框架的单页面应用中根据不同 router 改变不同页面的行为。严格意义上讲,他并不算是一种容器,但是它可以具备一种容纳组件的能力。 4 | 5 | ## 加载一个组件 6 | 7 | ```QML 8 | import QtQuick 2.12 9 | import QtQuick.Window 2.12 10 | import QtQuick.Controls 2.12 11 | import QtQuick.Layouts 1.12 12 | 13 | Window { 14 | visible: true 15 | width: 640 16 | height: 480 17 | title: qsTr('ListView example') 18 | MouseArea { 19 | anchors.fill: parent 20 | onClicked: componentLoader.sourceComponent = newComponent 21 | } 22 | 23 | Loader { 24 | id: componentLoader 25 | anchors.fill: parent 26 | } 27 | Component { 28 | id: newComponent 29 | Item { 30 | anchors.fill: parent 31 | Label { 32 | anchors.centerIn: parent 33 | font.pixelSize: 24 34 | text: 'New Page' 35 | } 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | 上面的示例代码中,我们创建了一个 Loader 和 MouseArea 区域充满整个窗口,当我们点击窗口任意位置的时候,Loader 会加载 `newComponent`,newComponent 中包含了一个简单的 Label 展示了一行文字。演示效果如下: 42 | 43 | 44 | 45 | ## 从文件加载不同组件 46 | 47 | 上面的例子非常简单,但是它并没有什么实质性作用,我们通过一个真是项目中可能会使用到的场景来讲述 Loader 的作用。 48 | 49 | 一些传统应用中,登录页面和实际运行时页面可能是不同的两个窗口,而如果使用 Qt Quick 来设计时,你可能会将他们设计为一个单页面的应用,就是登录和运行时都使用同一个窗口宿主,只不过在登录前我们展示登录页面,登录后我们展示实际的应用能力页面。为了演示这个功能,我们分别创建两个自定义页面,通过 Loader 来演示如何动态切换它们。 50 | 51 | - LoginPage 负责展示登录页的内容 52 | - MainPage 负责展示实际应用中的内容 53 | 54 | ```QML 55 | // LoginPage.qml 56 | import QtQuick 2.0 57 | import QtQuick.Controls 2.12 58 | import QtQuick.Layouts 1.12 59 | 60 | Item { 61 | anchors.fill: parent 62 | 63 | ColumnLayout { 64 | anchors.centerIn: parent 65 | 66 | TextField { 67 | Layout.fillWidth: true 68 | placeholderText: 'Username' 69 | } 70 | 71 | TextField { 72 | Layout.fillWidth: true 73 | placeholderText: 'Password' 74 | } 75 | 76 | Button { 77 | Layout.fillWidth: true 78 | text: 'Login' 79 | onClicked: pageLoader.setSource(Qt.resolvedUrl('qrc:/MainPage.qml')) 80 | } 81 | } 82 | } 83 | ``` 84 | 85 | ```QML 86 | // MainPage.qml 87 | import QtQuick 2.0 88 | import QtQuick.Controls 2.12 89 | 90 | Item { 91 | anchors.fill: parent 92 | Label { 93 | anchors.centerIn: parent 94 | font.pixelSize: 30 95 | text: 'Welcome my app' 96 | } 97 | } 98 | ``` 99 | 100 | 在 main.qml 中,同样我们使用一个 Loader 充满整个父容器,并且在 Window 加载完成时,我们让 Loader 默认加载 LoginPage.qml 页面: 101 | 102 | ```QML 103 | import QtQuick 2.12 104 | import QtQuick.Window 2.12 105 | import QtQuick.Controls 2.12 106 | import QtQuick.Layouts 1.12 107 | 108 | Window { 109 | visible: true 110 | width: 640 111 | height: 480 112 | title: qsTr('Hello world') 113 | 114 | Component.onCompleted: { 115 | // 默认加载 Login 页面 116 | pageLoader.setSource(Qt.resolvedUrl('qrc:/LoginPage.qml')) 117 | } 118 | 119 | Loader { 120 | id: pageLoader 121 | anchors.fill: parent 122 | } 123 | } 124 | ``` 125 | 126 | 此时运行程序,LoginPage 会映入我们眼帘,当我们点击了 Login 按钮后(这里没有做任何校验,主要为了演示 Loader 功能),我们通过 Loader 的 setSource 接口设置了新的组件。演示效果如下: 127 | 128 | 129 | 130 | 当我们加载了 MainPage.qml 后,LoginPage 还存在吗?这是一个很好的问题,为了验证 LoginPage 是否存在,我们可以关注组件的 onDestruction 事件来判断加载新的组件后之前的组件是不是被销毁掉了。 131 | 132 | ```QML 133 | import QtQuick 2.0 134 | import QtQuick.Controls 2.12 135 | import QtQuick.Layouts 1.12 136 | 137 | Item { 138 | anchors.fill: parent 139 | 140 | Component.onDestruction: { 141 | console.log('Login page destructed.') 142 | } 143 | 144 | // .... 145 | } 146 | ``` 147 | 148 | 当我们运行程序并点击 Login 按钮后,可以看到控制台打印了 `qml: Login page destructed.`,表示 LoginPage 已经销毁掉了。所以我们不需要太操心内存管理的问题,Loader 已经全权处理了。 149 | 150 | -------------------------------------------------------------------------------- /Containers/Views.md: -------------------------------------------------------------------------------- 1 | # Views 2 | 3 | 除了一些固定视觉资源的展示外,很多情况下我们还是要通过列表或其他方式来展示一组数据,比如一个成员列表等。Qt Quick 为我们提供了一些视图组件如 ListView、GridView 等,通过它们可以轻松的展示一组指定格式的数据。我们以 ListView 为例描述如何在 Qt Quick 中使用它们。 4 | 5 | ## 数据、视图、委托 6 | 7 | Qt Quick 的视图将显示和数据分离,数据使用 Model 描述,而 ListView 仅仅是一个容器,至于数据如何显示则依赖 Delegate 委托。以下是三者不同的关系: 8 | 9 | - Model 数据模型 - 包含有指定格式的数据,QML 提供了部分类型提供快速创建一个 Model 10 | - View 视图 - 用于显示数据的容器,如 ListView、GridView 等 11 | - Delegate 委托 - 描述了数据如何在 View 中展示,可以通过委托访问数据同时也可以通过委托来修改模型中的数据 12 | 13 | 其中数据的流向 Qt 官方文档中给出了这样一幅图: 14 | 15 | 16 | 17 | 数据的变动会让 View 进行重绘,重绘过程中 View 会根据 Delegate 给出的样式进行绘制,委托中可以直接访问模型中的数据用来展示不同的数据内容。下面通过一个简单的示例来描述他们是如何工作的。 18 | 19 | ## 展示一组数据 20 | 21 | 我们在窗口中放置一个 ListView,让它在 Frame 中主要是为了显示一个边框使我们可以清楚的看到 ListView 所在窗口的区域。 22 | 23 | ```QML 24 | import QtQuick 2.12 25 | import QtQuick.Window 2.12 26 | import QtQuick.Controls 2.12 27 | 28 | Window { 29 | visible: true 30 | width: 640 31 | height: 480 32 | title: qsTr('ListView example') 33 | Frame { 34 | width: 300 35 | height: parent.height 36 | anchors.centerIn: parent 37 | ListView { // 用于显示数据的 view 38 | anchors.fill: parent 39 | model: ListModel { // 用于插入数据的 model 40 | id: listModel 41 | } 42 | delegate: ItemDelegate {// 用于描述数据如何展示的 delegate 43 | height: 32 44 | width: parent.width 45 | Label { 46 | anchors.verticalCenter: parent.verticalCenter 47 | anchors.leftMargin: 10 48 | text: qsTr('List item %1').arg(model.index) // 使用模型中的数据显示在 item 中 49 | } 50 | onClicked: { 51 | console.log('Clicked item: ', model.index) 52 | } 53 | } 54 | Component.onCompleted: { 55 | // ListView 加载完成时,在 listModel 中插入 100 条数据 56 | for (let i = 0; i < 100; i++) { 57 | listModel.append({ index: i }) 58 | } 59 | } 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | ListView 中我们指定了一个 ListModel 分配给 model 属性,指定了一个 ItemDelegate 分配给 delegate 属性。 66 | 67 | 在 ListView 加载完毕后(Component.onCompleted)我们使用 listModel 对象给模型插入了 100 条数据。 68 | 69 | 在 ItemDelegate 中我们设置了一下每个 Item 的宽高,并放置了一个 Label 用于显示模型中的数据。最终效果如下: 70 | 71 | 72 | 73 | 使用鼠标滚轮滚动或者鼠标拖拽都可以滚动 ListView 来查看下面的数据,如果你的程序运行在一个可触控的设备上,你可以通过触控滑动来查看列表中的数据。 74 | 75 | ItemDelegate 是一个 QML 自带的类型,实现了一些简单的 ListItem 该有的能力比如点击后背景变色高亮等。你可以使用任何其他可视化组件替代它,比如 Rectangle 等。 76 | 77 | 在 ItemDelegate 中我们添加了其 clicked 信号的处理程序,用于在点击某个 Item 的时候做出响应,当我们点击不同的 Item 时会在控制台打印不同 Item 的索引。如下所示: 78 | 79 | ``` 80 | qml: Clicked item: 7 81 | qml: Clicked item: 8 82 | qml: Clicked item: 9 83 | qml: Clicked item: 10 84 | ``` 85 | 86 | ## 独立 Delegate 组件 87 | 88 | 有时 ListView 中的每一个子项可能包含很多能力,比如展示一组数据、对单独的数据做删改等操作,这时 Delegate 的样式代码可能会比较多,都放在 ListView 中去编写会导致代码结构可读性很差。你可以将 Delegate 要展示的样式单独使用一个文件来描述,ListView 当做组件将其引入,上面示例中,我们将 ItemDelegate 单独为一个 ListItem.qml 文件,并增加了一些功能。如下所示: 89 | 90 | ```QML 91 | // ListItem.qml 92 | import QtQuick 2.0 93 | import QtQuick.Controls 2.12 94 | import QtQuick.Layouts 1.12 95 | 96 | ItemDelegate {// 用于描述数据如何展示的 delegate 97 | RowLayout { 98 | width: parent.width 99 | anchors.verticalCenter: parent.verticalCenter 100 | Label { 101 | Layout.alignment: Qt.AlignVCenter 102 | text: qsTr('List item %1').arg(model.index) // 使用模型中的数据显示在 item 中 103 | } 104 | Button { 105 | id: btnGetIndex 106 | Layout.preferredHeight: 20 107 | Layout.preferredWidth: 60 108 | Layout.alignment: Qt.AlignRight | Qt.AlignVCenter 109 | visible: false 110 | text: qsTr('Index') 111 | onClicked: console.log('Clicked item: ', model.index) 112 | } 113 | } 114 | onHoveredChanged: { 115 | btnGetIndex.visible = hovered 116 | } 117 | } 118 | ``` 119 | 120 | 在 ListView 中,你可以直接引入这个组件作为他的委托(请保证文件在同一目录,否则你需要使用 import 语句引入指定组件) 121 | 122 | ```QML 123 | import QtQuick 2.12 124 | import QtQuick.Window 2.12 125 | import QtQuick.Controls 2.12 126 | import QtQuick.Layouts 1.12 127 | 128 | Window { 129 | visible: true 130 | width: 640 131 | height: 480 132 | title: qsTr('ListView example') 133 | Frame { 134 | width: 300 135 | height: parent.height 136 | anchors.centerIn: parent 137 | ListView { 138 | anchors.fill: parent 139 | model: ListModel { 140 | id: listModel 141 | } 142 | delegate: ListItem { // 使用自定义的 ListItem 143 | height: 32 144 | width: parent.width 145 | } 146 | Component.onCompleted: { 147 | for (let i = 0; i < 100; i++) { 148 | listModel.append({ index: i }) 149 | } 150 | } 151 | } 152 | } 153 | } 154 | ``` 155 | 156 | 我们在 ListItem.qml 中描述的委托增加了获取当前索引的按钮,并让鼠标悬浮时显示该按钮、鼠标移开时隐藏。当点击里面的按钮时我们打印一下当前 Item 的索引,演示效果如下: 157 | 158 | 159 | 160 | ## 总结 161 | 162 | 传统的一组数据展示通过上面的方法已经基本上满足需求,谨记 Model、View、Delegate 之间的关系会让你自如的使用它们。通过上面的示例中我们不难看出,数据是前端驱动的,但往往一些数据是通过后端 C++ 层来输出的,该如何将 C++ 中的数据传递到前端来使用将是我们后面要介绍的内容。 163 | -------------------------------------------------------------------------------- /Controls/Controls.md: -------------------------------------------------------------------------------- 1 | # Qt Quick Controls 2 | 3 | Qt Quick Controls 是目前来看提供的基础控件能力最完整的集合。它不像一些 React、Vue 这种前端 UI 框架仅仅提供基础能力,而由外部团队去完成交互控件的设计和实现(如 Ant Design 等类似视觉交互语言)而是将一些常用的基础交互能力的控件全部抽象出来提供开发者使用。 4 | 5 | 当我们使用 Duilib 或者 MFC 等类似 UI 框架时,如果需要实现一个 Loading 状态的图标,我们需要自己组织图片、使用基础控件能力去加载一个 gif 图像来实现类似功能。而 Qt Quick Controls 则直接提供了一个 `BusyIndicator Control` 提供开发者快速引入到项目中,并且它的样式你是可以高度自定义的。如下所示: 6 | 7 | 8 | 9 | 在 Qt 官方文档中,他们对 Qt Quick Controls 做了如下分类。 10 | 11 | | 控件类型 | 说明 | 12 | | -- |-- | 13 | | Button Controls | 按钮类型控件,Button、CheckBox、Switch 等 | 14 | | Container Controls | 容器类控件,Frame、ScrollView、TabBar 等 | 15 | | Delegate Controls | 委托类控件,ItemDelegate、CheckDelegate 等 | 16 | | Indicator Controls | 指示器类控件,BusyIndicator、ProgressBar 等 | 17 | | Input Controls | 输入类控件,TextField、ComboBox、Slider 等 | 18 | | Menu Controls | 菜单类控件,Menu、MenuItem、MenuBar 等 | 19 | | Navigation Controls | 导航类控件,Drawer、TabButton 等 | 20 | | Popup Controls | 弹出类控件,Dialog、Popup、Tooltip 等 | 21 | | Separator Controls | 分隔符控件,MenuSeparator、ToolSeparator | 22 | 23 | 在 Qt 的安装目录下的 Examples 文件夹中,有一个名为 `gallery` 的工程,工程演示了几乎所有控件的使用示例: 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ## 控件的事件响应 36 | 37 | 控件的存在既是为了人机交互,每当我们点下一个按钮、滑动一个页面、拖动一个进度条时,都会产生对应的事件,处理这些事件可以达到我们交互的需求。以按钮的点击事件为例,当我们点击一个按钮时,我们在控制台打印一些内容: 38 | 39 | ```QML 40 | import QtQuick 2.12 41 | import QtQuick.Window 2.12 42 | import QtQuick.Controls 2.12 43 | 44 | Window { 45 | visible: true 46 | width: 640 47 | height: 480 48 | title: qsTr('Hello world') 49 | 50 | Button { 51 | anchors.centerIn: parent 52 | text: 'Click me' 53 | onClicked: { 54 | console.log('Button clikced.') 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | 上面代码中,我们响应了按钮的点击事件,实际该按钮有一个 clicked 信号,当我们点击后触发该信号的处理程序 onClicked,随后在控制台打印了 `Button clikced.` 61 | 62 | 同样的,再举一个 Slider 的例子,Slider 是一个滑动条,当我们拖动滑动块的时候,会触发 Slider 的 valueChanged 信号,我们只需要响应这个信号即可观测到数据的变化 63 | 64 | ```QML 65 | Slider { 66 | onValueChanged: { 67 | console.log('Slider values changed: ', value) 68 | } 69 | } 70 | ``` 71 | 72 | 拖动滑动条后控制台打印: 73 | 74 | ``` 75 | qml: Slider values changed: 0.14375 76 | qml: Slider values changed: 0.20625 77 | qml: Slider values changed: 0.225 78 | qml: Slider values changed: 0.525 79 | qml: Slider values changed: 0.7875 80 | ``` 81 | 82 | ## 基础数据变更响应 83 | 84 | 在前文提到的 QML 基础中,我们了解到几乎所有控件都是派生于 Item,Item 有一些公共的属性,比如 visible、enabled、focus、height、width、x、y 等属性,我们同样可以检测这些属性的变更来做不同的事情,比如我们在点击按钮后将按钮禁用,通过 onEnabledChanged 事件即可检测到这一变更。 85 | 86 | ```QML 87 | Button { 88 | text: 'Click me' 89 | onClicked: { 90 | console.log('Button clikced.') 91 | enabled = false 92 | } 93 | onEnabledChanged: { 94 | console.log('Button enabled changed, enabled: ', enabled) 95 | } 96 | } 97 | ``` 98 | 99 | 示例中我们点击按钮后打印一句话并将按钮的 enabled 属性设置为 false,同时我们响应了 enabledChanged 信号,打印了 enabled 属性变更的通知,控制台将打印如下内容: 100 | 101 | ``` 102 | qml: Button clikced. 103 | qml: Button enabled changed, enabled: false 104 | ``` 105 | 106 | 同理其他基础属性一样可以通过这种方式来处理。除了在控件内部直接 on* 的方式响应属性变更,我们同样可以在外部使用 Connections 来连接这些信号: 107 | 108 | ```QML 109 | Button { 110 | id: btn1 111 | text: 'Click me' 112 | onClicked: { 113 | console.log('Button clikced.') 114 | enabled = false 115 | } 116 | } 117 | 118 | Connections { 119 | target: btn1 120 | onEnabledChanged: { 121 | console.log('Button enabled changed, enabled: ', enabled) 122 | } 123 | } 124 | ``` 125 | 126 | 我们使用 Connections 连接了 btn1 的 enabledChanged 信号,这个效果是一样的。 127 | 128 | ## 总结 129 | 130 | Qt Quick Controls 种类繁多,Qt 文档中有非常详细的说明,我们可能无法一一列举这些控件的使用方式,但是通过文档可以快速了解这些控件都具有哪些能力、哪些属性供我们实现需求。在后面的更新中,除了一些特殊控件的使用我们会再补充一些外,对于控件的基础使用可能不会再过多的赘述,把它用到你的项目中,可能你的理解会更加深刻。 131 | -------------------------------------------------------------------------------- /Controls/Customizing Controls.md: -------------------------------------------------------------------------------- 1 | # Customizing Qt Quick Controls(自定义控件样式) 2 | 3 | 虽然 Qt Quick 提供的基础控件已经非常丰富,但是往往我们开发的项目中需要对不同的控件做定制,比如我们希望按钮的背景不是灰色而是黑色,控件的默认文本颜色不是黑色而是白色。那么就需要通过自定义控件样式来实现了。以 Button 为例: 4 | 5 | ```QML 6 | Button { 7 | id: control 8 | text: 'Click me' 9 | contentItem: Text { 10 | text: control.text 11 | font: control.font 12 | opacity: enabled ? 1.0 : 0.3 13 | color: control.down ? "#CCCCCC" : "#FFFFFF" // 按钮按下文字为 #cccccc,普通状态为 #ffffff 14 | horizontalAlignment: Text.AlignHCenter 15 | verticalAlignment: Text.AlignVCenter 16 | elide: Text.ElideRight 17 | } 18 | 19 | background: Rectangle { 20 | implicitWidth: 100 21 | implicitHeight: 40 22 | opacity: enabled ? 1 : 0.3 23 | color: control.down ? '#333333' : '#999999' // 按钮按下背景色为 #333333,普通状态为 #999999 24 | radius: 2 25 | } 26 | } 27 | ``` 28 | 29 | 通过 contentItem 和 background 属性,我们定制了按钮按下时文字颜色的不同和背景颜色的不同,效果如下: 30 | 31 | 32 | 33 | 这是 Qt Quick Controls 2 系列控件的通用定制方案,通过 Qt Quick Controls 对每一个控件描述的文档中,你可以很方便的找到自定义控件样式的链接: 34 | 35 | 36 | 37 | 访问 Customizing* 的链接,便可以找到对应的自定义样式链接,里面包含了一些简单的示例足以让你来个性化自己的控件样式: 38 | 39 | 40 | -------------------------------------------------------------------------------- /Controls/Handlers.md: -------------------------------------------------------------------------------- 1 | # Handlers 2 | 3 | QML 中有一些特殊的对象类型(严格意义来讲他们并不属于控件类型,当我们有更好的分类时可能移动这部分到其他位置。)他们是不可见的,用于提供一些点击、触控等事件,当我们需要处理一些鼠标或者触控事件时,可能会用到他们。 4 | 5 | ## MouseArea 6 | 7 | 前面的文章中我们不止一次看到过这个 QML 对象类型,它在 QML 中是一个不可见对象,提供鼠标的一些简单的事件处理,比如鼠标进入、鼠标离开、鼠标按下、鼠标弹起、鼠标移动等。它不属于 Handlers 的一种,但行为相似,我们暂时将 MouseArea 放到这个部分来介绍。以下是一个 MouseArea 的示例: 8 | 9 | ```QML 10 | import QtQuick 2.12 11 | import QtQuick.Window 2.12 12 | import QtQuick.Controls 2.12 13 | 14 | Window { 15 | visible: true 16 | width: 640 17 | height: 480 18 | title: qsTr('Hello world') 19 | 20 | Rectangle { 21 | width: 200 22 | height: 200 23 | color: 'red' // default color 24 | anchors.centerIn: parent 25 | 26 | MouseArea { 27 | anchors.fill: parent 28 | hoverEnabled: true 29 | onEntered: { 30 | parent.color = 'green' // 鼠标进入 31 | } 32 | onExited: { 33 | parent.color = 'red' // 鼠标离开 34 | } 35 | onPressed: { 36 | parent.color = 'gray' // 鼠标按下 37 | } 38 | onReleased: { // 鼠标弹起 39 | if (containsMouse) { // 判断鼠标是否在当前区域内 40 | parent.color = 'green' 41 | } else { 42 | parent.color = 'red' 43 | } 44 | } 45 | onClicked: { // 鼠标点击 pressed + released 46 | console.log('Mouse area clicked.') 47 | } 48 | } 49 | } 50 | } 51 | ``` 52 | 53 | 54 | 55 | 它经常用于对一些自定义的控件如图片控件、某些非常规的控件要响应鼠标事件时使用,但 MouseArea 有一点不好的是,他需要依附在一个项目中,上面的例子就是在 Rectangle 里面的,让 Rectangle 具有鼠标事件的处理能力,我们需要控制他铺满整个 Rectangle 才能响应对应的事件,而且一旦我们开启了 `hoverEnabled` 属性,在它之前创建的一些控件就无法响应鼠标事件了,有时我们仅需响应鼠标的进入、离开,但又不想影响该区域内其他控件的事件响应,该如何实现呢? 56 | 57 | ## HoverHandler 58 | 59 | Qt 5.14.1 版本引入了另外一个事件处理程序 `HoverHandler`,他可以完美的实现我们的需求。先来看一个例子 60 | 61 | ```QML 62 | import QtQuick 2.12 63 | import QtQuick.Window 2.12 64 | import QtQuick.Controls 2.12 65 | 66 | Window { 67 | visible: true 68 | width: 640 69 | height: 480 70 | title: qsTr('Hello world') 71 | 72 | Rectangle { 73 | width: 200 74 | height: 200 75 | color: 'red' // default color 76 | anchors.centerIn: parent 77 | 78 | Button { 79 | text: 'Click me' 80 | anchors.centerIn: parent 81 | onClicked: console.log('Button clicked') 82 | } 83 | 84 | HoverHandler { 85 | onHoveredChanged: { 86 | if (hovered) { 87 | parent.color = 'green' 88 | } else { 89 | parent.color = 'red' 90 | } 91 | } 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | 我们使用 HoverHandler 替换了 MouseArea,它不具有鼠标点击事件的处理能力,但是通过监听 hoveredChanged 信号同样可以捕获鼠标的悬停、离开等事件。并且它不会影响内部其他控件的事件流,如下图所示: 98 | 99 | 100 | 101 | HoverHandler 以父容器为基准,不需要我们单独在使用 anchors.fill: parent 来告诉它要填充哪个区域。而且它还可以在不同的设备如带有鼠标和触控设备上都能很好的工作。HoverHandler 非常实用,我们可以用它来实现一个菜单的隐藏显示、工具条的隐藏显示等功能而不会影响界面中的其他 UI 元素。 102 | 103 | 同样的除了 HoverHandler 以外,Qt 还引入了其他的类似事件处理对象如 `PointHandler`、`TapHandler`、`WheelHandler`。 104 | -------------------------------------------------------------------------------- /Controls/UI Forms.md: -------------------------------------------------------------------------------- 1 | # UI Forms 2 | 3 | 在使用 Qt Quick 制作界面的过程中,我们往往都是通过一个单独的文件来描述一个 UI 元素或者一个窗口样式。你会发现我们一些事件处理程序(逻辑)和视觉展示元素(UI)高度耦合在一起,某些情况下会造成单一的文件代码逻辑庞大,影响整体的代码可读性。为此 Qt Quick 提出了一种 UI 和逻辑分离的概念。下面我们做一个演示。 4 | 5 | ## 创建一个 UI Form 6 | 7 | 在项目的 .qrc 文件上点击右键,添加新文件: 8 | 9 | 10 | 11 | 在弹出的对话框中选择 Qt Quick UI File 12 | 13 | 14 | 15 | 在随后的对话框中输入我们要创建的 UI Form 的名称,这里命名为 Login 16 | 17 | 18 | 19 | 可以看到在,导航框中出现两个 name 输入框,一个是 Component name,一个是 Component form name。我们只需要输入 Comopnent name 后 Component form name 会自动生成。创建完成后会生成两个文件: 20 | 21 | 22 | 23 | 其中 LoginForm.ui.qml 是专门用来描述 UI 资源的文件,而 Login.qml 则是我们要在其他位置引入的组件,Qt Quick 会自动帮我们将两个文件进行关联。接下来我们在 main.qml 中引入我们的 Login 组件并让他填充整个父节点(Window)。 24 | 25 | ```QML 26 | import QtQuick 2.12 27 | import QtQuick.Window 2.12 28 | import QtQuick.Controls 2.12 29 | 30 | Window { 31 | visible: true 32 | width: 640 33 | height: 480 34 | title: qsTr('Hello world') 35 | 36 | Login { 37 | anchors.fill: parent 38 | } 39 | } 40 | ``` 41 | 42 | 打开 LoginForm.ui.qml 文件,我们修改一下里面的内容,将默认的宽高度去掉(由外部指定),然后像布局一节中介绍到的制作一个登录窗口: 43 | 44 | ```QML 45 | import QtQuick 2.4 46 | import QtQuick.Layouts 1.12 47 | import QtQuick.Controls 2.12 48 | 49 | Item { 50 | ColumnLayout { 51 | anchors.centerIn: parent 52 | TextField { 53 | id: inputUsername 54 | placeholderText: 'Username' 55 | Layout.fillWidth: true 56 | } 57 | TextField { 58 | id: inputPassword 59 | placeholderText: 'Password' 60 | Layout.fillWidth: true 61 | } 62 | RowLayout { 63 | Button { 64 | id: btnLogin 65 | text: 'Login' 66 | highlighted: true 67 | } 68 | Button { 69 | id: btnRegister 70 | text: 'Register' 71 | } 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | 修改完成后运行效果如下: 78 | 79 | 80 | 81 | 我们没有做任何控件的信号处理程序,界面仅仅展示部分内容,可以看到只要在 LoginForm.ui.qml 文件中将所需的视觉资源添加到这里面就可以了。但是这里要注意,我们不能直接在 LoginForm.ui.qml 文件中添加事件处理程序,否则 IDE 会有错误提示,如下所示: 82 | 83 | 84 | 85 | IDE 告诉我们,Qt Quick UI form 中不支持 JavaScript 函数,虽然这样提示,但是实际我们运行项目后点击 Login 按钮也是一样可以响应这些信号的,其实就是 IDE 层显示的告诉我们“不要这样做!” 86 | 87 | 那怎样的做法才是正确的呢?首先我们要把所有控件对象的 id 导出让外部可以访问,像下面这样。 88 | 89 | ```QML 90 | import QtQuick 2.4 91 | import QtQuick.Layouts 1.12 92 | import QtQuick.Controls 2.12 93 | 94 | Item { 95 | // 导出所有控件,告诉外部该控件可以通过 inputUsername 这个名称来访问 96 | property alias inputUsername: inputUsername 97 | property alias inputPassword: inputPassword 98 | property alias btnLogin: btnLogin 99 | property alias btnRegister: btnRegister 100 | 101 | ColumnLayout { 102 | anchors.centerIn: parent 103 | TextField { 104 | id: inputUsername 105 | placeholderText: 'Username' 106 | Layout.fillWidth: true 107 | } 108 | TextField { 109 | id: inputPassword 110 | placeholderText: 'Password' 111 | Layout.fillWidth: true 112 | } 113 | RowLayout { 114 | Button { 115 | id: btnLogin 116 | text: 'Login' 117 | highlighted: true 118 | } 119 | Button { 120 | id: btnRegister 121 | text: 'Register' 122 | } 123 | } 124 | } 125 | } 126 | ``` 127 | 128 | 随后打开 Login.qml 文件,在这里添加具体的信号处理程序。 129 | 130 | ```QML 131 | import QtQuick 2.4 132 | 133 | LoginForm { 134 | btnLogin.onClicked: { 135 | const username = inputUsername.text 136 | const password = inputPassword.text 137 | // Login to your app server 138 | // app.Login(username, password) 139 | console.log('Login button, username: ', username, ', Password: ', password) 140 | } 141 | btnRegister.onClicked: { 142 | const username = inputUsername.text 143 | const password = inputPassword.text 144 | // Register a account from your app server 145 | // app.Register(username, password, password) 146 | console.log('Register button, username: ', username, ', Password: ', password) 147 | } 148 | } 149 | ``` 150 | 151 | 我们分别添加了两个按钮的 clicked 信号处理程序,当我们运行程序后随便输入一些信息,分别点击 Login 按钮和 Register 按钮时,控制台则会打印如下信息。 152 | 153 | ``` 154 | qml: Login button, username: MyAccount , Password: MyPassword 155 | qml: Register button, username: MyAccount , Password: MyPassword 156 | ``` 157 | 158 | ## 总结 159 | 160 | 以上就是一个最简单的 UI form 示例程序了。它将我们实际的 UI 展示元素和逻辑强制分离,其设计的目的其实是考虑一些团队协作的方式中 UI 的设计和逻辑开发可能并不是一个团队来完成的,UI 的设计可能是视觉人员来做的,但是在中国的一些互联网公司中,往往交互视觉并不是直接通过这些开发工具来完成的,而是一个统一的交互视觉平台来做这些事情,开发人员再根据交互视频的稿子制作出对应的 UI 界面。很明显工具好用但是水土不服,实际的开发过程中我曾不断尝试使用 UI Form 的方式来设计应用程序,但是会发现它带来的成本是大量的 alias 别名,并不会应为引入这样的方式带来任何便利,所以如果你的团队协作方式还是相对传统的,我还是推荐使用普通的 Qt Quick 文件来完成 UI 元素的设计。这里也希望国内一些互联网公司能加快脚步,赶上潮流。 161 | 162 | -------------------------------------------------------------------------------- /Integrating/Context-Properties.md: -------------------------------------------------------------------------------- 1 | # Embedding C++ Objects into QML with Context Properties 2 | 3 | 在使用 Qt Quick 开发客户端程序时,我们以往固有的一些 C++ 类已经比较成熟,它可能具备一些调用第三方库的能力,或者去发送一个网络请求等。如何将他们注册到 QML 中使用就是我们比较关心的内容了。 4 | 5 | ## 注册 C++ 类到 QML 全局上下文 6 | 7 | 假设我们有一个已经创建好的类名为 AuthManager。其中包含了一些登录、注册等发送 HTTP 请求的相关能力。这些接口在发送一个异步 HTTP 请求后通过回调的方式告诉 C++ 这一层请求发送成功还是失败。代码如下: 8 | 9 | ```C++ 10 | #ifndef AUTHMANAGER_H 11 | #define AUTHMANAGER_H 12 | 13 | #include 14 | 15 | class AuthManager : public QObject 16 | { 17 | Q_OBJECT 18 | public: 19 | explicit AuthManager(QObject *parent = nullptr); 20 | void login(const QString& username, const QString& password) { 21 | // Send HTTP request 22 | // emit loginSignal(true or false) 23 | } 24 | void registerAccount(const QString& username, const QString& password) { 25 | // Send HTTP request 26 | // emit registerSignal(true or false) 27 | } 28 | 29 | signals: 30 | void loginSignal(bool success); 31 | void registerSignal(bool success); 32 | 33 | private: 34 | bool m_loggedIn = false; 35 | }; 36 | 37 | #endif // AUTHMANAGER_H 38 | ``` 39 | 40 | 这个类继承了 QObject,包含了两个方法,一个是 login、一个是 registerAccount,并且该类有一个成员变量 m_loggedIn 用来判断是否已经是登录状态,默认该值为 false。 41 | 42 | 要把这个类实例化的对象注册给前端使用,首先要先实例化一个对象,然后通过 QML 引擎的 setContextProperty 接口将其嵌入到 QML 的全局上下文中,main.cpp 的代码如下: 43 | 44 | ```C++ 45 | #include 46 | #include 47 | 48 | #include // 引入 QQmlContext 49 | #include "auth_manager.h" // 引入 AuthManager 50 | 51 | int main(int argc, char *argv[]) 52 | { 53 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 54 | 55 | QGuiApplication app(argc, argv); 56 | 57 | QQmlApplicationEngine engine; 58 | 59 | // 实例化 AuthManager 对象 60 | AuthManager authManager; 61 | // 注入到 QML 引擎的全局上下文中,命名为 authManager 62 | engine.rootContext()->setContextProperty("authManager", &authManager); 63 | 64 | const QUrl url(QStringLiteral("qrc:/main.qml")); 65 | QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, 66 | &app, [url](QObject *obj, const QUrl &objUrl) { 67 | if (!obj && url == objUrl) 68 | QCoreApplication::exit(-1); 69 | }, Qt::QueuedConnection); 70 | engine.load(url); 71 | 72 | return app.exec(); 73 | } 74 | ``` 75 | 76 | 注册成功后,我们就可以在 QML 中访问 authManager 了,在前面写好的 Login 界面中,我们响应 Login 按钮的点击事件,看看能否调用 authManager 中的一些方法: 77 | 78 | 79 | 80 | 可以看到 authManager 这个关键字已经高亮了,但是我们无法访问它任何方法或者属性。其实直接将一个纯 C++ 类注册给 QML 使用,是无法在前端使用它所有功能的,我们需要将要暴露给前端的方法声明为一个 QML 可调用的函数。 81 | 82 | ## Q_INVKEALBE 声明函数可被调用 83 | 84 | 在 login 和 registerAccount 方法前,我们使用 Q_INVOKABLE 宏来修饰这两个函数允许被前端调用,类似如下代码: 85 | 86 | ```C++ 87 | Q_INVOKABLE void login(const QString& username, const QString& password) { 88 | // Send HTTP request 89 | // emit loginSignal(true or false) 90 | } 91 | Q_INVOKABLE void registerAccount(const QString& username, const QString& password) { 92 | // Send HTTP request 93 | // emit registerSignal(true or false) 94 | } 95 | ``` 96 | 97 | 请注意方法前的 Q_INVOKABLE 宏是在函数返回值之前的。再次到前端尝试调用这两个方法: 98 | 99 | 100 | 101 | 可以看到,authManager 中多出了 login 和 registerAccount 方法来提供我们调用。尝试调用一下 login 方法并传递我们在界面中输入的用户名和密码,在 C++ 的 login 方法中,我们打印一下这两个参数: 102 | 103 | ```QML 104 | // in QML 105 | Button { 106 | Layout.fillWidth: true 107 | text: 'Login' 108 | onClicked: { 109 | authManager.login(inputUsername.text, inputPassword.text) 110 | } 111 | } 112 | ``` 113 | 114 | ```C++ 115 | Q_INVOKABLE void login(const QString& username, const QString& password) { 116 | // Send HTTP request 117 | // emit loginSignal(true or false) 118 | qInfo() << "Received login request, username: " << username << ", password: " << password; 119 | } 120 | ``` 121 | 122 | 运行程序后随便输入一些内容然后点击 Login 按钮,效果如下: 123 | 124 | 125 | 126 | 控制台打印出了我们 UI 上输入的用户名和密码,这样一个简单的 QML 调用 C++ 方法的流程就打通了。 127 | 128 | ## Q_PROPERTY 声明属性供 QML 访问 129 | 130 | 除了调用一些方法外,我们可能还需要访问 C++ 类中的一些成员,要访问这些成员可以通过 Q_PROPERTY 宏来表示哪些成员是可以被访问的,比如我们希望 QML 允许访问成员变量 m_loggedIn。 131 | 132 | 首先创建一个该变量变更的通知信号: 133 | 134 | ```C++ 135 | signals: 136 | // ... 其他信号 137 | void loggedInChanged(); 138 | ``` 139 | 140 | 然后创建对该变量的读写函数,在写函数中修改了 m_loggedIn 变量后我们发送一个 loggedInChanged 信号通知观察者这个变量有变更了。 141 | 142 | ```C++ 143 | bool loggedIn() const { 144 | return m_loggedIn; 145 | } 146 | void setLoggedIn(bool value) { 147 | m_loggedIn = value; 148 | emit loggedInChanged(); 149 | } 150 | ``` 151 | 152 | 最后我们通过 Q_PROPERTY 宏来告诉 QML,要访问这个变量你需要调用的各个函数。 153 | 154 | ```C++ 155 | Q_PROPERTY(bool loggedIn READ loggedIn WRITE setLoggedIn NOTIFY loggedInChanged) 156 | ``` 157 | 158 | Q_PROPERTY 一共有4个值 159 | 160 | - 第一个是告诉 QML 你要访问的这个属性的类型和名称 161 | - 第二个以 READ 开头表示要读取该属性你要调用的方法(我们已经创建好) 162 | - 第三个以 WRITE 开头表示要修改该属性你要调用的方法(同样已经创建好) 163 | - 第四个以 NOTIFY 开头表示这个属性变更时要关注的通知是哪一个(我们创建好的 loggedInChanged 信号) 164 | 165 | 请注意,Q_PROPERTY 并不是一个函数,而是一个宏,中间是不需要加逗号分割的。完整的 C++ 代码如下: 166 | 167 | ```C++ 168 | #ifndef AUTHMANAGER_H 169 | #define AUTHMANAGER_H 170 | 171 | #include 172 | #include 173 | 174 | class AuthManager : public QObject 175 | { 176 | Q_OBJECT 177 | public: 178 | explicit AuthManager(QObject *parent = nullptr); 179 | 180 | Q_PROPERTY(bool loggedIn READ loggedIn WRITE setLoggedIn NOTIFY loggedInChanged) 181 | 182 | Q_INVOKABLE void login(const QString& username, const QString& password) { 183 | // Send HTTP request 184 | // emit loginSignal(true or false) 185 | qInfo() << "Received login request, username: " << username << ", password: " << password; 186 | } 187 | Q_INVOKABLE void registerAccount(const QString& username, const QString& password) { 188 | // Send HTTP request 189 | // emit registerSignal(true or false) 190 | } 191 | 192 | bool loggedIn() const { 193 | return m_loggedIn; 194 | } 195 | void setLoggedIn(bool value) { 196 | m_loggedIn = value; 197 | emit loggedInChanged(); 198 | } 199 | 200 | signals: 201 | void loginSignal(bool success); 202 | void registerSignal(bool success); 203 | void loggedInChanged(); 204 | 205 | private: 206 | bool m_loggedIn = false; 207 | }; 208 | 209 | #endif // AUTHMANAGER_H 210 | ``` 211 | 212 | 接下来我们尝试在 QML 中添加一些代码,让 Login 按钮在 loggedIn 为 false 时才允许点击,为 true 的时候禁用该按钮 213 | 214 | ```QML 215 | Button { 216 | Layout.fillWidth: true 217 | text: 'Login' 218 | enabled: !authManager.loggedIn // 为 true 时禁用该按钮 219 | onClicked: { 220 | authManager.login(inputUsername.text, inputPassword.text) 221 | } 222 | } 223 | ``` 224 | 225 | 在 C++ 的 login 接口中我们调用 setLoggedIn 将 m_loggedIn 设置为 true,设置完成后会发出信号 loggedInChanged。看看我们通过前端调用 login 接口后成员 m_loggedIn 的变化会引起哪些变化。 226 | 227 | ```C++ 228 | Q_INVOKABLE void login(const QString& username, const QString& password) { 229 | // Send HTTP request 230 | // emit loginSignal(true or false) 231 | qInfo() << "Received login request, username: " << username << ", password: " << password; 232 | setLoggedIn(true); 233 | } 234 | ``` 235 | 236 | 效果如下: 237 | 238 | 239 | 240 | 当我们点击 Login 按钮后调用了 login 方法,login 方法将 m_loggedIn 设置为 true 并发出信号 loggedInChanged,告诉 QML 该值变化,QML 会根据该值的变化动态修改按钮的状态为禁用。 241 | 242 | ## 在 QML 中添加 C++ 的信号处理程序 243 | 244 | 除了调用 C++ 的方法和访问自定义属性外,我们还可以在 QML 中添加一个信号处理程序来处理 C++ 层发出的信号。在 C++ login 方法中,我们添加一行发送 loginSignal 信号的代码: 245 | 246 | ```C++ 247 | Q_INVOKABLE void login(const QString& username, const QString& password) { 248 | // Send HTTP request 249 | qInfo() << "Received login request, username: " << username << ", password: " << password; 250 | setLoggedIn(true); 251 | emit loginSignal(true); 252 | } 253 | ``` 254 | 255 | 随后在 QML 中使用 Connections 来连接 authManager 的 loginSignal 信号并向控制台输出一条内容打印传给信号的参数。 256 | 257 | ```QML 258 | Connections { 259 | target: authManager // 设置目标为 authManager 260 | onLoginSignal: { 261 | console.log('Login signal, success: ', success) 262 | } 263 | } 264 | ``` 265 | 266 | 运行程序后,点击 Login 按钮,就可以在控制台看到输出了如下内容: 267 | 268 | ``` 269 | Received login request, username: "abc" , password: "def" 270 | qml: Login signal, success: true 271 | ``` 272 | 273 | 首先是 C++ login 接口打印了我们输入的用户名密码,然后 login 方法发出了 loginSignal 信号,前端接收到这个信号后输入了信号传递的参数为 true。 274 | 275 | ## 总结 276 | 277 | 以上既是通过注入全局上下文的方式让 C++ 与 QML 建立通讯的方法,这种方式比较简单,一般我们会将某个业务功能的控制器注入到 QML 提供调用和访问属性。它在 QML 中全局都只有一份数据,而如果我们希望根据不同需求创建不同的 C++ 对象在前端使用,就需要注册自定义类型来实现了,这是我们接下来要介绍的内容。 278 | -------------------------------------------------------------------------------- /Integrating/Integrating-QML-and-C++.md: -------------------------------------------------------------------------------- 1 | # Integrating QML and C++ 2 | 3 | Qt Quick 提供了多种 C++ 与 QML 交互的方法,不同的方法是根据你项目实际开发过程中的需求而决定的,总体思想是通过 C++ 创建一个基于 QObject 的子类并把这个子类通过 Qt 提供的不同的方法注册到 QML 中使用。在 QML 中可以实例化一个自定义 C++ 类型或者直接通过变量名调用它们。下图为 Qt 官方文档给出的集成方式。 4 | 5 | 6 | 7 | 我将图片中的内容进行了翻译: 8 | 9 | 10 | 11 | 除了上面的方式,Qt 官方文档中还描述了另外一种将一个 C++ 类通过全局上下文的方式注册给 QML 使用,它借助接口 `setContextProperty` 来实现。一旦注册到 QML 的全局上下文,在前端任何 .qml 文件中都可以无需导入任何外部包的方式直接访问一个 C++ 对象。下面我们来详细介绍这些方式。 12 | -------------------------------------------------------------------------------- /Integrating/Register-QML-Type.md: -------------------------------------------------------------------------------- 1 | # Defining QML Types from C++ 2 | 3 | 除了向 QML 注入一个全局的上下文对象以外,Qt Quick 还提供了一种机制是提供一个 QML 可以创建的类型,任何 QObject 的子类都可以注册为 QML 对象类型。一旦注册了一个类,就可以像其他对象一样在 QML 中创建并使用它。 4 | 5 | ## 创建一个自定义 C++ 类型到 QML 中 6 | 7 | QML 中没有对象可以对剪切板做很好的支持,但是 Qt C++ 类中包含了剪切板相关的能力。我们创建一个复用了剪切板对象能力的 QObject 子类,然后将他注册给 QML 使用。 8 | 9 | ```C++ 10 | #ifndef CLIPBOARD_H 11 | #define CLIPBOARD_H 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | class Clipboard : public QObject 18 | { 19 | Q_OBJECT 20 | public: 21 | explicit Clipboard(QObject *parent = nullptr) 22 | : QObject(parent) { 23 | clipboard = QGuiApplication::clipboard(); 24 | } 25 | Q_INVOKABLE void setText(QString text) { 26 | clipboard->setText(text, QClipboard::Clipboard); 27 | } 28 | Q_INVOKABLE QString getText() { 29 | return clipboard->text(QClipboard::Clipboard); 30 | } 31 | private: 32 | QClipboard *clipboard; 33 | }; 34 | 35 | #endif // CLIPBOARD_H 36 | ``` 37 | 38 | 该类功能非常简单 ,使用 QClipboard 对象,创建两个方法 setText(设置剪切板)和 getText(获取剪切板内容)来提供外部使用。 39 | 40 | 在 main.cpp 中,我们将自定义的这个 QObject 子类注册给 QML 系统。 41 | 42 | ```C++ 43 | // 实例化 Clipboard 类对象并注册到 QML 中 44 | Clipboard clipboard; 45 | qmlRegisterType("Clipboard", 1, 0, "Clipboard"); 46 | ``` 47 | 48 | qmlRegisterType 模板方法包含四个参数 49 | 50 | - 第一个是在 QML 中用什么名字导入这个模块 51 | - 第二个是主版本号 52 | - 第三个是次版本号 53 | - 第四个是导入该模块后你应该以什么名称创建这个组件 54 | 55 | 一切就绪,在 QML 中,我们首先导入这个自定义对象: 56 | 57 | ```QML 58 | // 注意这里的名称和版本号 59 | import Clipboard 1.0 60 | ``` 61 | 62 | 随后在我们需要的地方创建一个 Clipboard 对象: 63 | 64 | ``` 65 | Clipboard { 66 | id: clipboard 67 | } 68 | ``` 69 | 70 | 在需要使用到剪切板的位置调用 `clipboard.setText()` 即可设置系统剪切板的内容,比如我们在前面示例中的登录窗口中,点下 Login 按钮时调用他的 setText 方法。 71 | 72 | ```QML 73 | Button { 74 | Layout.fillWidth: true 75 | text: 'Login' 76 | enabled: !authManager.loggedIn 77 | onClicked: { 78 | // 设置剪切板内容为 用户名 -- 密码 79 | clipboard.setText(inputUsername.text + '--' + inputPassword.text) 80 | } 81 | } 82 | ``` 83 | 84 | 运行程序后,在输入框随便输入一些内容,点击 Login 按钮,用户名和密码就会设置到剪切板中了。 85 | 86 | 87 | 88 | ## 总结 89 | 90 | 这是一个最简单不过的注册自定义 C++ 类型到 QML 的示例了。实际开发场景中,我们会有很多场景需要将自定义的对象注册给 QML 使用,除了这种方式外在我们前面介绍的 [QML 与 C++ 交互](Integrating-QML-and-C++.md) 一文中还可以注册一些单例对象、不可创建的纯数据形对象到 QML 中。基本上满足了我们所有的交互需求。 -------------------------------------------------------------------------------- /Integrating/Using-C++-Models.md: -------------------------------------------------------------------------------- 1 | # Using C++ Models with Qt Quick Views 2 | 3 | 在 QML 中使用视图容器时,前面的示例中我们是使用 JavaScript 来生成数据模型插入到 Model 中的。如果沿用这种方式,我们需要建立一种机制,将 C++ 中一些原始数据通过信号或者属性等方式传递给 QML,再使用 JavaScript 遍历的方式将数据插入到 Model 中。对于内容较大的数据,这绝对会严重影响程序性能。 4 | 5 | ## 使用自定义数据模型 6 | 7 | Qt 提供了几种 C++ 数据模型的抽象类来让我们从 C++ 这一层维护数据,配合与 QML 之间通信的机制传递一个实例让 QML 可以访问这些数据作为模型。下表是 Qt 提供的一些预定义好的数据模型抽象,你可以继承他们来实现自己的数据模型。 8 | 9 | | 类名 | 作用 | 10 | | -- | -- | 11 | | QAbstractListModel | 一个列表模型的抽象类 | 12 | | QAbstractProxyModel | 一个代理模型的抽象类,可以执行排序、过滤或其他数据处理任务 | 13 | | QAbstractTableModel | 一个表格模型的抽象类 | 14 | | QConcatenateTablesProxyModel | 表格模型代理抽象类,同样可以执行排序、过滤等功能 | 15 | | QDirModel | 本地文件系统目录数据模型 | 16 | | QFileSystemModel | 本地文件系统文件数据模型 | 17 | | QStandardItemModel | 一个储存自定数据的通用数据模型 | 18 | 19 | Qt 官方文档中使用 QAbstractListModel 为例制作了一个视频教程,教大家如何使用自定义数据模型,如果你尚未了解如何注册自定义类型请先阅读前面的文章,否则您可以无法理解视频中的全部内容。 20 | 21 | 文档地址:https://doc.qt.io/qt-5/qtquick-modelviewsdata-cppmodels.html 22 | 23 | [![Using C++ Models with Qt Quick Views](https://res.cloudinary.com/marcomontalbano/image/upload/v1601989723/video_to_markdown/images/youtube--9BcAYDlpuT8-c05b58ac6eb4c4700831b2b3070cd403.jpg)](https://www.youtube.com/watch?v=9BcAYDlpuT8&feature=emb_logo "Using C++ Models with Qt Quick Views") 24 | -------------------------------------------------------------------------------- /Others/DPI-Scaling.md: -------------------------------------------------------------------------------- 1 | # DPI Scaling 2 | 3 | 默认情况下你创建的 QML 应用程序都是开启了根据系统分辨率缩放的能力的,如下所示: 4 | 5 | ```QML 6 | #include 7 | #include 8 | 9 | int main(int argc, char *argv[]) 10 | { 11 | // 开启 DPI 自动适配能力 12 | QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); // <-- 13 | QGuiApplication app(argc, argv); 14 | QQmlApplicationEngine engine; 15 | const QUrl url(QStringLiteral("qrc:/main.qml")); 16 | engine.load(url); 17 | return app.exec(); 18 | } 19 | ``` 20 | 21 | 你必须在实例化 QGuiApplication 对象之前去设置该能力。很多场景下这一行代码就足够了,但实际 Qt 在对应用程序窗口及内部控件在缩放时,比例并不是严格按着系统的缩放比例来缩放的。我们来看一个示例: 22 | 23 | ```QML 24 | import QtQuick 2.12 25 | import QtQuick.Window 2.12 26 | import QtQuick.Controls 2.12 27 | 28 | Window { 29 | id: mainWindow 30 | visible: true 31 | width: 480 32 | height: 360 33 | title: qsTr('Hello world') 34 | color: '#EFEFEF' 35 | flags: Qt.Window | Qt.FramelessWindowHint 36 | 37 | Component.onCompleted: { 38 | console.log('Screen device pixel ratio:', Screen.devicePixelRatio) 39 | console.log('Window width:', mainWindow.width * Screen.devicePixelRatio) 40 | console.log('Window height:', mainWindow.height * Screen.devicePixelRatio) 41 | } 42 | 43 | Label { 44 | anchors.centerIn: parent 45 | font.pixelSize: 24 46 | font.family: 'Microsoft YaHei UI' 47 | text: qsTr('Windows size: %1 x %2') 48 | .arg(mainWindow.width * Screen.devicePixelRatio) 49 | .arg(mainWindow.height * Screen.devicePixelRatio) 50 | } 51 | } 52 | ``` 53 | 54 | 示例在窗口加载完毕后,打印了当前屏幕的 `Screen.devicePixelRatio` 属性,它是实际物理像素和设备无关像素之间的比例,就是我们俗话说的缩放比例。 55 | 56 | 在屏幕分辨率为 2560x1440 缩放 100% 的时候,我们运行程序,打印如下内容: 57 | 58 | ``` 59 | qml: Screen device pixel ratio: 1 60 | qml: Window width: 480 61 | qml: Window height: 360 62 | ``` 63 | 64 | 可以看到信息是正确的,随后我们再设置缩放 125% 的时候,我们运行程序,打印如下内容: 65 | 66 | ``` 67 | qml: Screen device pixel ratio: 1 68 | qml: Window width: 480 69 | qml: Window height: 360 70 | ``` 71 | 72 | 好像不太正确?是的,这时 devicePixelRatio 的值应该是 1.25,但这里依然打印了 1。我们再将缩放比例调整为 150% 看一下效果: 73 | 74 | ``` 75 | qml: Screen device pixel ratio: 2 76 | qml: Window width: 960 77 | qml: Window height: 720 78 | ``` 79 | 80 | 实际我们看到,devicePixelRatio 的值被四舍五入了。这时 Qt 内部默认的设定,这种四舍五入的计算方式,在某些非正规屏幕如 1920*1080 缩放 150% 的场景下,窗口实际会被放大 200%。原本 1080P 缩放 150% 以后就没有多少空间了,自身窗口再放大 200%,这将导致部分原始窗口较大的窗口严重超出屏幕范围。要解决这个问题可以在 main.cpp 中设置启用缩放后,设置一下 Qt 的缩放策略。如下所示: 81 | 82 | ```C++ 83 | // 设置程序根据分辨率自适应缩放 84 | QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 85 | // 设置缩放比例精确到小数点不需要四舍五入 86 | QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); 87 | ``` 88 | 89 | 我们通过 `setHighDpiScaleFactorRoundingPolicy` 将缩放策略设置为 `Qt::HighDpiScaleFactorRoundingPolicy::PassThrough`,该参数目的是告诉 Qt 程序,不对缩放比例四舍五入。 90 | 91 | 随后我们再测试一下屏幕分辨率为 2560x1440 缩放 150% 时候的效果: 92 | 93 | ``` 94 | qml: Screen device pixel ratio: 1.5 95 | qml: Window width: 720 96 | qml: Window height: 540 97 | ``` 98 | 99 | 这时我们看到的缩放比例和实际缩放后的分辨率就没有问题了。 100 | 101 | ## 总结 102 | 103 | 在实际开发中我们很少会去修改这些 Qt 默认的设置,但个别场景下应为 UI 适应的需求,我们要根据情况去做一下修改。在不同分辨率适配的场景,除了单独窗口的缩放适应问题,还有窗口、内嵌窗口等适配的问题,您可以根据自己的情况来分别处理不同窗口的缩放范围。 104 | -------------------------------------------------------------------------------- /Others/Global-Styles.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/Others/Global-Styles.md -------------------------------------------------------------------------------- /Others/Translation.md: -------------------------------------------------------------------------------- 1 | # Translates -------------------------------------------------------------------------------- /Positioning/Anchors.md: -------------------------------------------------------------------------------- 1 | # Anchors 锚点(Anchor-based Layout) 2 | 3 | ## 基本认知 4 | 5 | 像其他 UI 框架一样,QML 的组件允许我们通过固定的 x、y 坐标来控制位置的显示,但这样未免有些死板切不容易控制。QML 中提出了一个比较新颖的布局方式(这里叫布局可能不太合适,暂时这样表示,如有更好的表达方式将会修正)就是“Anchors”锚点。每一个 Item 都具有 7 个锚线(Anchor lines)包括上方 top、下方 bottom、左侧 left、右侧 right、垂直中心 verticalCenter、水平中心 horizontalCenter 和基线 baseline。下图演示了一个 Item 几个方向的基准线: 6 | 7 | 8 | 9 | ## 以指定节点为基准 10 | 11 | 默认情况下,一个 Item 放置到一个 Window 中是没有具体定位的,他们都将父级的左上角作为起点。如下所示: 12 | 13 | ``` 14 | import QtQuick 2.12 15 | import QtQuick.Window 2.12 16 | 17 | Window { 18 | visible: true 19 | width: 320 20 | height: 240 21 | title: qsTr('Hello World') 22 | 23 | Rectangle { 24 | width: 100 25 | height: 100 26 | color: 'red' 27 | } 28 | 29 | Rectangle { 30 | width: 100 31 | height: 100 32 | color: 'green' 33 | } 34 | } 35 | ``` 36 | 37 | 38 | 可以看到,两个 Rectangle 重叠了,因为我们并没有指定它们的位置属性。如果希望绿色的 Rectangle 在红色的 Rectangle 右侧,那么通过 anchors 属性来控制就非常简单了。 39 | 40 | ```QML 41 | import QtQuick 2.12 42 | import QtQuick.Window 2.12 43 | 44 | Window { 45 | visible: true 46 | width: 320 47 | height: 240 48 | title: qsTr('Hello World') 49 | 50 | Rectangle { 51 | id: rect1 52 | width: 100 53 | height: 100 54 | color: 'red' 55 | } 56 | 57 | Rectangle { 58 | id: rect2 59 | width: 100 60 | height: 100 61 | color: 'green' 62 | anchors.left: rect1.right 63 | } 64 | } 65 | ``` 66 | 67 | 我们通过设置 rect2 的 `anchors.left` 属性,指定了 rect2 的 left 以 rect1 的 right 为基准,这样 rect2 就会排列在 rect1 的右侧,如下图所示: 68 | 69 | 70 | 71 | 同理,如果你希望 rect2 在 rect1 下方,则修改 `anchors.left: rect1.right` 为 `anchors.top: rect1.bottom`,这里不再过多代码演示,可以自己写一些代码来进行演示。 72 | 73 | ## 居中 74 | 75 | 除了上方、下方、左侧、右侧,我们还可以指定他们根据某个 Item 的垂直中心和水平中心进行布局。去掉绿色的矩形仅保留一个红色的矩形,我们让红色的矩形在 Window 的中心显示,可以这样来实现: 76 | 77 | ```QML 78 | import QtQuick 2.12 79 | import QtQuick.Window 2.12 80 | 81 | Window { 82 | visible: true 83 | width: 320 84 | height: 240 85 | title: qsTr('Hello World') 86 | 87 | Rectangle { 88 | id: rect1 89 | width: 100 90 | height: 100 91 | color: 'red' 92 | anchors.verticalCenter: parent.verticalCenter 93 | anchors.horizontalCenter: parent.horizontalCenter 94 | } 95 | } 96 | ``` 97 | 98 | 这里的 parent 代表这个矩形的父节点,我们指定了矩形的水平中心和垂直中心都是基于父节点来做参考的,这样就实现了居中: 99 | 100 | 101 | 102 | 像这样常用的功能,QML 早就考虑到了,我们可以通过 `anchors.centerIn` 来控制居中显示在哪个父节点中。 103 | 104 | ```QML 105 | import QtQuick 2.12 106 | import QtQuick.Window 2.12 107 | 108 | Window { 109 | visible: true 110 | width: 320 111 | height: 240 112 | title: qsTr('Hello World') 113 | 114 | Rectangle { 115 | id: rect1 116 | width: 100 117 | height: 100 118 | color: 'red' 119 | anchors.centerIn: parent 120 | } 121 | } 122 | ``` 123 | 124 | ## 填充 125 | 126 | 上面代码效果是一样的。除了 centerIn,还有一个比较常用的 anchors.fill,表示要填充到哪个父节点中。比如我们希望红色的矩形填充整个窗口,则去掉宽高属性后,指定 anchors.fill 为 parent 即可: 127 | 128 | ```QML 129 | import QtQuick 2.12 130 | import QtQuick.Window 2.12 131 | 132 | Window { 133 | visible: true 134 | width: 320 135 | height: 240 136 | title: qsTr('Hello World') 137 | 138 | Rectangle { 139 | id: rect1 140 | width: 100 141 | height: 100 142 | color: 'red' 143 | anchors.fill: parent 144 | } 145 | } 146 | ``` 147 | 148 | 效果如下: 149 | 150 | 151 | 152 | ## 边距 153 | 154 | 有时我们希望填充后具有一定边距,那么可以通过 anchors.margins 来控制填充后的边距: 155 | 156 | ```QML 157 | import QtQuick 2.12 158 | import QtQuick.Window 2.12 159 | 160 | Window { 161 | visible: true 162 | width: 320 163 | height: 240 164 | title: qsTr('Hello World') 165 | 166 | Rectangle { 167 | id: rect1 168 | width: 100 169 | height: 100 170 | color: 'red' 171 | anchors.fill: parent 172 | anchors.margins: 10 173 | } 174 | } 175 | ``` 176 | 177 | 效果图: 178 | 179 | 180 | 181 | 如果你希望只控制一边的边距,可以指定 leftMargin、rightMargin、topMargin 或 bottomMargin 属性来达到目的。 182 | 183 | ## 其他 184 | 185 | 使用锚点布局必须是有父子关系的节点或是同级节点,不能跳级,也不能跨节点。比如下面的示例中,我们希望 rect3 居中显示在 rect2 中,但是由于他们并不是父子关系也不是同一级别的,导致设置失效: 186 | 187 | ```QML 188 | import QtQuick 2.12 189 | import QtQuick.Window 2.12 190 | 191 | Window { 192 | visible: true 193 | width: 320 194 | height: 240 195 | title: qsTr('Hello World') 196 | 197 | Rectangle { 198 | id: rect1 199 | width: 200 200 | height: 200 201 | color: 'red' 202 | Rectangle { 203 | id: rect3 204 | height: 50 205 | width: 50 206 | color: 'blue' 207 | anchors.centerIn: rect2 208 | } 209 | } 210 | 211 | Rectangle { 212 | id: rect2 213 | width: 200 214 | height: 200 215 | anchors.right: parent.right 216 | anchors.bottom: parent.bottom 217 | color: 'green' 218 | } 219 | } 220 | ``` 221 | 222 | 结果图: 223 | 224 | 225 | 226 | ## 总结 227 | 228 | 以上为 Anchors 锚点方式布局的一些常用示例和说明,更多可以参考 Qt 官方文档:https://doc.qt.io/qt-5/qtquick-positioning-anchors.html 229 | 230 | -------------------------------------------------------------------------------- /Positioning/Layouts.md: -------------------------------------------------------------------------------- 1 | # Layouts 布局 2 | 3 | 锚点布局虽然已经可以满足大部分场景,但是当你去编写一个复杂界面的时候,使用锚点布局你会发现将产生大量的 anchors... 代码,各 UI 显示资源高度关联耦合,一个变动可能会影响其他的项目变动。这不是我们希望看到的,除了锚点布局外,QML 也有像其他 UI 框架一样的 Layouts 方式来进行布局。常用的 Layouts 布局器有垂直布局 ColumnLayout、水平布局 RowLayout、网格布局 GridLayout。还有不太常用到的如 StackLayout。 4 | 5 | ## 水平布局 6 | 7 | 上文中第一个示例我们用锚点的方式让两个矩形左右布局的方式显示,同样,通过 RowLayout 我们可以达到同样的目的: 8 | 9 | ```QML 10 | import QtQuick 2.12 11 | import QtQuick.Window 2.12 12 | import QtQuick.Layouts 1.12 13 | 14 | Window { 15 | visible: true 16 | width: 320 17 | height: 240 18 | title: qsTr('Hello World') 19 | 20 | RowLayout { 21 | Rectangle { 22 | id: rect1 23 | width: 100 24 | height: 100 25 | color: 'red' 26 | } 27 | 28 | Rectangle { 29 | id: rect2 30 | width: 100 31 | height: 100 32 | color: 'green' 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | 效果图: 39 | 40 | 41 | 42 | 可以看到,我们删掉了 anchors 相关的代码,外部使用一个 RowLayout 将两个矩形包了起来,两个矩形以水平铺开的方式做展示,而且不难发现,使用 RowLayout 后会让子项目中间具有一个默认的间距。这也是布局属性中一个经常使用到的属性,如果你希望间距为 0 或者加大间距,则指定 RowLayout 的 spacing 属性即可,其值为像素值。 43 | 44 | > 注意:要使用 Layouts 布局相关能力,需要导入 `QtQuick.Layouts` 相关类型。 45 | 46 | ## 垂直布局 47 | 48 | 同样如果你希望两个矩形垂直排布,将 RowLayout 修改为 ColumnLayout 即可: 49 | 50 | ```QML 51 | import QtQuick 2.12 52 | import QtQuick.Window 2.12 53 | import QtQuick.Layouts 1.12 54 | 55 | Window { 56 | visible: true 57 | width: 320 58 | height: 240 59 | title: qsTr('Hello World') 60 | 61 | ColumnLayout { 62 | Rectangle { 63 | id: rect1 64 | width: 100 65 | height: 100 66 | color: 'red' 67 | } 68 | 69 | Rectangle { 70 | id: rect2 71 | width: 100 72 | height: 100 73 | color: 'green' 74 | } 75 | } 76 | } 77 | ``` 78 | 79 | 效果图: 80 | 81 | 82 | 83 | ## 网格布局 84 | 85 | 类似的 GridLayout 提供了网格方式布局的能力: 86 | 87 | ```QML 88 | import QtQuick 2.12 89 | import QtQuick.Window 2.12 90 | import QtQuick.Layouts 1.12 91 | 92 | Window { 93 | visible: true 94 | width: 320 95 | height: 240 96 | title: qsTr('Hello World') 97 | 98 | GridLayout { 99 | columns: 2 // 每行显示2列内容 100 | 101 | Rectangle { 102 | id: rect1 103 | width: 100 104 | height: 100 105 | color: 'red' 106 | } 107 | 108 | Rectangle { 109 | id: rect2 110 | width: 100 111 | height: 100 112 | color: 'green' 113 | } 114 | 115 | Rectangle { 116 | id: rect3 117 | width: 100 118 | height: 100 119 | color: 'gray' 120 | } 121 | 122 | Rectangle { 123 | id: rect4 124 | width: 100 125 | height: 100 126 | color: 'blue' 127 | } 128 | } 129 | } 130 | ``` 131 | 132 | 我们指定了 GridLayout 每行只显示 2 列内容,效果图: 133 | 134 | 135 | 136 | ## 嵌套 137 | 138 | 通过 Layouts,我们可以很方便的创建一个我们需要的布局体系,以一个非常常用的登陆对话框为例,我们做一个实操: 139 | 140 | ```QML 141 | import QtQuick 2.12 142 | import QtQuick.Window 2.12 143 | import QtQuick.Layouts 1.12 144 | import QtQuick.Controls 2.12 145 | 146 | Window { 147 | visible: true 148 | width: 640 149 | height: 480 150 | title: qsTr('Login') 151 | 152 | ColumnLayout { 153 | anchors.centerIn: parent 154 | TextField { 155 | placeholderText: 'Username' 156 | } 157 | TextField { 158 | placeholderText: 'Password' 159 | } 160 | RowLayout { 161 | Button { 162 | text: 'Login' 163 | } 164 | Button { 165 | text: 'Register' 166 | } 167 | } 168 | } 169 | } 170 | ``` 171 | 172 | 展示效果: 173 | 174 | 175 | 176 | 首先我们外部使用了一个 ColumnLayout 将所有关键内容包在一起,并设置该布局居中显示在窗口上。布局决定子项目是上下排布的,我们先放入了两个 TextField(输入控件)随后又增加了一个 RowLayout 嵌套在 ColumnLayout 中并在里面放置了两个 Button(按钮) 左右排布,最终达到了我们想要的效果。 177 | 178 | 大部分场景我们都需要嵌套使用 Layouts 才能满足界面布局上的需求。而如果使用锚点方式布局,会有很多的冗余代码。 179 | 180 | ## 子项目附加属性 181 | 182 | 上面的示例中,我们并没有控制控件的各类属性,比如宽、高、位置排布等,如果我们希望修改子项目的这些属性,需要使用 Layouts 给定的 `Layout.*` 属性,而不能直接使用控件原有的 width、height,更不能在 Layouts 子项目中使用锚点 `anchors` 关键字,否则我们会得到一些警告并且设置可能会失效,下面例子演示了错误的用法: 183 | 184 | ```QML 185 | import QtQuick 2.12 186 | import QtQuick.Window 2.12 187 | import QtQuick.Layouts 1.12 188 | import QtQuick.Controls 2.12 189 | 190 | Window { 191 | visible: true 192 | width: 640 193 | height: 480 194 | title: qsTr('Login') 195 | 196 | ColumnLayout { 197 | anchors.centerIn: parent 198 | TextField { 199 | width: 100 // 无效 200 | height: 30 // 无效 201 | anchors.horizontalCenter: parent.horizontalCenter // 无效且警告 202 | placeholderText: 'Username' 203 | } 204 | TextField { 205 | placeholderText: 'Password' 206 | } 207 | RowLayout { 208 | Button { 209 | text: 'Login' 210 | } 211 | Button { 212 | text: 'Register' 213 | } 214 | } 215 | } 216 | } 217 | ``` 218 | 219 | 我们给 ColumnLayout 的子项目指定了 width、height 和 anchors.horizontalCenter 属性,希望控制其宽高和居中属性。但是我们得到了如下警告并且界面并不是会随着我们代码修改而得到想象的改变: 220 | 221 | ``` 222 | qrc:/main.qml:14:9: QML TextField: Detected anchors on an item that is managed by a layout. This is undefined behavior; use Layout.alignment instead. 223 | ``` 224 | 225 | 警告中描述在一个使用了 Layouts 布局管理器管理的项目中错误的使用了 anchors 来进行布局,这是一个未定义行为,需要使用 Layout.alignment 来控制居中属性。虽然宽高并没有得到警告,但是我们看到也是并不生效的。正确的做法应该是这样: 226 | 227 | ```QML 228 | import QtQuick 2.12 229 | import QtQuick.Window 2.12 230 | import QtQuick.Layouts 1.12 231 | import QtQuick.Controls 2.12 232 | 233 | Window { 234 | visible: true 235 | width: 640 236 | height: 480 237 | title: qsTr('Login') 238 | 239 | ColumnLayout { 240 | anchors.centerIn: parent 241 | TextField { 242 | Layout.preferredWidth: 100 // width 243 | Layout.preferredHeight: 30 // height 244 | Layout.alignment: Qt.AlignHCenter // horizontalCenter 245 | placeholderText: 'Username' 246 | } 247 | TextField { 248 | placeholderText: 'Password' 249 | } 250 | RowLayout { 251 | Button { 252 | text: 'Login' 253 | } 254 | Button { 255 | text: 'Register' 256 | } 257 | } 258 | } 259 | } 260 | ``` 261 | 262 | 效果: 263 | 264 | 265 | 266 | Layout.* 附加属性除了推荐宽高、排列属性以外还有一些像边距、填充宽高、最大宽高等属性,更多可参考 Qt 官方文档:https://doc.qt.io/qt-5/qml-qtquick-layouts-layout.html 267 | 268 | ## 总结 269 | 270 | Layouts 与 Anchors 不同的是,你可以通过一个外部布局管理器来统一管理子项目的排列,而不需要对每个子项目指定锚点的方式来做排布,如果有个别子项需要单独属性,可通过 Layout.* 附加属性来控制子项目的展示规则,而且在一些自适应布局窗口中,Layouts 的表现更好。也大大降低了 UI 开发中布局的一些繁琐问题,这里也推荐大家使用 Layouts 方式布局来设计你的 UI 界面。 271 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qt Quick tutorial 2 | 3 | ## The Repository 4 | 5 | 此项目使用中文文本的方式描述了 Qt Quick 及其描述性语言 QML 的使用方法,内容丰富生动,通过这些学习,您可以快速从其他 UI 框架转入 Qt Quick 并制作任何自己想要的桌面应用。 6 | 7 | - 前言 8 | - 基本概念 9 | - [什么是 Qt Quick](Concepts/Qt%20Quick.md) (What's Qt Quick) 10 | - [什么是 QML](Concepts/QML.md) (What's QML) 11 | - [信号与事件处理](Concepts/Signal&Handler.md)(Signal and Handler Event System) 12 | - 布局定位 13 | - [锚点](Positioning/Anchors.md) (Anchors) 14 | - [布局](Positioning/Layouts.md) (Layouts) 15 | - 控件 16 | - [控件](Controls/Controls.md) (Controls) 17 | - [自定义控件样式](./Controls/Customizing%20Controls.md)(Styling Qt Quick Controls) 18 | - [Handlers](Controls/Handlers.md) 19 | - [表单](Controls/UI%20Forms.md) UI Forms 20 | - 容器(Containers) 21 | - [视图容器](Containers/Views.md)(Models and Views in Qt Quick) 22 | - [组件加载器](Containers/Loader.md)(Loader) 23 | - C++ 与 QML 通信 24 | - [全局上下文](Integrating/Context-Properties.md)(Context Properties) 25 | - [注册自定义 QML 类型](Integrating/Register-QML-Type.md)(Integrating QML and C++) 26 | - [注册自定义数据模型](Integrating/Using-C++-Models.md)(Using C++ Models with Qt Quick Views) 27 | - 多窗口管理 28 | - [动态创建窗口或组件](./Windows/Create-Component-Dynamically.md)(Dynamic QML Object Creation from JavaScript) 29 | - [内联窗口](Windows/Inside-Window.md) 30 | - [单例窗口](Windows/Singleton-Objects.md) 31 | - 其他 32 | - 全局样式(Global Styles)TODO 33 | - 翻译(Translation)TODO 34 | - [缩放](Others/DPI-Scaling.md)(DPI Scaling)TODO 35 | 36 | ## Feedback 37 | 38 | 为避免误导,如果您发现内容中有任何错误,欢迎提交 Issue 来帮助我完善它们。 39 | 40 | -------------------------------------------------------------------------------- /Windows/Create-Component-Dynamically.md: -------------------------------------------------------------------------------- 1 | # Dynamic QML Object Creation from JavaScript 2 | 3 | 除了静态的在 QML 文件中创建一些已知的对象外,我们还可以通过 Qt Quick 提供的动态创建组件功能来创建一个组件为可使用的对象。这在某些场景下非常实用,比如我们希望动态创建一个 Dialog 对话框或自定义的 MessageBox 对话框。它们的内容是每次弹出时跟随传入的参数动态变动的,而声明式创建可能无法达到我们的需求。 4 | 5 | 动态创建的对象有时是需要我们手动进行内存管理的,如果在创建时你明确生命了组件的依附节点,那么组件的生命周期可以托管给 QML 帮我们完成。 6 | 7 | ## 从文件动态创建对象 8 | 9 | 一些传统的 Windows、macOS 应用程序,系统都会提供一些全局的对话框用来显示一些内容,Qt Quick 也提供了类似的功能比如 Dialog,它基于 Popup 实现,内置了一些按钮、标题和内容的展示。但有时他的样式可能并不能满足我们的需求,所以我们可以基于 Popup 自定义一个模仿系统 MessageBox 的弹窗,显示标题、内容和确定取消按钮。 10 | 11 | 通过不同的事件响应来动态创建它们实现一些基础业务能力,首先来看一下我们自定义的 Popup 组件。 12 | 13 | ```QML 14 | import QtQuick 2.0 15 | import QtQuick.Layouts 1.12 16 | import QtQuick.Controls 2.12 17 | 18 | Popup { 19 | anchors.centerIn: parent 20 | modal: true 21 | 22 | property string title: '' 23 | property string content: '' 24 | 25 | signal confirm 26 | signal cancel 27 | 28 | onClosed: { 29 | console.log('Message box popup closed.') 30 | destroy() 31 | } 32 | 33 | Component.onDestruction: { 34 | console.log('Message box popup destroyed.') 35 | } 36 | 37 | ColumnLayout { 38 | anchors.fill: parent 39 | Label { 40 | Layout.preferredHeight: 40 41 | Layout.fillWidth: true 42 | text: title 43 | font.pixelSize: 18 44 | horizontalAlignment: Text.AlignHCenter 45 | verticalAlignment: Text.AlignVCenter 46 | } 47 | ToolSeparator { 48 | Layout.fillWidth: true 49 | orientation: Qt.Horizontal 50 | } 51 | Label { 52 | Layout.fillHeight: true 53 | Layout.fillWidth: true 54 | text: content 55 | font.pixelSize: 14 56 | wrapMode: Text.WrapAnywhere 57 | horizontalAlignment: Text.AlignHCenter 58 | verticalAlignment: Text.AlignVCenter 59 | } 60 | ToolSeparator { 61 | Layout.fillWidth: true 62 | orientation: Qt.Horizontal 63 | } 64 | RowLayout { 65 | Layout.fillWidth: true 66 | Layout.preferredHeight: 35 67 | Button { 68 | Layout.fillWidth: true 69 | text: qsTr('OK') 70 | onClicked: { 71 | confirm() 72 | close() 73 | } 74 | } 75 | Button { 76 | Layout.fillWidth: true 77 | text: qsTr('Cancel') 78 | onClicked: { 79 | cancel() 80 | close() 81 | } 82 | } 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | 它基于 Popup 制作,显示效果是这样的: 89 | 90 | 91 | 92 | 我们提供了连个属性一个是 title 用来显示标题,一个是 content 用来显示提示具体内容。 93 | 94 | 除了属性我们还提供了俩个信号,一个是 confirm 用以在点击了确定按钮后发出信号通知外部,另外一个是 cancel 则是点击取消按钮会发出的信号。分别在两个 Button 的 onClicked 信号处理程序中,发射了不同的信号并关闭 Popup 窗口。 95 | 96 | 另外我们添加了 Popup closed 信号的处理程序,在处理程序中我们检测如果已经关闭则自动销毁对象: 97 | 98 | ```QML 99 | onClosed: { 100 | console.log('Message box popup closed.') 101 | destroy() 102 | } 103 | ``` 104 | 105 | 另外在组件完全销毁后,我们打印了一条控制台消息用来确认是否已经被销毁: 106 | 107 | ```QML 108 | Component.onDestruction: { 109 | console.log('Message box popup destroyed.') 110 | } 111 | ``` 112 | 113 | 在 main.qml 中,我们可以这样来使用它们: 114 | 115 | ```QML 116 | import QtQuick 2.12 117 | import QtQuick.Window 2.12 118 | import QtQuick.Controls 2.12 119 | 120 | Window { 121 | visible: true 122 | width: 640 123 | height: 480 124 | title: qsTr("Hello World") 125 | 126 | property var component: undefined 127 | 128 | Component.onCompleted: { 129 | component = Qt.createComponent('qrc:/MessageBox.qml') 130 | } 131 | 132 | Column { 133 | anchors.centerIn: parent 134 | Button { 135 | text: 'Info' 136 | onClicked: { 137 | if (component.status === Component.Ready) { 138 | const infoObj = component.createObject(Window.window, { 139 | title: 'Info', 140 | content: 'This is a info message.' 141 | }) 142 | infoObj.confirm.connect(function () { 143 | console.log('Clicked confirm button from info message box.') 144 | }) 145 | infoObj.open() 146 | } 147 | } 148 | } 149 | Button { 150 | text: 'Warning' 151 | onClicked: { 152 | if (component.status === Component.Ready) { 153 | const warningObj = component.createObject(Window.window, { 154 | title: 'Warning', 155 | content: 'This is a warning message.' 156 | }) 157 | warningObj.confirm.connect(function () { 158 | console.log('Clicked confirm button from warning message box.') 159 | }) 160 | warningObj.open() 161 | } 162 | } 163 | } 164 | Button { 165 | text: 'Error' 166 | onClicked: { 167 | if (component.status === Component.Ready) { 168 | const errorObj = component.createObject(Window.window, { 169 | title: 'Error', 170 | content: 'This is a error message.' 171 | }) 172 | errorObj.confirm.connect(function () { 173 | console.log('Clicked confirm button from error message box.') 174 | }) 175 | errorObj.open() 176 | } 177 | } 178 | } 179 | } 180 | } 181 | ``` 182 | 183 | 首先我们在窗口加载完毕时,基于 MessageBox.qml 文件使用 `Qt.createComponent` 接口创建了一个组件并将它保存在全局的 component 属性中,此时他还是不可用的: 184 | 185 | ```QML 186 | Component.onCompleted: { 187 | component = Qt.createComponent('qrc:/MessageBox.qml') 188 | } 189 | ``` 190 | 191 | 界面中我们放置了三个按钮,分别显示不同标题和内容的 MessageBox,使用 `component.createObject` 方法可以将创建好的组件实例化为一个具体的对象。 192 | 193 | ```QML 194 | if (component.status === Component.Ready) { 195 | // 实例化对象 196 | const infoObj = component.createObject(Window.window, { 197 | title: 'Info', 198 | content: 'This is a info message.' 199 | }) 200 | infoObj.confirm.connect(function () { 201 | console.log('Clicked confirm button from info message box.') 202 | }) 203 | infoObj.open() 204 | } 205 | ``` 206 | 207 | 在对象实例化过程中,通过第一个参数我们指定了该对象的父节点是谁(我们指定了 Window.window 就是当前窗口),通过第二个参数将我们需要的不同 title 和 content 传递给该对象,使其构建时就可以获得这两个参数从而显示在界面上。 208 | 209 | 在对象创建完毕后,我们调用了对象中已有信号 confirm 的 connect 方法将其连接到一个我们写好的 JavaScript function 中,这样,在内部触发了 confirm 信号后,我们的方法会自动被调用。 210 | 211 | 另外两个按钮如法炮制,最终效果如下: 212 | 213 | 214 | 215 | 通过下方控制台打印的日志我们可以看出,当窗口弹出后我们点击了确定按钮,它的调用步骤如下: 216 | 217 | ``` 218 | // 首先调用了我们外部添加的 confirm 信号处理函数 219 | qml: Clicked confirm button from info message box. 220 | // 然后 Popup closed 信号处理程序被调动 221 | qml: Message box popup closed. 222 | // 最后 Popup destruction 被调用表示已经被销毁 223 | qml: Message box popup destroyed. 224 | ``` 225 | 226 | 当然,如果我们在打开这个 Popup 后并没有点击任何按钮而是直接关闭了窗口,同样 `qml: Message box popup destroyed.` 内容一样会被打印的,因为在创建对象时,我们指定了其父节点是主窗口,当主窗口关闭后,会附带销毁所有的子项目。这也是一种托管销毁的机制。 227 | 228 | ## 从字符串动态创建对象 229 | 230 | 除了从文件中创建一个组件并实例化对象以外,我们还可以通过纯字符串的方式创建一个组件,但是这种方式明显会造成代码难以阅读,我们并不推荐使用,这里仅仅从官网 Copy 一份示例提供大家参考: 231 | 232 | ```QML 233 | var newObject = Qt.createQmlObject('import QtQuick 2.0; Rectangle {color: "red"; width: 20; height: 20}', 234 | parentItem, 235 | "dynamicSnippet1"); 236 | ``` 237 | 238 | ## 删除动态创建的对象 239 | 240 | 如果一个组件对象频繁的被创建,那你需要考虑动态的去销毁他们,否则可能会造成一些内存泄露,直到应用程序生命周期结束这些内容才会被回收。上面的示例中我们在窗口关闭是调用了窗口自身的 `destroy()` 来销毁自己,从外部,你一样可以调用对象的 `destroy()` 接口来销毁对象: 241 | 242 | ``` 243 | if (component.status === Component.Ready) { 244 | const infoObj = component.createObject(Window.window, { 245 | title: 'Info', 246 | content: 'This is a info message.' 247 | }) 248 | infoObj.confirm.connect(function () { 249 | console.log('Clicked confirm button from info message box.') 250 | }) 251 | infoObj.open() 252 | infoObj.destroy() 253 | } 254 | ``` 255 | 256 | 以上代码没有实质性意义,在组件打开后既被销毁,可能无法展示,目的是为了演示如何在外部销毁对象。 257 | 258 | ## 总结 259 | 260 | 动态创建组件对象的方法在实际开发中会经常使用到,除了一些常规控件以外,我们也可以将一个独立窗口保存为单独的 .qml 文件然后通过以上方式动态创建他们,但是窗口的维护和管理远比控件要繁琐,除非特殊原因,否则不要使用动态创建窗口的方式来管理窗口。 261 | -------------------------------------------------------------------------------- /Windows/Inside-Window.md: -------------------------------------------------------------------------------- 1 | # Inside Window 2 | 3 | Qt Quick 创建窗口的方式有很多种,最简单的就是在一个 Window 中声明式创建另外一个窗口,并在需要的时候展示它们。 4 | 5 | ## 子窗口创建 6 | 7 | ```QML 8 | import QtQuick 2.12 9 | import QtQuick.Window 2.12 10 | import QtQuick.Controls 2.12 11 | 12 | Window { 13 | visible: true 14 | width: 640 15 | height: 480 16 | title: qsTr("Hello World") 17 | 18 | Window { 19 | id: childWindow 20 | width: 480 21 | height: 360 22 | title: 'Child Window' 23 | modality: Qt.WindowModal 24 | Label { 25 | anchors.centerIn: parent 26 | font.pixelSize: 20 27 | text: 'This is a child window' 28 | } 29 | } 30 | 31 | Button { 32 | text: 'Show' 33 | anchors.centerIn: parent 34 | onClicked: { 35 | childWindow.show() 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | 上面的示例中,我们在主窗口 Window 中又新建了一个 Window,主窗口显示时,内部的这个 Window 并不会立即展示,它默认是隐藏的。 42 | 43 | 当我们点击按钮 Show 时,子窗口随之显示。 44 | 45 | 46 | 47 | 我们通过 `modality` 属性设置了这个子窗口为模态窗台,像传统模态窗口一样,除非我们关闭了子窗口,否则主窗口是无法被操作的。 48 | -------------------------------------------------------------------------------- /Windows/Singleton-Objects.md: -------------------------------------------------------------------------------- 1 | # Registering Singleton Objects with a Singleton Type 2 | 3 | 通过前面的学习我们发现,Qt Quick 更像是一个单页面应用,像 Dialog、Popup 等这类控件都是在窗口内部弹出的,当我们需要制作一个超出窗口范围的显示区域时就需要单独使用一个窗口来表示了,这也符合桌面程序设计的原则。 4 | 5 | 有些窗口展示的样式是非常固定的,比如它可能是一个全局的居中 Toast 提示,我们没必要在每次显示它的时候都去动态创建它,这无疑会消耗一些不需要重复操作的资源。它只是内部内容可能会发生改变,我们可以通过一个单例窗口保证它只被创建一次,在它展示的时候我们仅仅修改其内部要显示的内容即可。让我们开始创建一个自己的全局 Toast 提示。 6 | 7 | ## 创建单例窗口 8 | 9 | 在创建这个单例窗口时,你还是需要先创建一个 Window 并将他独立在一个文件中,不同的是我们需要在文件的开头使用 `pragma Singleton` 关键字描述它是单例形式存在的,代码如下: 10 | 11 | ```QML 12 | pragma Singleton 13 | 14 | import QtQuick 2.12 15 | import QtQuick.Window 2.12 16 | import QtQuick.Controls 2.12 17 | import QtQuick.Layouts 1.12 18 | 19 | Window { 20 | id: root 21 | visible: true 22 | width: toastContainer.width + 60 23 | height: toastContainer.height 24 | x: (Screen.width - width) / 2 + Screen.virtualX 25 | y: (Screen.height - height) / 2 + Screen.virtualY 26 | title: qsTr("Global Toast") 27 | color: "transparent" 28 | flags: { 29 | if (Qt.platform.os === 'windows') 30 | Qt.Popup | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint 31 | else 32 | Qt.Popup | Qt.FramelessWindowHint 33 | } 34 | 35 | property string content: "" 36 | property real time: defaultTime 37 | readonly property real defaultTime: 3000 38 | readonly property real fadeTime: 300 39 | 40 | function displayText(text) { 41 | content = text 42 | root.show() 43 | anim.restart() 44 | } 45 | 46 | Rectangle { 47 | width: childrenRect.width 48 | height: childrenRect.height 49 | color: "#CC1E1E1E" 50 | radius: 4 51 | RowLayout { 52 | spacing: 0 53 | Item { Layout.preferredWidth: 30 } 54 | ColumnLayout { 55 | id: toastContainer 56 | spacing: 0 57 | Item { Layout.preferredHeight: 20 } 58 | Label { 59 | color: "#FFFFFF" 60 | text: content 61 | font.pixelSize: 18 62 | } 63 | Item { Layout.preferredHeight: 20 } 64 | } 65 | Item { Layout.preferredWidth: 30 } 66 | } 67 | } 68 | 69 | SequentialAnimation on opacity { 70 | id: anim 71 | running: false 72 | 73 | NumberAnimation { 74 | to: 1 75 | duration: fadeTime 76 | } 77 | PauseAnimation { 78 | duration: time - 2 * fadeTime 79 | } 80 | NumberAnimation { 81 | to: 0 82 | duration: fadeTime 83 | } 84 | onRunningChanged: { 85 | if (!running) 86 | root.hide(); 87 | else 88 | root.show(); 89 | } 90 | } 91 | } 92 | ``` 93 | 94 | 在这个窗口内部,真正用来显示内容的是一个 Label 控件,其他都是一些辅助功能,比如我们制作了一个动画效果,在窗口隐藏时使用了渐变透明度的效果。 95 | 96 | 这个窗口组件提供了一个 `displayText` 的函数,用来显示你要显示的文本内容,当该接口被调用,窗口根据屏幕坐标自动居中展示,并且开启一个定时器在几秒后自动隐藏窗口并带有动画效果。 97 | 98 | ## 将全局窗口注册为单例 99 | 100 | UI 部分资源完成后,我们需要使用 C++ 将这个单例窗口类型注册给 QML,这样其他文件才能使用,我们不能再像传统的直接声明式创建组件的方式来实例化这个对象了: 101 | 102 | ```C++ 103 | // main.cpp 104 | #include 105 | #include 106 | 107 | int main(int argc, char *argv[]) 108 | { 109 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 110 | QGuiApplication app(argc, argv); 111 | QQmlApplicationEngine engine; 112 | 113 | // 注册窗口为单例类型 114 | qmlRegisterSingletonType(QUrl("qrc:/GlobalToast.qml"), "GlobalToast", 1, 0, "GlobalToast"); 115 | 116 | const QUrl url(QStringLiteral("qrc:/main.qml")); 117 | engine.load(url); 118 | 119 | return app.exec(); 120 | } 121 | ``` 122 | 123 | 通过我们之前学习过的注册自定义类型的一种单例方式,我们将自己的 qml 组件使用 `qmlRegisterSingletonType` 接口注册为单例。 124 | 125 | 接下来在其他 QML 文件中我们需要使用时,首先要导入这个单例组件: 126 | 127 | ```QML 128 | // main.qml 129 | import GlobalToast 1.0 130 | ``` 131 | 132 | 当你要使用这个窗口时,像下面这样直接访问单例窗口的 displayText 接口就可以了。 133 | 134 | ```QML 135 | Button { 136 | text: 'Global Toast' 137 | anchors.centerIn: parent 138 | onClicked: { 139 | // 直接使用窗口实例,不需要单独创建 140 | GlobalToast.displayText('This is a global taost.') 141 | } 142 | } 143 | ``` 144 | 145 | 展示效果: 146 | 147 | 148 | 149 | ## 总结 150 | 151 | 通过单例窗口,我们可以重复使用一些窗口资源而不需要频繁的创建和销毁他们,这也带来了程序性能上的小小提升。不建议将一些不会频繁使用的窗口注册为单例窗口,你可以使用内联窗口或者动态创建的方式来使用他们。 152 | -------------------------------------------------------------------------------- /images/2020-09-26_16-46-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-09-26_16-46-13.png -------------------------------------------------------------------------------- /images/2020-09-26_17-25-53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-09-26_17-25-53.png -------------------------------------------------------------------------------- /images/2020-09-26_17-30-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-09-26_17-30-10.png -------------------------------------------------------------------------------- /images/2020-09-26_17-34-00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-09-26_17-34-00.png -------------------------------------------------------------------------------- /images/2020-09-26_17-35-09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-09-26_17-35-09.png -------------------------------------------------------------------------------- /images/2020-09-26_23-59-51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-09-26_23-59-51.png -------------------------------------------------------------------------------- /images/2020-09-26_23-59-59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-09-26_23-59-59.png -------------------------------------------------------------------------------- /images/2020-09-27_00-05-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-09-27_00-05-29.png -------------------------------------------------------------------------------- /images/2020-10-04_21-15-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-04_21-15-44.png -------------------------------------------------------------------------------- /images/2020-10-04_21-29-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-04_21-29-17.png -------------------------------------------------------------------------------- /images/2020-10-04_22-11-31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-04_22-11-31.png -------------------------------------------------------------------------------- /images/2020-10-04_22-23-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-04_22-23-20.png -------------------------------------------------------------------------------- /images/2020-10-04_22-30-09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-04_22-30-09.png -------------------------------------------------------------------------------- /images/2020-10-04_22-33-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-04_22-33-03.png -------------------------------------------------------------------------------- /images/2020-10-04_22-38-34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-04_22-38-34.png -------------------------------------------------------------------------------- /images/2020-10-04_22-44-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-04_22-44-15.png -------------------------------------------------------------------------------- /images/2020-10-04_22-56-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-04_22-56-15.png -------------------------------------------------------------------------------- /images/2020-10-04_23-00-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-04_23-00-17.png -------------------------------------------------------------------------------- /images/2020-10-04_23-03-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-04_23-03-18.png -------------------------------------------------------------------------------- /images/2020-10-04_23-08-59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-04_23-08-59.png -------------------------------------------------------------------------------- /images/2020-10-04_23-27-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-04_23-27-57.png -------------------------------------------------------------------------------- /images/2020-10-05_14-34-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-05_14-34-07.png -------------------------------------------------------------------------------- /images/2020-10-05_14-34-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-05_14-34-24.png -------------------------------------------------------------------------------- /images/2020-10-05_14-34-38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-05_14-34-38.png -------------------------------------------------------------------------------- /images/2020-10-05_14-34-56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-05_14-34-56.png -------------------------------------------------------------------------------- /images/2020-10-05_14-36-27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-05_14-36-27.png -------------------------------------------------------------------------------- /images/2020-10-05_15-17-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-05_15-17-22.png -------------------------------------------------------------------------------- /images/2020-10-05_15-18-33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-05_15-18-33.png -------------------------------------------------------------------------------- /images/2020-10-05_20-01-56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-05_20-01-56.png -------------------------------------------------------------------------------- /images/2020-10-05_20-02-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-05_20-02-02.png -------------------------------------------------------------------------------- /images/2020-10-05_20-04-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-05_20-04-12.png -------------------------------------------------------------------------------- /images/2020-10-05_20-05-39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-05_20-05-39.png -------------------------------------------------------------------------------- /images/2020-10-05_20-16-55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-05_20-16-55.png -------------------------------------------------------------------------------- /images/2020-10-05_20-19-09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-05_20-19-09.png -------------------------------------------------------------------------------- /images/2020-10-05_23-58-53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-05_23-58-53.png -------------------------------------------------------------------------------- /images/2020-10-06_14-58-46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-06_14-58-46.png -------------------------------------------------------------------------------- /images/2020-10-06_15-06-45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-06_15-06-45.png -------------------------------------------------------------------------------- /images/2020-10-06_15-10-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-06_15-10-36.png -------------------------------------------------------------------------------- /images/2020-10-06_15-28-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-06_15-28-02.png -------------------------------------------------------------------------------- /images/2020-10-07_11-32-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-07_11-32-19.png -------------------------------------------------------------------------------- /images/2020-10-07_11-32-45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/2020-10-07_11-32-45.png -------------------------------------------------------------------------------- /images/Containers/modelview-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/Containers/modelview-overview.png -------------------------------------------------------------------------------- /images/Controls/customizing-button.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/Controls/customizing-button.gif -------------------------------------------------------------------------------- /images/Controls/hoverhandler.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/Controls/hoverhandler.gif -------------------------------------------------------------------------------- /images/Controls/listview-delegate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/Controls/listview-delegate.gif -------------------------------------------------------------------------------- /images/Controls/loader-different-page.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/Controls/loader-different-page.gif -------------------------------------------------------------------------------- /images/Controls/mousearea.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/Controls/mousearea.gif -------------------------------------------------------------------------------- /images/Controls/page-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/Controls/page-loader.gif -------------------------------------------------------------------------------- /images/Controls/qtquickcontrols2-busyindicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/Controls/qtquickcontrols2-busyindicator.png -------------------------------------------------------------------------------- /images/Integrating/clipboard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/Integrating/clipboard.gif -------------------------------------------------------------------------------- /images/Integrating/cpp-qml-integration-flowchart-cn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/Integrating/cpp-qml-integration-flowchart-cn.jpg -------------------------------------------------------------------------------- /images/Integrating/cpp-qml-integration-flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/Integrating/cpp-qml-integration-flowchart.png -------------------------------------------------------------------------------- /images/Integrating/loggedIn-property.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/Integrating/loggedIn-property.gif -------------------------------------------------------------------------------- /images/Windows/child-window.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/Windows/child-window.gif -------------------------------------------------------------------------------- /images/Windows/dynamic-create.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/Windows/dynamic-create.gif -------------------------------------------------------------------------------- /images/Windows/galbal-toast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmgwddj/qt-quick-tutorial/1d10072808d4b0006966680a8738413bdc526431/images/Windows/galbal-toast.gif --------------------------------------------------------------------------------