1284 lines
34 KiB
Markdown
1284 lines
34 KiB
Markdown
# QML 应用程序开发技术总结
|
||
|
||
- [QML 应用程序开发技术总结](#qml-应用程序开发技术总结)
|
||
- [1. 基础部分](#1-基础部分)
|
||
- [1.1. 方法/属性名称的大小写](#11-方法属性名称的大小写)
|
||
- [1.2. 显示顺序](#12-显示顺序)
|
||
- [2. 全局属性](#2-全局属性)
|
||
- [3. 信号与槽](#3-信号与槽)
|
||
- [3.1. 信号与信号处理器](#31-信号与信号处理器)
|
||
- [3.2. 属性变化信号与属性变化信号处理器](#32-属性变化信号与属性变化信号处理器)
|
||
- [3.3. 附加属性与附加信号处理器](#33-附加属性与附加信号处理器)
|
||
- [3.4. Connections 建立信号与槽的连接](#34-connections-建立信号与槽的连接)
|
||
- [3.5. connect()方法](#35-connect方法)
|
||
- [3.6. 自定义信号](#36-自定义信号)
|
||
- [4. 界面加载完成信号](#4-界面加载完成信号)
|
||
- [5. 包别名](#5-包别名)
|
||
- [6. 添加图标](#6-添加图标)
|
||
- [6.1. 制作图标](#61-制作图标)
|
||
- [6.2. 添加图标到应用](#62-添加图标到应用)
|
||
- [7. 绘制圆形](#7-绘制圆形)
|
||
- [8. 常用组件](#8-常用组件)
|
||
- [8.1. QtQuick.Loader](#81-qtquickloader)
|
||
- [8.2. QtQuick.Controls.Button](#82-qtquickcontrolsbutton)
|
||
- [8.3. Dialog 对象](#83-dialog-对象)
|
||
- [8.3.1. QtQuick.Controls 中的 Dialog](#831-qtquickcontrols-中的-dialog)
|
||
- [8.3.2. QtQuick.Dialogs](#832-qtquickdialogs)
|
||
- [8.3.2.1. FileDialog](#8321-filedialog)
|
||
- [8.3.2.2. MessageDialog](#8322-messagedialog)
|
||
- [8.4. ComboBox](#84-combobox)
|
||
- [8.5. Grid](#85-grid)
|
||
- [8.6. ScrollView](#86-scrollview)
|
||
- [8.7. QtQuick.ListView](#87-qtquicklistview)
|
||
- [8.7. GridView](#87-gridview)
|
||
- [8.8. BusyIndicator](#88-busyindicator)
|
||
- [8.9. VirtualKeyboard](#89-virtualkeyboard)
|
||
- [9. 多文档开发](#9-多文档开发)
|
||
- [9.1. 多 QML 文件的管理](#91-多-qml-文件的管理)
|
||
- [9.2. 如何引用自定义 QML 文件](#92-如何引用自定义-qml-文件)
|
||
- [9.3. 使用另一 QML 文件中的元件或属性](#93-使用另一-qml-文件中的元件或属性)
|
||
- [9.4. 示例](#94-示例)
|
||
- [10. QML 与 C++ 交互](#10-qml-与-c-交互)
|
||
- [10.1. QML 访问 C++ 中声明的类型](#101-qml-访问-c-中声明的类型)
|
||
- [10.2. C++ 访问 QML 对象](#102-c-访问-qml-对象)
|
||
- [10.3. 通过信号槽传递自建类型](#103-通过信号槽传递自建类型)
|
||
- [10.4. QML 与 C++ 交互综合示例](#104-qml-与-c-交互综合示例)
|
||
- [11. Windows 下 QML 程序的打包发布](#11-windows-下-qml-程序的打包发布)
|
||
- [12. 外部参考资料](#12-外部参考资料)
|
||
|
||
## 1. 基础部分
|
||
|
||
### 1.1. 方法/属性名称的大小写
|
||
|
||
QML 语法与 JS 相同,具体请参考相关教程,此处只对一些需要特殊注意的地方进行说明。
|
||
|
||
QML 的属性、变量、函数等一般为小写字母开头,大写字母开头通常具有特殊意义,比如使用 QML 连接名为 demo() 的信号时,系统将自动连接 onDemo() 作为其槽函数。以下以连接 Button 的 clicked 信号为例进行演示:
|
||
|
||
```js
|
||
import QtQuick 2.12
|
||
import QtQuick.Window 2.12
|
||
import QtQuick.Controls 2.12
|
||
|
||
Window {
|
||
width: 640
|
||
height: 480
|
||
visible: true
|
||
|
||
Button {
|
||
onClicked: {
|
||
// Do something...
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 1.2. 显示顺序
|
||
|
||
QML 可以包含多个可显示对象,默认情况下 QML 文件中先写的对象显示在下层,后写的对象显示在上层。在显示时,上层的对象覆盖下层对象。
|
||
|
||
## 2. 全局属性
|
||
|
||
使用 property 关键字可以定义全局属性,该语法格式如下:
|
||
|
||
```js
|
||
[default] [required] [readonly] property <propertyType> <propertyName>
|
||
```
|
||
|
||
属性名称必须以小写字母开头,并且只能包含字母、数字和下划线。
|
||
|
||
```js
|
||
import QtQuick 2.12
|
||
import QtQuick.Window 2.12
|
||
import QtQuick.Controls 2.12
|
||
|
||
Window {
|
||
width: 640
|
||
height: 480
|
||
visible: true
|
||
|
||
property int demoInt: 1
|
||
property string demoStr: "Hello"
|
||
}
|
||
```
|
||
|
||
## 3. 信号与槽
|
||
|
||
在 QML 中连接信号槽主要有以下 5 种方法。
|
||
|
||
### 3.1. 信号与信号处理器
|
||
|
||
```js
|
||
signal()
|
||
|
||
on<Signal>: {}
|
||
```
|
||
|
||
这是最常见的方法,比如 onClicked: {} 槽。
|
||
|
||
### 3.2. 属性变化信号与属性变化信号处理器
|
||
|
||
```js
|
||
property
|
||
|
||
on<Property>Changed: {}
|
||
```
|
||
|
||
QML 规范,当属性变化后会发送一个 \<Property\>Changed 信号。从 C 环境创建属性时需要注册 NOTIFY 信号,其命名格式也需要遵循该规范。
|
||
|
||
### 3.3. 附加属性与附加信号处理器
|
||
|
||
```js
|
||
Attached Signal(附加属性)
|
||
|
||
XX元素.on<附加属性>: {}
|
||
```
|
||
|
||
最常见的是 Component.onCompleted:{} 槽。
|
||
|
||
### 3.4. Connections 建立信号与槽的连接
|
||
|
||
```js
|
||
Connections {
|
||
target: 发送者
|
||
发送者信号处理器:{}
|
||
}
|
||
```
|
||
|
||
前面的信号连接都嵌入在某个对象中,当信号与槽的连接不依赖具体对象时,需要将其嵌入到 Connections 对象中。Connections 的 target 指向信号的发送者,一个 Connections 可以绑定同一个 target 的多个信号处理机。
|
||
|
||
```js
|
||
Connections {
|
||
target: whomSentTheSignal
|
||
|
||
function onSignal0(arg) {
|
||
console.log(arg)
|
||
}
|
||
|
||
function onSignal1(arg) {
|
||
console.log(arg)
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 3.5. connect()方法
|
||
|
||
使用 connect() 方法,可以为信号指定非 on\<Signal\> 格式命名的响应方法,也可以指令信号触发下一个信号形成信号的连锁反应。
|
||
|
||
```js
|
||
[元素对象.]信号.connect(信号/方法)
|
||
```
|
||
|
||
示例如下:
|
||
|
||
```js
|
||
Rectangle {
|
||
id: root
|
||
|
||
signal mySignal()
|
||
// 信号响应处理
|
||
onMySignal: console.log("clicked connect mySignal")
|
||
|
||
// 普通方法
|
||
function slt_clicked() {
|
||
console.log("clicked connect slt_clicked");
|
||
}
|
||
|
||
Component.onCompleted: {
|
||
mousearea.clicked.connect(slt_clicked);
|
||
mousearea.clicked.connect(mySignal);
|
||
}
|
||
|
||
MouseArea {
|
||
id: mousearea
|
||
anchors.fill: parent
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.6. 自定义信号
|
||
|
||
用以下形式声明自定义的信号:
|
||
|
||
```js
|
||
signal <name>[([<type> <parameter name>[, ...]])]
|
||
```
|
||
|
||
示例如下:
|
||
|
||
```js
|
||
Rectangle {
|
||
id: root
|
||
signal mysignal(int x, int y)
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
onPressed: root.mysignal(mouse.x, mouse.y)
|
||
}
|
||
}
|
||
```
|
||
|
||
## 4. 界面加载完成信号
|
||
|
||
许多时候需要在 QML 程序界面完成加载后做一些事情,比如设置一些属性的初始值。这时候就需要用到 Component 的 completed() 信号:
|
||
|
||
```js
|
||
import QtQuick 2.12
|
||
import QtQuick.Window 2.12
|
||
import QtQuick.Controls 2.12
|
||
|
||
Window {
|
||
width: 640
|
||
height: 480
|
||
visible: true
|
||
|
||
TextField {
|
||
id: demoText
|
||
text: qsTr("Demo")
|
||
font.pixelSize: 12
|
||
}
|
||
|
||
Component.onCompleted: {
|
||
demoText.text = qsTr("DemoText")
|
||
// 加载完成后 demoText 默认获得焦点.
|
||
demoText.forceActiveFocus()
|
||
}
|
||
}
|
||
```
|
||
|
||
## 5. 包别名
|
||
|
||
编写 QML 应用时需要像 include 那样引用一些软件包,有的时候软件包中的对象会有重名的情况,比如 QtQuick.Dialogs 1.3 和 Qt.labs.qmlmodels 1.0 中均包含了 FileDialog 对象,但他们的功能并不相同。此时可以使用 import as 来实现类似别名或者命名空间的功能。
|
||
|
||
```js
|
||
import QtQuick 2.12
|
||
import QtQuick.Window 2.12
|
||
import QtQuick.Dialogs 1.3
|
||
import Qt.labs.platform 1.1 as QLP
|
||
|
||
Window {
|
||
width: 640
|
||
height: 480
|
||
visible: true
|
||
|
||
FileDialog {
|
||
title: qsTr("保存到…")
|
||
folder: QLP.StandardPaths.writableLocation(QLP.StandardPaths.DocumentsLocation)
|
||
}
|
||
}
|
||
```
|
||
|
||
## 6. 添加图标
|
||
|
||
### 6.1. 制作图标
|
||
|
||
先安装 ImageMagic 工具,并将其添加到系统 PATH 下方便使用。
|
||
|
||
使用以下命令将多个不同尺寸的图片打包成一个 ICO 文件。
|
||
|
||
```bash
|
||
magick.exe convert icon-16.png icon-32.png icon-256.png myappico.ico
|
||
```
|
||
|
||
### 6.2. 添加图标到应用
|
||
|
||
在 Qt .pro 文件中添加以下内容,以便将图标编译到 Qt 程序中:
|
||
|
||
```js
|
||
RC_ICONS = myappico.ico
|
||
```
|
||
|
||
However, if you already have an .rc file, for example, with the name myapp.rc, which you want to reuse, the following two steps will be required. First, put a single line of text to the myapp.rc file:
|
||
|
||
```js
|
||
IDI_ICON1 ICON "myappico.ico"
|
||
```
|
||
|
||
Then, add this line to your myapp.pro file:
|
||
|
||
```js
|
||
RC_FILE = myapp.rc
|
||
```
|
||
|
||
If you do not use qmake, the necessary steps are: first, create an .rc file and run the rc or windres program on the .rc file, then link your application with the resulting .res file.
|
||
|
||
更详细内容见《[Setting the Application Icon](https://doc.qt.io/qt-5/appicon.html)》。
|
||
|
||
## 7. 绘制圆形
|
||
|
||
设置 Rectangle 的 radius 为边长的一半即可。
|
||
|
||
## 8. 常用组件
|
||
|
||
### 8.1. QtQuick.Loader
|
||
|
||
TODO:
|
||
TODO: 参考 XiaYu 项目中的 SubMenuGrp 和 SubMenuBtn 对 default 属性和子组件引用父组件方法、属性进行说明。
|
||
|
||
### 8.2. QtQuick.Controls.Button
|
||
|
||
```js
|
||
Button {
|
||
id: butDemo
|
||
width: 200
|
||
height: 100
|
||
text: qsTr("Demo")
|
||
// 获得焦点时高亮.
|
||
highlighted: activeFocus
|
||
onClicked: butDemo.text = qsTr("DemoButton")
|
||
// 当 Button 获得焦点时可响应回车按钮.
|
||
Keys.onReturnPressed: butDemo.text = qsTr("Button")
|
||
}
|
||
```
|
||
|
||
### 8.3. Dialog 对象
|
||
|
||
QML 中有三大类 Dialog 对象,这里主要介绍 QtQuick.Controls 中的 Dialog 和 QtQuick.Dialogs。
|
||
|
||
#### 8.3.1. QtQuick.Controls 中的 Dialog
|
||
|
||
QtQuick.Controls 中的 Dialog 比较原始,属性需要自行定义和实现,所以它的自由度也比较高。该 Dialog 包含了页眉(Header)、页脚(Footer)和内容(Content)三部分,每个部分都可以单独设定。
|
||
|
||
- footer:Item
|
||
|
||
对话框页脚项。页脚项目位于底部,并调整为对话框的宽度。 默认值为空。
|
||
|
||
注意:将 DialogButtonBox 指定为对话框页脚会自动将其 accepted() 和 rejected() 信号连接到 Dialog 中的相应信号。
|
||
|
||
注意:将 DialogButtonBox、ToolBar 或 TabBar 指定为对话框页脚会自动将相应的 DialogButtonBox::position、ToolBar::position 或 TabBar::position 属性设置为 Footer。
|
||
|
||
- header:Item
|
||
|
||
对话框标题项。标题项位于顶部,并调整为对话框的宽度。默认值为空。
|
||
|
||
注意:将 DialogButtonBox 指定为对话框标题会自动将其 accepted() 和 rejected() 信号连接到 Dialog 中的相应信号。
|
||
|
||
注意:将 DialogButtonBox、ToolBar 或 TabBar 指定为对话框标题会自动将相应的 DialogButtonBox::position、ToolBar::position 或 TabBar::position 属性设置为 Header。
|
||
|
||
该 Dialog 不会阻塞父窗口,并且在点击 Dialog 之外的区域会自动关闭,如果需要阻塞父窗口并且不自动关闭,则需要设定如下属性:
|
||
|
||
```js
|
||
Dialog {
|
||
modal : true
|
||
closePolicy: Popup.NoAutoClose
|
||
}
|
||
```
|
||
|
||
上面的对话框没有提供任何按钮,可以通过 standardButtons 设定标准按钮:
|
||
|
||
```js
|
||
Dialog {
|
||
title: qsTr("Demo")
|
||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||
modal : true
|
||
closePolicy: Popup.NoAutoClose
|
||
}
|
||
```
|
||
|
||
除 Ok Button 和 Cancel Button 外,还有许多系统预定义的 Standard Button 可自行翻阅 QML 帮助获得相关帮助信息。
|
||
|
||
#### 8.3.2. QtQuick.Dialogs
|
||
|
||
在使用 QtQuick.Dialogs 的 Dialog 对象时,如果使用 QGuiApplication 来执行则会导致无法加载主题风格,并且对话框无法正确显示图标。如果最初使用 QGuiApplication 创建了 app,则需要进行如下修改:
|
||
|
||
```cpp
|
||
/**
|
||
* .pro 文件中增加 widgets
|
||
*/
|
||
QT += widgets
|
||
|
||
/**
|
||
* main.cpp 中替换 QGuiApplication 为 QApplication
|
||
*/
|
||
// #include <QGuiApplication>
|
||
#include <QApplication>
|
||
// QGuiApplication app(argc, argv);
|
||
QApplication app(argc, argv);
|
||
```
|
||
|
||
Dialog 对象默认不显示,当调用 Dialog 的 open() 方法后弹出窗口并阻塞父窗体的执行。
|
||
|
||
##### 8.3.2.1. FileDialog
|
||
|
||
FileDialog 为标准文件对话框。
|
||
|
||
```js
|
||
import QtQuick 2.12
|
||
import QtQuick.Window 2.12
|
||
import QtQuick.Controls 2.12
|
||
import QtQuick.Dialogs 1.3
|
||
import Qt.labs.platform 1.1 as QLP
|
||
|
||
Window {
|
||
width: 640
|
||
height: 480
|
||
visible: true
|
||
|
||
Button {
|
||
text: qsTr("保存到")
|
||
|
||
onClicked: {
|
||
savetoDialog.open()
|
||
}
|
||
}
|
||
|
||
FileDialog {
|
||
id: savetoDialog
|
||
title: qsTr("保存到…")
|
||
selectFolder: true
|
||
onAccepted: {
|
||
// ...
|
||
}
|
||
}
|
||
|
||
Component.onCompleted: {
|
||
savetoDialog.folder = QLP.StandardPaths.writableLocation(QLP.StandardPaths.DocumentsLocation)
|
||
}
|
||
}
|
||
```
|
||
|
||
##### 8.3.2.2. MessageDialog
|
||
|
||
MessageDialog 为标准消息对话框。
|
||
|
||
```js
|
||
import QtQuick 2.12
|
||
import QtQuick.Window 2.12
|
||
import QtQuick.Controls 2.12
|
||
import QtQuick.Dialogs 1.3
|
||
import Qt.labs.platform 1.1 as QLP
|
||
|
||
Window {
|
||
width: 640
|
||
height: 480
|
||
visible: true
|
||
|
||
function showInfoDialog(title, info, detail) {
|
||
infoDialog.title = title
|
||
infoDialog.text = info
|
||
infoDialog.detailedText = detail
|
||
infoDialog.open()
|
||
}
|
||
|
||
MessageDialog {
|
||
id: infoDialog
|
||
standardButtons: MessageDialog.Ok
|
||
title: ""
|
||
text: ""
|
||
}
|
||
|
||
Button {
|
||
text: qsTr("显示消息")
|
||
|
||
onClicked: {
|
||
showInfoDialog(qsTr("标题"), qsTr("信息"), qsTr("细节"))
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.4. ComboBox
|
||
|
||
ComboBox 为标准组合框
|
||
|
||
```js
|
||
import QtQuick 2.12
|
||
import QtQuick.Window 2.12
|
||
import QtQuick.Controls 2.12
|
||
|
||
Window {
|
||
width: 640
|
||
height: 480
|
||
visible: true
|
||
|
||
ComboBox {
|
||
id: baudrate
|
||
width: 88
|
||
height: 24
|
||
anchors.left: port.right
|
||
anchors.top: parent.top
|
||
currentIndex: 6
|
||
anchors.leftMargin: 8
|
||
anchors.topMargin: 8
|
||
model: ["2400", "4800", "9600", "19200", "38400", "57600", "115200", "230400", "460800"]
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.5. Grid
|
||
|
||
布局组件,可以布局多个行和列,每个子成员必须是相同类型,一般使用 Item 来组织多个不同类型。每个子成员必须设置高度和宽度,否则不被显示。
|
||
|
||
```js
|
||
import QtQuick
|
||
import QtQuick.Controls 6.3
|
||
|
||
Grid {
|
||
spacing: 8
|
||
rows: 2
|
||
columns: 2
|
||
|
||
Item {
|
||
height: 20; width: 68
|
||
Text {
|
||
anchors.fill: parent
|
||
text: qsTr("DemoItem0")
|
||
}
|
||
}
|
||
Item {
|
||
height: 20; width: 68
|
||
TextField {
|
||
anchors.fill: parent
|
||
text: qsTr("DemoItem1")
|
||
}
|
||
}
|
||
Item {
|
||
height: 20; width: 68
|
||
Button {
|
||
anchors.fill: parent
|
||
text: qsTr("DemoItem2")
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.6. ScrollView
|
||
|
||
ScrollView 会为其所容纳的对象创建滚动条。
|
||
|
||
### 8.7. QtQuick.ListView
|
||
|
||
TODO: header, headerPositioning
|
||
|
||
TODO: 如果通过设置 header 和 headerPositioning 参数来实现固定 header,且同时伴有 ScrollBar 的情况下,滚动条依然会覆盖到 header 上,效果不理想。此时最好在 ListView 之外使用 Row 嵌套 Label 来实现类似效果。
|
||
|
||
### 8.7. GridView
|
||
|
||
GridView 可以以网格的形式显示模型内容。可以使用 ListModel 或 XmlListModel 作为模型。
|
||
|
||
```js
|
||
import QtQuick 2.12
|
||
import QtQuick.Window 2.12
|
||
import QtQuick.Controls 2.12
|
||
|
||
Window {
|
||
width: 640
|
||
height: 480
|
||
visible: true
|
||
id: window
|
||
// 测试按钮, 添加项目
|
||
Button {
|
||
id: butAdd
|
||
width: 64
|
||
height: 24
|
||
text: qsTr("添加")
|
||
anchors.left: parent.left
|
||
anchors.top: parent.top
|
||
anchors.leftMargin: 4
|
||
anchors.topMargin: 4
|
||
|
||
onClicked: {
|
||
demoList.append({fname: "fruit", fcost: "price"})
|
||
}
|
||
}
|
||
// 测试按钮, 清空全部项目
|
||
Button {
|
||
id: butClear
|
||
width: 64
|
||
height: 24
|
||
text: qsTr("清空")
|
||
anchors.left: butAdd.right
|
||
anchors.top: parent.top
|
||
anchors.leftMargin: 4
|
||
anchors.topMargin: 4
|
||
|
||
onClicked: {
|
||
demoList.clear()
|
||
}
|
||
}
|
||
|
||
// 使用 Rectangle 作为背景.
|
||
Rectangle {
|
||
color: "#e0e0e0"
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.top: butAdd.bottom
|
||
anchors.bottom: parent.bottom
|
||
anchors.rightMargin: 4
|
||
anchors.bottomMargin: 4
|
||
anchors.leftMargin: 4
|
||
anchors.topMargin: 4
|
||
|
||
GridView {
|
||
id: demoView
|
||
anchors.fill: parent
|
||
cellWidth: 128
|
||
cellHeight: 32
|
||
contentWidth: cellWidth
|
||
contentHeight: cellHeight
|
||
// clip 属性非常重要, 如不设置该属性, 显示的单元信息有可能溢出 GridView 边界.
|
||
clip: true
|
||
|
||
// 滚动条设置.
|
||
ScrollBar.vertical: ScrollBar {
|
||
policy: ScrollBar.AlwaysOn
|
||
}
|
||
|
||
// 使用 ListModel 作为模型.
|
||
model: ListModel {
|
||
id: demoList
|
||
ListElement {fname: "Apple"; fcost: "2.45"}
|
||
ListElement {fname: "Orange"; fcost: "3.25"}
|
||
}
|
||
|
||
// 委托包含用于显示信息的 Text 对象和用于获取鼠标点击事件的 MouseArea
|
||
delegate: Rectangle {
|
||
id: demoDelegate
|
||
width: demoView.cellWidth
|
||
height: demoView.cellHeight
|
||
color: "#f6f6f6"
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
onClicked: {
|
||
// index 为系统传入参数, 代表当前点击对象的索引值.
|
||
// fcost 和 fcost 为 demoList 中的对象属性, 需要特别注意其用法.
|
||
demoList.setProperty(index, "fcost", "free")
|
||
console.log(demoList.get(index).fcost)
|
||
}
|
||
}
|
||
|
||
Text {
|
||
anchors.fill: parent
|
||
horizontalAlignment: Text.AlignHCenter
|
||
verticalAlignment: Text.AlignVCenter
|
||
text: fname+": "+fcost
|
||
}
|
||
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.8. BusyIndicator
|
||
|
||
用于指示工作状态,设置 BusyIndicator 的 running 属性为 true 将默认显示一个旋转的圆圈;设置 running 属性为 false 则 BusyIndicator 将不显示。
|
||
|
||
```js
|
||
import QtQuick 2.12
|
||
import QtQuick.Window 2.12
|
||
import QtQuick.Controls 2.12
|
||
|
||
Window {
|
||
width: 640
|
||
height: 480
|
||
visible: true
|
||
|
||
BusyIndicator {
|
||
id: busyInd
|
||
anchors.horizontalCenter: parent.horizontalCenter
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
running: true
|
||
}
|
||
}
|
||
```
|
||
|
||
可以对 BusyIndicator 进行自定义。
|
||
|
||
### 8.9. VirtualKeyboard
|
||
|
||
一些涉及触屏的应用会涉及到虚拟键盘/软键盘的应用,不同系统平台上往往会提供不同的软键盘工具,但相比之下,Qt 内嵌的 VirtualKeyboard 更加易用,并具有很好的跨平台能力,中文(拼音)、英文以及其他主要语言的支持能力也比较好。
|
||
|
||
若使用 VirtualKeyboard,只需要在创建“Qt Quick Application”时选中“Use Qt Virtual Keyboard”(旧版本 QtCreator 没有该选项)即可自动向项目添加 VirtualKeyboard 功能。
|
||
|
||
对比源码不难发现,启用 VirtualKeyboard 后主要在 main.cpp 中增加了:
|
||
|
||
```cpp
|
||
qputenv("QT_IM_MODULE", QByteArray("qtvirtualkeyboard"));
|
||
```
|
||
|
||
在 main.qml 中增加了:
|
||
|
||
```js
|
||
import QtQuick.VirtualKeyboard 2.4
|
||
|
||
Window {
|
||
InputPanel {
|
||
states: State {
|
||
// ...
|
||
}
|
||
transitions: Transition {
|
||
// ...
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
可以使用 VirtualKeyboardSettings 对 VirtualKeyboard 进行一些设置,这是一个全局对象,在 QML 中直接通过 VirtualKeyboardSettings.xxxxx 方式使用即可。VirtualKeyboardSettings 可对键盘风格、语言列表进行设置。需要特殊说明的是,VirtualKeyboard 所支持的语言列表是在编译时决定的,通过 VirtualKeyboardSettings.availableLocales 可以获得当前支持的语言列表;而 VirtualKeyboardSettings.activeLocales 可以在应用程序中临时限定允许使用的语言,最终在 Qt 的虚拟键盘中允许切换的语言为二者的交集。如下示例限制 VirtualKeyboard 只可以使用“简体中文”和“英文”输入法,如果编译 VirtualKeyboard 没有使能中文输入法则最终只能使用英文输入:
|
||
|
||
```js
|
||
import QtQuick.VirtualKeyboard.Settings 2.4
|
||
|
||
Window {
|
||
Component.onCompleted: {
|
||
VirtualKeyboardSettings.activeLocales = ["zh_CN", "en_US"]
|
||
}
|
||
}
|
||
```
|
||
|
||
如果是使用 Qt 提供的安装包来安装 VirtualKeyboard 模块则默认开启了中文支持,部分系统如 Ubuntu 中使用 apt 进行安装的可能不支持中文,可改用官方安装包来安装 Qt 或使用源码自行编译,注意在编译 VirtualKeyboard 的 qmake 阶段需要增加:
|
||
|
||
```makefile
|
||
CONFIG+="lang-en_GB lang-zh_CN"
|
||
```
|
||
|
||
更多关于 VirtualKeyboard 的应用可以参考官方自带示例。
|
||
|
||
## 9. 多文档开发
|
||
|
||
### 9.1. 多 QML 文件的管理
|
||
|
||
在创建基于 Qt6 的 QML 项目时,不会包含 qml.qrc 文件,系统编译时自动创建该文件,创建的依据是 .pro 文件中的 resources.xxx 字段:
|
||
|
||
```js
|
||
resources.files = main.qml
|
||
|
||
resources.prefix = /$${TARGET}
|
||
```
|
||
|
||
当我们向工程增加自定义 QML 文件时,会被增加到 DISTFILES 字段,比如:
|
||
|
||
```js
|
||
DISTFILES += \
|
||
Demo.qml
|
||
```
|
||
|
||
这会导致无法将 Demo.qml 加如到系统自建的 .qrc 文件中,此时会报:
|
||
|
||
```js
|
||
failed to load component ... is not a type
|
||
```
|
||
|
||
错误,此时需要手动添加 Demo.qml 到 resources.files 字段中。
|
||
|
||
### 9.2. 如何引用自定义 QML 文件
|
||
|
||
引用同级目录下的 QML 文件时不需要 import,在引用其他文件夹下的 QML 文件时需要:
|
||
|
||
```js
|
||
import "./DemoQml"
|
||
```
|
||
|
||
QML 文件必须以大写字母开头,以表明这是一个 QML 类型。
|
||
|
||
### 9.3. 使用另一 QML 文件中的元件或属性
|
||
|
||
可以使用:
|
||
|
||
```js
|
||
property alias <别名>:<属性名称/ID>
|
||
```
|
||
|
||
将一个 QML 文件中的某属性,某子组件的属性或某个子组件变为外部可见。
|
||
|
||
### 9.4. 示例
|
||
|
||
假设存在 DemoQml/Demo.qml 文件,该文件内容如下:
|
||
|
||
```js
|
||
import QtQuick
|
||
import QtQuick.Controls 6.3
|
||
|
||
Item {
|
||
property alias dia: diaDemo
|
||
|
||
Dialog {
|
||
id: diaDemo
|
||
title: qsTr("DemoDialog")
|
||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||
|
||
onRejected: console.log("Cancel clicked")
|
||
}
|
||
}
|
||
```
|
||
|
||
注意这里使用的是 QtQuick.Controls 中的 Dialog,需要保持 main.cpp 中的 app 为 QGuiApplication 类型。
|
||
|
||
.pro 文件的相关字段如下:
|
||
|
||
```js
|
||
resources.files = main.qml \
|
||
DemoQml/Demo.qml
|
||
|
||
resources.prefix = /$${TARGET}
|
||
```
|
||
|
||
main.qml 文件内容如下:
|
||
|
||
```js
|
||
import QtQuick
|
||
import "./DemoQml"
|
||
|
||
Window {
|
||
id: window
|
||
width: 640
|
||
height: 480
|
||
visible: true
|
||
title: qsTr("Hello World")
|
||
|
||
Demo {
|
||
id: compDemo
|
||
dia.onAccepted: {
|
||
console.log("clicked Ok button.")
|
||
}
|
||
}
|
||
|
||
Button {
|
||
anchors.fill: parent
|
||
text: qsTr("显示 Dialog")
|
||
onClicked: {
|
||
compDemo.dia.open()
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 10. QML 与 C++ 交互
|
||
|
||
QML 与 C++ 交互的主要实现方式是:
|
||
|
||
1. QML 访问 C++ 中声明的类型;
|
||
2. QML 访问 C++ 中创建的对象;
|
||
3. QML 调用 C++ 中的方法或发射信号;
|
||
4. QML 响应 C++ 中产生的信号;
|
||
5. C++ 访问 QML 对象。
|
||
|
||
QML 与 C++ 之间主要通过信号槽机制来传递消息。
|
||
|
||
### 10.1. QML 访问 C++ 中声明的类型
|
||
|
||
QML 使用 C++ 中声明的类型可以为类、结构体或枚举等。若需要将 C++ 类导出给 QML,则需要使用 qmlRegisterType() 方法进行注册:
|
||
|
||
```cpp
|
||
qmlRegisterType<Type>("package.Type", <version>, <sub-version>, "Type");
|
||
```
|
||
|
||
之后在 QML 中使用 C++ 类创建对象即可。若需要在 QML 中使用类中定义的枚举,格式如下:
|
||
|
||
```js
|
||
<类名>.<枚举值>
|
||
```
|
||
|
||
若 QML 需要使用 C++ 中声明的枚举类型,则该枚举需要使用 Q_ENUM() 进行修饰(必须放在声明之后);若访问属性成员则需要使用 Q_PROPERTY() 进行修饰;若访问方法则该方法需要使用 Q_INVOKABLE() 来修饰。C++ 中的信号不需要特殊处理,QML 可直接访问。
|
||
|
||
若对象在 C++ 上下文中创建,并需要在 QML 中使用该对象,可以使用:
|
||
|
||
```cpp
|
||
QQmlApplicationEngine engine;
|
||
engine.rootContext()->setContextProperty("qmlObj", cObj);
|
||
```
|
||
|
||
将 C++ 对象注册到 QML 上下文环境中。
|
||
|
||
### 10.2. C++ 访问 QML 对象
|
||
|
||
在 QML 中为对象添加 objectName 属性后,在 C++ 中可使用:
|
||
|
||
```cpp
|
||
auto root = engine.rootObjects();
|
||
auto qmlObj = root.first()->findChild<QObject*>("object name");
|
||
```
|
||
|
||
查找该对象,再连接信号槽等。
|
||
|
||
大部分情况下在 QML 中访问 C++ 即可实现较完善的功能,QML 传递信息给 C++ 完全可以通过信号槽机制实现。除非需要在 C++ 中动态创建对象并连接到 QML 中的信号槽,否则没必要这样设计。
|
||
|
||
### 10.3. 通过信号槽传递自建类型
|
||
|
||
当使用信号槽机制时,需要注意一点:如果需要通过信号槽传递自建类型数据,需要使用 qRegisterMetaType() 方法进行注册。
|
||
|
||
```cpp
|
||
qRegisterMetaType<MyClass>("Myclass");
|
||
```
|
||
|
||
### 10.4. QML 与 C++ 交互综合示例
|
||
|
||
该示例包含以下文件:
|
||
|
||
- QmlDemo.por
|
||
- qml.qrc
|
||
- DemoDirect.h
|
||
- DemoDirect.cpp
|
||
- DemoJoint.h
|
||
- DemoJoint.cpp
|
||
- DemoIndirect.h
|
||
- DemoIndirect.cpp
|
||
- main.qml
|
||
- main.cpp
|
||
|
||
QmlDemo.por 内容如下:
|
||
|
||
```js
|
||
QT += quick widgets
|
||
|
||
# You can make your code fail to compile if it uses deprecated APIs.
|
||
# In order to do so, uncomment the following line.
|
||
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||
|
||
SOURCES += \
|
||
DemoDirect.cpp \
|
||
DemoIndirect.cpp \
|
||
DemoJoint.cpp \
|
||
main.cpp
|
||
|
||
RESOURCES += qml.qrc
|
||
|
||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||
QML_IMPORT_PATH =
|
||
|
||
# Additional import path used to resolve QML modules just for Qt Quick Designer
|
||
QML_DESIGNER_IMPORT_PATH =
|
||
|
||
# Default rules for deployment.
|
||
qnx: target.path = /tmp/$${TARGET}/bin
|
||
else: unix:!android: target.path = /opt/$${TARGET}/bin
|
||
!isEmpty(target.path): INSTALLS += target
|
||
|
||
HEADERS += \
|
||
DemoDirect.h \
|
||
DemoIndirect.h \
|
||
DemoJoint.h
|
||
```
|
||
|
||
qml.qrc 内容如下:
|
||
|
||
```xml
|
||
<RCC>
|
||
<qresource prefix="/">
|
||
<file>main.qml</file>
|
||
</qresource>
|
||
</RCC>
|
||
```
|
||
|
||
DemoDirect.h 内容如下
|
||
|
||
```cpp
|
||
#ifndef DEMODIRECT_H
|
||
#define DEMODIRECT_H
|
||
|
||
#include <QObject>
|
||
|
||
class DemoDirect : public QObject
|
||
{
|
||
Q_OBJECT
|
||
// 声明 cppStr 为只读属性. NOTIFY 信号是必须的.
|
||
Q_PROPERTY(QString cppStr READ readCppStr NOTIFY cppStrChanged)
|
||
public:
|
||
explicit DemoDirect(QObject *parent = nullptr);
|
||
|
||
// 使用 Q_INVOKABLE 向 QML 声明函数方法.
|
||
Q_INVOKABLE void writeCppStr(QString str);
|
||
Q_INVOKABLE QString readCppStr();
|
||
|
||
signals:
|
||
void cppStrChanged(QString str);
|
||
|
||
protected:
|
||
QString cppStr;
|
||
|
||
};
|
||
|
||
#endif // DEMODIRECT_H
|
||
```
|
||
|
||
DemoDirect.cpp 内容如下
|
||
|
||
```cpp
|
||
#include "DemoDirect.h"
|
||
#include <QDebug>
|
||
|
||
DemoDirect::DemoDirect(QObject *parent)
|
||
: QObject{parent}
|
||
{
|
||
cppStr = "Hello";
|
||
}
|
||
|
||
void DemoDirect::writeCppStr(QString str)
|
||
{
|
||
cppStr = str;
|
||
qDebug()<<"[CPP:DemoDirect:writeCppStr]srt="<<cppStr;
|
||
emit cppStrChanged(cppStr);
|
||
}
|
||
|
||
QString DemoDirect::readCppStr()
|
||
{
|
||
return cppStr;
|
||
}
|
||
```
|
||
|
||
DemoJoint.h 内容如下
|
||
|
||
```cpp
|
||
#ifndef DEMOJOINT_H
|
||
#define DEMOJOINT_H
|
||
|
||
#include <QThread>
|
||
#include <QSemaphore>
|
||
|
||
class DemoJoint : public QThread
|
||
{
|
||
Q_OBJECT
|
||
public:
|
||
enum DemoEnum {
|
||
DE_OPEN,
|
||
DE_READ,
|
||
DE_WRITE,
|
||
DE_CLOSE,
|
||
|
||
DE_OPEN_DONE,
|
||
DE_READ_DONE,
|
||
DE_WRITE_DONE,
|
||
DE_CLOSE_DONE
|
||
};
|
||
// 向 QML 声明 DemoEnum 类型.
|
||
Q_ENUM(DemoEnum)
|
||
|
||
explicit DemoJoint(QObject *parent = nullptr);
|
||
~DemoJoint();
|
||
|
||
signals:
|
||
// 从 QML 调用的信号, 信号不必使用 Q_INVOKABLE 进行显式声明.
|
||
void qml2Cpp(DemoJoint::DemoEnum op);
|
||
// 从 DemoIndirect 中发射该信号, QML 中响应该信号.
|
||
void cpp2Qml(DemoJoint::DemoEnum op);
|
||
|
||
protected:
|
||
class DemoIndirect* demoIndirect;
|
||
|
||
void run() override;
|
||
|
||
private:
|
||
QSemaphore initLock;
|
||
|
||
};
|
||
|
||
#endif // DEMOJOINT_H
|
||
```
|
||
|
||
DemoJoint.cpp 内容如下
|
||
|
||
```cpp
|
||
#include "DemoJoint.h"
|
||
#include "DemoIndirect.h"
|
||
|
||
DemoJoint::DemoJoint(QObject *parent)
|
||
: QThread{parent}
|
||
{
|
||
this->start();
|
||
initLock.acquire(1);
|
||
}
|
||
|
||
DemoJoint::~DemoJoint()
|
||
{
|
||
this->wait();
|
||
}
|
||
|
||
void DemoJoint::run()
|
||
{
|
||
this->setPriority(QThread::NormalPriority);
|
||
demoIndirect = new DemoIndirect(this, nullptr);
|
||
connect(this, &DemoJoint::qml2Cpp, demoIndirect, &DemoIndirect::onQml2Cpp, Qt::QueuedConnection);
|
||
initLock.release(1);
|
||
this->exec();
|
||
demoIndirect->deleteLater();
|
||
}
|
||
```
|
||
|
||
DemoIndirect.h 内容如下
|
||
|
||
```cpp
|
||
#ifndef DEMOINDIRECT_H
|
||
#define DEMOINDIRECT_H
|
||
|
||
#include <QObject>
|
||
#include "DemoJoint.h"
|
||
|
||
class DemoIndirect : public QObject
|
||
{
|
||
Q_OBJECT
|
||
public:
|
||
explicit DemoIndirect(DemoJoint* j, QObject *parent = nullptr);
|
||
|
||
public slots:
|
||
// 在 DemoJoint run() 方法中连接了该槽.
|
||
void onQml2Cpp(DemoJoint::DemoEnum op);
|
||
|
||
signals:
|
||
|
||
private:
|
||
DemoJoint* joint;
|
||
|
||
};
|
||
|
||
#endif // DEMOINDIRECT_H
|
||
```
|
||
|
||
DemoIndirect.cpp 内容如下
|
||
|
||
```cpp
|
||
#include "DemoIndirect.h"
|
||
#include <QDebug>
|
||
|
||
DemoIndirect::DemoIndirect(class DemoJoint* j, QObject *parent)
|
||
: QObject{parent}
|
||
{
|
||
joint = j;
|
||
}
|
||
|
||
void DemoIndirect::onQml2Cpp(DemoJoint::DemoEnum op)
|
||
{
|
||
qDebug()<<"[CPP:DemoIndirect:onQml2Cpp]op="<<op;
|
||
switch (op) {
|
||
case DemoJoint::DE_OPEN:
|
||
emit joint->cpp2Qml(DemoJoint::DE_OPEN_DONE);
|
||
break;
|
||
default:
|
||
emit joint->cpp2Qml(DemoJoint::DE_CLOSE_DONE);
|
||
break;
|
||
}
|
||
}
|
||
```
|
||
|
||
main.cpp 内容如下:
|
||
|
||
```cpp
|
||
#include <QApplication>
|
||
#include <QQmlApplicationEngine>
|
||
#include <QQmlContext>
|
||
#include "DemoJoint.h"
|
||
#include "DemoDirect.h"
|
||
|
||
int main(int argc, char *argv[])
|
||
{
|
||
// 注册 DemoDirect 类型, 否则无法在 QML 中使用 DemoDirect 创建对象.
|
||
qmlRegisterType<DemoDirect>("project.DemoDirect", 1, 0, "DemoDirect");
|
||
// 注册 DemoJoint 类型, 否则无法在 QML 中使用 DemoJoint.DemoEnum 类型.
|
||
qmlRegisterType<DemoJoint>("project.DemoJoint", 1, 0, "DemoJoint");
|
||
// 注册用于在信号槽中使用的类型.
|
||
qRegisterMetaType<DemoJoint::DemoEnum>("DemoJoint::DemoEnum");
|
||
|
||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||
#endif
|
||
QApplication app(argc, argv);
|
||
|
||
// 在 C 上下文创建 demoJoint 对象.
|
||
DemoJoint* demoJoint = new DemoJoint();
|
||
|
||
QQmlApplicationEngine engine;
|
||
const QUrl url(QStringLiteral("qrc:/main.qml"));
|
||
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
|
||
&app, [url](QObject *obj, const QUrl &objUrl) {
|
||
if (!obj && url == objUrl)
|
||
QCoreApplication::exit(-1);
|
||
}, Qt::QueuedConnection);
|
||
|
||
// 将 C 环境下的 demoJoint 注册到 QML 上下文中.
|
||
engine.rootContext()->setContextProperty("demoJoint", demoJoint);
|
||
engine.load(url);
|
||
|
||
return app.exec();
|
||
}
|
||
```
|
||
|
||
main.qml 内容如下:
|
||
|
||
```js
|
||
import QtQuick 2.12
|
||
import QtQuick.Window 2.12
|
||
import QtQuick.Controls 2.12
|
||
// 导入自定义包, 包名与版本号要与 C 环境下注册的一致.
|
||
import project.DemoDirect 1.0
|
||
import project.DemoJoint 1.0
|
||
|
||
Window {
|
||
width: 640
|
||
height: 480
|
||
visible: true
|
||
id: window
|
||
|
||
// 直接从 QML 环境下实例化 C 对象.
|
||
DemoDirect {
|
||
id: demoDirect
|
||
}
|
||
|
||
Text {
|
||
id: demoText
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.top: parent.top
|
||
anchors.rightMargin: 8
|
||
anchors.leftMargin: 8
|
||
anchors.topMargin: 8
|
||
// 显示 demoDirect 的 cppStr 属性值, 注意这是一个只读属性.
|
||
text: demoDirect.cppStr
|
||
}
|
||
|
||
Button {
|
||
id: demoBut
|
||
width: 64
|
||
height: 24
|
||
text: qsTr("信号测试")
|
||
anchors.left: parent.left
|
||
anchors.top: demoText.bottom
|
||
anchors.leftMargin: 8
|
||
anchors.topMargin: 8
|
||
|
||
onClicked: {
|
||
// 从 QML 环境中向 C 环境发送信号.
|
||
demoJoint.qml2Cpp(DemoJoint.DE_OPEN)
|
||
// 直接调用 C 方法.
|
||
demoDirect.writeCppStr("Demo")
|
||
}
|
||
}
|
||
|
||
// 使用 Connections 与 C 信号建立连接, 并处理 cpp2Qml() 信号.
|
||
Connections {
|
||
target: demoJoint
|
||
function onCpp2Qml(op) {
|
||
console.log("[QML:onCpp2Qml]op=", op)
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 11. Windows 下 QML 程序的打包发布
|
||
|
||
Qt 提供了导出 Qt 环境变量的命令行脚本,比如“Qt 5.15.2 (MinGW 8.1.0 64-bit)”,运行该脚本可进入带有 Qt 环境变量的命令行界面,之后可通过如下命令打包程序(编译生成的可执行程序需要拷贝到\<Package Output Path\>):
|
||
|
||
```bash
|
||
cd <Package Output Path>
|
||
windeployqt <Exe File> [--qmldir <Project QML File Path>]
|
||
```
|
||
|
||
Qt 自带的打包程序会添加额外的库,如果想进一步减小体积,可手动筛减。
|
||
|
||
## 12. 外部参考资料
|
||
|
||
1. [深入了解JS中的整数](https://www.jianshu.com/p/1ba45c3894ab)
|
||
2. [QML 中的信号与槽](https://blog.csdn.net/Love_XiaoQinEr/article/details/123746983)
|
||
3. [QML 信号与响应方法的总结](https://zhuanlan.zhihu.com/p/576316521)
|
||
4. [QML 控件类型:ScrollBar、ScrollIndicator](https://blog.csdn.net/kenfan1647/article/details/122522063)
|
||
5. [QML 类型:GridView](https://blog.csdn.net/kenfan1647/article/details/120761466)
|
||
6. [Qt Quick 常用元素:ComboBox(下拉列表) 与 ProgressBar(进度条)](https://www.cnblogs.com/linuxAndMcu/p/11949814.html)
|
||
7. [【QML Model-View】ListView-增删改查(二)](https://www.cnblogs.com/linuxAndMcu/p/13597106.html)
|
||
8. [关于 Q_ENUMS 和 Q_ENUM 的区别和用法](https://blog.csdn.net/lsylovezsl/article/details/108766437)
|
||
9. [C++ 共享枚举类型给 QML](https://www.cnblogs.com/linkyip/p/14462288.html)
|
||
10. [QML Connections: Implicitly defined onFoo properties in Connections are deprecated.](https://blog.csdn.net/weixin_43720622/article/details/112346039)
|
||
11. [QML 调用 C++ 方法](https://blog.csdn.net/woshouji1/article/details/121348179)
|
||
12. [Qml 与 C++ 交互3:Qml 的信号与 C++ 的槽函数连接](https://blog.csdn.net/tanxuan231/article/details/124990296)
|
||
13. [Qt-虚拟键盘](https://blog.csdn.net/qq_39175540/article/details/87972667)
|
||
14. [Qt5软键盘实现中文拼音输入法](https://blog.csdn.net/onlyshi/article/details/78408000)
|
||
15. [Qt6中加载自定义qml遇到的问题](https://blog.csdn.net/youyicc/article/details/124367513)
|
||
16. [QML控件类型:Dialog(Qt Quick Controls 模块)](https://blog.csdn.net/kenfan1647/article/details/123109241)
|