- 1. 基础部分
- 2. 项目配置
- 3. 多文档开发
- 4. QML 与 C++ 交互
- 5. 打包发布与部署
- 6. 常用模块与组件
- 6.1. Rectangle
- 6.2. QtQuick.Loader
- 6.3. QtQuick.Controls.Button
- 6.4. Dialog 对象
- 6.5. ComboBox
- 6.6. Grid
- 6.7. ScrollView
- 6.8. QtQuick.ListView
- 6.9. GridView
- 6.10. BusyIndicator
- 6.11. VirtualKeyboard
- 6.12. Multimedia
- 6.13. ChartView
- 6.14. MenuBar/Menu/MenuItem
- 6.15. StackLayout/TabBar/TabButton
- 6.16. SplitView
- 6.17. TableView
- 6.18. Qt Data Visualization
- 7. Linux 下为 Qt Quick 应用程序隐藏鼠标指针
- 8. 外部参考资料
1. 基础部分
1.1. 方法/属性名称的大小写
QML 语法与 JS 相同,具体请参考相关教程,此处只对一些需要特殊注意的地方进行说明。
QML 的属性、变量、函数等一般为小写字母开头,大写字母开头通常具有特殊意义,比如使用 QML 连接名为 demo() 的信号时,系统将自动连接 onDemo() 作为其槽函数。以下以连接 Button 的 clicked 信号为例进行演示:
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 文件中先写的对象显示在下层,后写的对象显示在上层。在显示时,上层的对象覆盖下层对象。
1.3. 使用自定义元件
QML 中元件被定义在包中,通过 import 引用对应的包来使用其中的元件,一个文件夹可以构成一个包。
也可以自定义元件,每个 QML 文件都可以是一个自定义元件,QML 文件的名称就是元件的名称。例如定义一个 SubRec 元件,首先创建 SubRec.qml 文件,内容如下:
import QtQuick 2.12
Rectangle {
color: "#bad6bf"
}
当 QML 文件作为元件使用时,文件名必须以大写字母开头,以表明这是一个 QML 类型。
如果调用 SubRec 的地方与 SubRec.qml 处于同一文件夹中,则认为他们是同一个包中的元件,不需要 import 就可以直接使用。
如果调用 SubRec 的地方与 SubRec.qml 处于不同文件夹中(子文件夹也算不同文件夹),则认为不是同一个包中的元件,需要 import 才可以使用,例如,上面的 SubRec.qml 处于子文件夹 SubComp 中时,使用相对路径方式进行引用,如下:
import QtQuick 2.12
import QtQuick.Window 2.12
import "./SubComp"
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Rectangle {
color: "#bfd1da"
anchors.fill: parent
SubRec {
anchors.rightMargin: 64
anchors.leftMargin: 64
anchors.bottomMargin: 64
anchors.topMargin: 64
anchors.fill: parent
}
}
}
1.4. 包别名
编写 QML 应用时需要像 include 那样引用一些软件包,有的时候软件包中的对象会有重名的情况,比如 QtQuick.Dialogs 1.3 和 Qt.labs.qmlmodels 1.0 中均包含了 FileDialog 对象,但他们的功能并不相同。此时可以使用 import as 来实现类似别名或者命名空间的功能。
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)
}
}
1.5. 对象 ID
QML 中可以使用各种元素来构建 UI,比如 Window、Rectangle、Item 等。被使用的元素形成具体的对象,在运行时创建对应实例。对象可以被分配 ID,对象之间可以使用 ID 相互引用,这种引用是“全局”的。例如,在不同 QML 文件中,分别创建主对象和子对象,在子对象中可以直接引用主对象的 ID。
主对象 mainRec 在 main.qml 文件中,内容如下:
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
id: root
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Rectangle {
id: mainRec
color: "#bfd1da"
anchors.fill: parent
SubRec {
anchors.rightMargin: 64
anchors.leftMargin: 64
anchors.bottomMargin: 64
anchors.topMargin: 64
anchors.fill: parent
}
}
}
子对象在 SubRec.qml 中,内容如下:
import QtQuick 2.12
import QtQuick.Controls 2.12
Rectangle {
id: root
color: "#bad6bf"
Timer {
id: timeout
interval: 3000 // 10s
running: true
repeat: false
onTriggered: {
mainRec.color = "#bad6bf"
root.color = "#bfd1da"
}
}
}
虽然并没有在 SubRec.qml 文件中对 mainRec 进行任何声明或者引用,但仍然可以直接使用,QML 引擎能够在运行时找到 mainRec。
另外我们注意到,SubRec.qml 和 main.qml 都使用了 root 分别作为 Rectangle 和 Window 对象的 ID,此时在 SubRec.qml 中优先使用“局部 ID”,也就是 Rectangle 类型的对象。
使用 QML 创建 UI 时,各对象逐级构建,形成树形结构。不难发现,QML 引擎能够动态推演所使用的 ID 指的是具体哪个对象(因此跨文件使用对象时不需要声明),其规则为从当前的使用位置开始,逐级向树的根部搜索,直到搜寻到一个匹配的对象为止。
1.6. 组件级联
组件级联涉及到两个主要问题:
- 父组件对子组件的管理。
- 子组件对父组件的访问。
1.6.1. 父组件对子组件的管理
通常,子组件要分配给父组件的一个属性上。如果没有指定父属性,子组件会被添加到父组件的默认属性中,通常是父组件的 children 列表,父组件可以通过 属性或 children 列表访问子组件。
当然,也可以定义一个 default/default alias 属性(Default Property 是一个特殊的属性,它允许在声明对象时,若其子对象没有明确指定要分配到的属性名,则子对象会被自动赋值给默认属性。),来指定子组件对象默认分配给哪个属性。
例如 MyType.qml:
import QtQuick 2.0
Item {
width: 100
height: 100
// 自定义默认属性
default property list<Item> subObject: []
// default property alias subObject: content.children // 别名定义默认属性
Rectangle {
id: content
width: parent.width
height: parent.height
color: "lightblue"
}
}
接下来,在主文件 main.qml 中使用这个自定义类型:
import QtQuick 2.0
import QtQuick.Controls 2.0
ApplicationWindow {
visible: true
width: 300
height: 300
MyType {
x: 50
y: 50
// Button 子对象将被添加到 MyType 默认属性中
Button {
text: "Click me"
}
}
}
在使用 default 属性时,有几点需要注意:
- 隐式的属性赋值:当你定义了一个默认属性后,QML 会隐式地将子对象添加到这个属性中,而无需显式声明属性名称。这使代码看起来更简洁,但也可能增加理解难度,尤其是在代码较为复杂时。
- 你可以将 default 属性理解为一种访问派生类子节点的方式。使用 default,可以通过父类访问到它的子节点,而不必显式地声明属性名。这种特性在某些情况下可以减少代码重复,但也可能导致代码的可读性降低。
- 避免滥用:在实际项目中,除非确实需要简化代码并且明确其带来的影响,否则应慎用 default 属性。它的隐式特性可能使代码的逻辑不够清晰,尤其是在团队合作中,这种隐式行为可能导致他人难以理解代码的结构。
1.6.2. 子组件对父组件的访问
子组件(无论是 Qt 原生组件还是自定义组件)可以通过 parent 访问父组件中的属性或者方法,例如:
ChartView {
MouseArea {
anchors.fill: parent
onDoubleClicked: {
parent.zoomIn()
}
}
}
1.6.3. 附加属性与附加信号处理器
附加属性(Attached Properties)是 QML 中一种特殊类型的属性,它允许对象访问与它相关联的其他对象或类型的额外属性,其主要作用在于:
- 提供与特定对象相关联的额外属性
- 允许访问与组件相关的元信息或功能
- 实现类似"装饰器"模式的功能
附加信号处理器(Attached Signal Handlers)是与附加属性相关的特殊信号处理器,用于响应附加对象发出的信号,其主要作用在于:
- 处理来自附加对象的信号
- 提供与特定上下文相关的事件处理
- 实现更清晰的事件处理逻辑分离
使用附加属性与附加信号处理器的方法如下:
<AttachingType>.<propertyName>
<AttachingType>.on<SignalName>
常用附加属性有:
- ListView 和 GridView 中的 ListView.isCurrentItem
- Item 的 Keys 附加属性处理键盘输入
- Component 的附加属性访问组件相关信息
常用附加信号处理器有:
- 处理键盘事件 (Keys.onPressed)
- 处理布局变化 (Layout.onCompleted)
- 处理动画信号 (Animation.onFinished)
例如 ListView 类型的 ListView.isCurrentItem 就可以被附加到其委托对象上,用于判断当前项是否被选中:
import QtQuick
ListView {
width: 240; height: 320
model: 3
delegate: Rectangle {
width: 100; height: 30
color: ListView.isCurrentItem ? "red" : "yellow"
}
}
而 Component.onCompleted 附加信号处理器则常用于在 QML 程序界面完成加载后做一些事情(比如设置一些属性的初始值):
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()
}
}
附加属性与附加信号处理器可以被附加到对象的子对象直接访问,但不代表可以被不相关的子对象或者子子对象访问:
import QtQuick
ListView {
width: 240; height: 320
model: 3
delegate: Item {
width: 100; height: 30
Rectangle {
width: 100; height: 30
color: ListView.isCurrentItem ? "red" : "yellow" // WRONG! This won't work.
}
}
}
由于 ListView.isCurrentItem 只被附加到 delegate 对象上,并不包含其子对象,所以 Rectangle 无法直接访问 ListView.isCurrentItem。如果想保持上面的结构,应该使用下面的写法:
ListView {
delegate: Item {
id: delegateItem
width: 100; height: 30
Rectangle {
width: 100; height: 30
color: delegateItem.ListView.isCurrentItem ? "red" : "yellow" // correct
}
}
}
1.7. 全局属性
使用 property 关键字可以定义全局属性,该语法格式如下:
[default] [required] [readonly] property <propertyType> <propertyName>
属性名称必须以小写字母开头,并且只能包含字母、数字和下划线。
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"
}
1.8. 信号与槽
在 QML 中连接信号槽主要有以下 5 种方法。
1.8.1. 信号与信号处理器
signal()
on<Signal>: {}
这是最常见的方法,比如 onClicked: {} 槽。
1.8.2. 属性变化信号与属性变化信号处理器
property
on<Property>Changed: {}
QML 规范,当属性变化后会发送一个 <Property>Changed 信号。从 C 环境创建属性时需要注册 NOTIFY 信号,其命名格式也需要遵循该规范。
1.8.3. 附加属性与附加信号处理器
Attached Signal(附加属性)
XX元素.on<附加属性>: {}
最常见的是 Component.onCompleted:{} 槽。
1.8.4. Connections 建立信号与槽的连接
Connections {
target: 发送者
发送者信号处理器:{}
}
前面的信号连接都嵌入在某个对象中,当信号与槽的连接不依赖具体对象时,需要将其嵌入到 Connections 对象中。Connections 的 target 指向信号的发送者,一个 Connections 可以绑定同一个 target 的多个信号处理机。
Connections {
target: whomSentTheSignal
function onSignal0(arg) {
console.log(arg)
}
function onSignal1(arg) {
console.log(arg)
}
// ...
}
1.8.5. connect()方法
使用 connect() 方法,可以为信号指定非 on<Signal> 格式命名的响应方法,也可以指令信号触发下一个信号形成信号的连锁反应。
[元素对象.]信号.connect(信号/方法)
示例如下:
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
}
}
1.8.6. 自定义信号
用以下形式声明自定义的信号:
signal <name>[([<type> <parameter name>[, ...]])]
示例如下:
Rectangle {
id: root
signal mysignal(int x, int y)
MouseArea {
anchors.fill: parent
onPressed: root.mysignal(mouse.x, mouse.y)
}
}
2. 项目配置
2.1. 设置主题风格
直接 import 对应的 Theme Style:
import QtQuick.Controls.Material
import QtQuick.Controls.Basic
import QtQuick.Controls.Fusion
import QtQuick.Controls.Imagine
import QtQuick.Controls.Universal
import QtQuick.Controls.Windows
以上主题选择一个即可。风格定制直接写在根 QML 对象中即可:
Window {
Material.theme: Material.Light
Material.accent: Material.Indigo
}
或者在 main.cpp 中设置:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
// ...
QQuickStyle::setStyle("Material");
// ...
return app.exec();
}
2.2. 添加图标
更详细内容见:
2.2.1. 制作图标
先安装 ImageMagic 工具,并将其添加到系统 PATH 下方便使用。
使用以下命令将多个不同尺寸的图片打包成一个 ICO 文件。
magick.exe convert icon-16.png icon-32.png icon-256.png myappico.ico
2.2.2. Windows 下添加图标到应用
2.2.2.1. 使用 QMake 的图标添加方式
在 Qt .pro 文件中添加以下内容,以便将图标编译到 Qt 程序中:
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:
IDI_ICON1 ICON "myappico.ico"
Then, add this line to your myapp.pro file:
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.
2.2.2.1. 使用 Qt6 CMake 的图标添加方式
在 CMakeLists.txt 中添加并修改以下内容:
if (${CMAKE_SYSTEM_NAME} MATCHES "Windows")
enable_language("RC")
set (APP_ICON_RESOURCE_WINDOWS ${CMAKE_CURRENT_SOURCE_DIR}/Path/to/Your/RCFile.rc)
endif()
qt_add_executable(YOUR_APP_NAME
main.cpp
${APP_ICON_RESOURCE_WINDOWS}
)
RCFile.rc 内容如下:
IDI_ICON1 ICON DISCARDABLE "myappico.ico"
3. 多文档开发
3.1. 多 QML 文件的管理
在创建基于 Qt6 的 QML 项目时,不会包含 qml.qrc 文件,系统编译时自动创建该文件,创建的依据是 .pro 文件中的 resources.xxx 字段:
resources.files = main.qml
resources.prefix = /$${TARGET}
当使用 QtCreator 新建文件向导向工程增加自定义 QML 文件时,QML 文件可能会被错误的增加到 DISTFILES 字段,比如:
DISTFILES += \
Demo.qml
此时编译程序会报:
failed to load component ... is not a type
错误。解决方法是:手动修改 Demo.qml 到 resources.files 字段中即可。
3.2. 如何引用自定义 QML 文件
参考 1.3. 使用自定义元件
3.3. 使用另一 QML 文件中的元件或属性
子对象使用父对象中的对象时,可以直接使用对象的 ID(即便父子对象元件在不同文件中实现)。
父对象只能看到同一文件中的子对象,如果子对象元件在另一文件中实现,需要在子元件的实现中使用:
property alias <别名>:<属性名称/ID>
将元件的某属性、某子元件或某子元件的属性变为外部可见。
函数方法可直接使用,不需要使用别名。
虽然子对象可以直接访问父对象中的对象、属性和方法。但还是建议在子对象的实现文件中通过 property object 来为父对象提供绑定接口,这样可以保证子对象的封装性。比如某子对象想要使用父对象的 TextField 类型元件,则在子元件中声明:
property TextField parentTextField: null
然后在父对象中为子对象的 parentTextField 提供绑定对象即可。
3.4. 示例
假设存在 DemoQml/Demo.qml 文件,该文件内容如下:
import QtQuick
import QtQuick.Controls 6.3
Item {
property alias dia: diaDemo
property Button btn: null
Dialog {
id: diaDemo
title: qsTr("DemoDialog")
standardButtons: Dialog.Ok | Dialog.Cancel
onRejected: {
btn.text = qsTr("已显示 Dialog")
console.log("Cancel clicked")
}
}
}
注意这里使用的是 QtQuick.Controls 中的 Dialog,需要保持 main.cpp 中的 app 为 QGuiApplication 类型。
.pro 文件的相关字段如下:
resources.files = main.qml \
DemoQml/Demo.qml
resources.prefix = /$${TARGET}
main.qml 文件内容如下:
import QtQuick
import "./DemoQml"
Window {
id: window
width: 640
height: 480
visible: true
title: qsTr("Hello World")
Demo {
id: compDemo
btn: btnDemo
dia.onAccepted: {
console.log("clicked Ok button.")
}
}
Button {
id: btnDemo
anchors.fill: parent
text: qsTr("显示 Dialog")
onClicked: {
compDemo.dia.open()
}
}
}
4. QML 与 C++ 交互
QML 与 C++ 交互的主要实现方式是:
- QML 访问 C++ 中声明的类型;
- QML 访问 C++ 中创建的对象;
- QML 调用 C++ 中的方法或发射信号;
- QML 响应 C++ 中产生的信号;
- C++ 访问 QML 对象。
QML 与 C++ 之间主要通过信号槽机制来传递消息。
4.1. QML 访问 C++ 中声明的类型
4.1.1. Qt5 中的方法
QML 使用 C++ 中声明的类型可以为类、结构体或枚举等。若需要将 C++ 类导出给 QML,则需要使用 qmlRegisterType() 方法进行注册:
qmlRegisterType<Type>("package.Type", <version>, <sub-version>, "Type");
之后在 QML 中使用 C++ 类创建对象即可。若需要在 QML 中使用类中定义的枚举,格式如下:
<类名>.<枚举值>
若 QML 需要使用 C++ 中声明的枚举类型,则该枚举需要使用 Q_ENUM() 进行修饰(必须放在声明之后);若访问属性成员则需要使用 Q_PROPERTY() 进行修饰;若访问方法则该方法需要使用 Q_INVOKABLE 来修饰。C++ 中的信号不需要特殊处理,QML 可直接访问。
若对象在 C++ 上下文中创建,并需要在 QML 中使用该对象,可以使用:
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("qmlObj", cObj);
将 C++ 对象注册到 QML 上下文环境中。
4.1.2. Qt6 中的方法
在 Qt6 以后不在推荐使用 qmlRegisterType() 方法注册 C++ 类。而是使用 QML_ELEMENT 宏。该方法非常的简单,只要在创建要导出给 QML 的类时,在 QtCreator 的新文件创建向导中勾选 QML_ELEMENT 宏即可。
此时 QtCreator 会自动为该类增加 QML_ELEMENT 宏,并在 CMakeLists.txt 中增加:
qt_add_qml_module(appdicpAUV
URI demoProj
VERSION 1.0
// ...
SOURCES demoClass.h demoClass.cpp
)
之后在 QML 中即可直接使用该类创建对象:
import demoProj 1.0
Item {
//...
demoClass {
//...
}
}
如果使用 qmake 进行构建,可在 pro 文件中增加:
QT += core qml quick
CONFIG += qmltypes
HEADERS += demoClass.h
SOURCES += demoClass.cpp
# Define the QML module
QML_IMPORT_NAME = demoProj
QML_IMPORT_MAJOR_VERSION = 1
QML_ADDED_IN_MINOR_VERSION = 0
4.2. C++ 访问 QML 对象
在 QML 中为对象添加 objectName 属性后,在 C++ 中可使用:
auto root = engine.rootObjects();
auto qmlObj = root.first()->findChild<QObject*>("object name");
查找该对象,再连接信号槽等。
大部分情况下在 QML 中访问 C++ 即可实现较完善的功能,QML 传递信息给 C++ 完全可以通过信号槽机制实现。除非需要在 C++ 中动态创建对象并连接到 QML 中的信号槽,否则没必要这样设计。
4.3. 通过信号槽传递自建类型
当使用信号槽机制时,需要注意一点:如果需要通过信号槽传递自建类型数据,需要使用 qRegisterMetaType() 方法进行注册。
qRegisterMetaType<MyClass>("Myclass");
4.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 内容如下:
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 内容如下:
<RCC>
<qresource prefix="/">
<file>main.qml</file>
</qresource>
</RCC>
DemoDirect.h 内容如下
#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 内容如下
#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 内容如下
#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 内容如下
#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 内容如下
#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 内容如下
#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 内容如下:
#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 内容如下:
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)
}
}
}
5. 打包发布与部署
5.1. Windows 下 QML 程序的打包发布
Qt 提供了导出 Qt 环境变量的命令行脚本,比如“Qt 5.15.2 (MinGW 8.1.0 64-bit)”,运行该脚本可进入带有 Qt 环境变量的命令行界面,之后可通过如下命令打包程序(编译生成的可执行程序需要拷贝到<Package Output Path>):
cd <Package Output Path>
windeployqt <Exe File> [--qmldir <Project QML File Path>]
Qt 自带的打包程序会添加额外的库,如果想进一步减小体积,可手动筛减。
如果是使用 GCC 编译的 Qt 程序,还依赖一些 GCC 库,比如:
- libb2-1.dll
- libbrotlicommon.dll
- libbrotlidec.dll
- libbz2-1.dll
- libdeflate.dll
- libdouble-conversion.dll
- libfreetype-6.dll
- libgcc_s_seh-1.dll
- libglib-2.0-0.dll
- libgraphite2.dll
- libharfbuzz-0.dll
- libiconv-2.dll
- libicudt77.dll
- libicuin77.dll
- libicuuc77.dll
- libintl-8.dll
- libjasper.dll
- libjbig-0.dll
- libjpeg-8.dll
- liblcms2-2.dll
- libstdc++-6.dll
- libwinpthread-1.dll
- libzstd.dll
- zlib1.dll
等。
如果部署的程序运行不起来,或出现闪退等问题,可以先使用 Dependency Walker 对程序的依赖库进行静态分析,如果程序调试运行正常,也可以在调试程序运行时使用 Process Explorer 进行动态分析。
当所有的依赖库都正常导入,但部署的程序还是运行不起来,则有可能是某些库或插件的搜索路径存在问题,此时可通过:
gdb ./your_app.exe
(gdb) run
对 Debug 编译的程序进行运行时调试,寻找退出原因。
5.1.1. 打包发布时遇到的问题
5.1.1.1. qrc:/qt/qml/dicpAUV/Main.qml:2:1: module “QtQuick.Controls” plugin “qtquickcontrols2plugin” not found
当遇到这个问题时,已经用 Process Explorer 对依赖库进行了分析,并拷贝了正确的依赖文件到程序目录下,但程序依然闪退。最后用 GDB 对程序进行运行时调试,发现了这条错误日志。
由于这个问题出现在部署过程中,说明该库已经被正确安装,只是处于某种原因,部署的程序没有找到正确的路径。
QT 程序会在应用程序所在目录的 qml 子目录中寻找 QtQuick/Controls 插件。实际上这个目录及其中的文件已经被拷贝到应用程序目录。因此怀疑是版本 Bug 或其他原因导致程序并没有到这个路径下寻找插件。因此先通过修改环境变量的方式手动指定该目录:
export QML2_IMPORT_PATH=./qml
然后重新运行程序,发现问题解决。
但是不能每次部署时都让用户去指定这个环境变量,因此,最终需要在 main.cpp 中增加:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
// ...
QQmlApplicationEngine engine;
engine.addImportPath("qml");
// ...
return app.exec();
}
以便程序不需要手动指定环境变量便能正确的寻找 QtQuick.Controls 插件。
5.1.1.2. 基于 MSYS2 环境开发程序的部署
需要将 MSYS2 的相关库拷贝的应用程序目录,否则可能由于目标系统没有 MSYS2 环境而无法执行程序。这些库文件在:
/MSYS2_Install_Path/usr/bin
目录下,将其中的 *.dll 文件全部拷贝到应用程序目录下即可。
5.2. 运行环境设置
Qt 程序启动时会检查系统环境变量,传入参数等。在部署 Qt 程序时,需要正确设置环境变量以及启动参数。其中环境变量设置在 Linux 中可修改 /etc/environment 或者 ~/.bashrc,甚至是 systemd 服务文件(.service 文件)中配置。
Windows 下可在“系统->高级系统设置->环境变量”中进行设置。
也可以在 Qt 程序源代码最开始的地方使用 qputenv() 函数进行设置,比如:
int main(int argc, char *argv[])
{
qputenv("QT_IM_MODULE", QByteArray("qtvirtualkeyboard"));
// ...
QGuiApplication app(argc, argv);
// ...
}
5.2.1. GPU 环境设置
5.2.1.1. 切换 GPU 后端
Qt5 中可以使用 QT_QUICK_BACKEND/QMLSCENE_DEVICE 等宏来切换使用哪种 GPU 后端和 API。
# Qt5
export QT_QUICK_BACKEND=software
# 或 legacy QMLSCENE_DEVICE
export QMLSCENE_DEVICE=softwarecontext
QT_QUICK_BACKEND 可选值为:
- rhi
- software
- openvg
也可在 Qt 程序源码中使用 void QQuickWindow::setSceneGraphBackend(QSGRendererInterface::GraphicsApi api) 函数实现:
#include <QQuickWindow>
int main(int argc, char *argv[])
{
QQuickWindow::setSceneGraphBackend(QSGRendererInterface::Software);
// ...
QGuiApplication app(argc, argv);
QQuickView view;
// ...
}
QSGRendererInterface::GraphicsApi 的值可以为:
- QSGRendererInterface::Unknown : An unknown graphics API is in use
- QSGRendererInterface::Software : The Qt Quick 2D Renderer is in use
- QSGRendererInterface::OpenGL : OpenGL ES 2.0 or higher
- QSGRendererInterface::Direct3D12 : Direct3D 12
- QSGRendererInterface::OpenVG : OpenVG via EGL
- QSGRendererInterface::OpenGLRhi : OpenGL ES 2.0 or higher via a graphics abstraction layer. This value was introduced in Qt 5.14.
- QSGRendererInterface::Direct3D11Rhi : Direct3D 11 via a graphics abstraction layer. This value was introduced in Qt 5.14.
- QSGRendererInterface::VulkanRhi : Vulkan 1.0 via a graphics abstraction layer. This value was introduced in Qt 5.14.
- QSGRendererInterface::MetalRhi : Metal via a graphics abstraction layer. This value was introduced in Qt 5.14.
- QSGRendererInterface::NullRhi : Null (no output) via a graphics abstraction layer. This value was introduced in Qt 5.14.
Qt6 中使用 QSG_RHI_BACKEND 设置 GPU 后端和 API。
export QSG_RHI_BACKEND="opengl"
也可在 Qt 程序源码中使用 QQuickWindow::setGraphicsApi(QSGRendererInterface::GraphicsApi api) 函数实现:
#include <QQuickWindow>
int main(int argc, char *argv[])
{
/**
* Requests the specified graphics api.
When the built-in, default graphics adaptation is used, api specifies which graphics API (OpenGL, Vulkan, Metal, or Direct3D) the scene graph should use to render. In addition, the software backend is built-in as well, and can be requested by setting api to QSGRendererInterface::Software.
Unlike setSceneGraphBackend(), which can only be used to request a given backend (shipped either built-in or installed as dynamically loaded plugins), this function works with the higher level concept of graphics APIs. It covers the backends that ship with Qt Quick, and thus have corresponding values in the QSGRendererInterface::GraphicsApi enum.
When this function is not called at all, and the equivalent environment variable QSG_RHI_BACKEND is not set either, the scene graph will choose the graphics API to use based on the platform.
To query what graphics API the scene graph is using to render, QSGRendererInterface::graphicsApi() after the scene graph has initialized, which typically happens either when the window becomes visible for the first time, or when QQuickRenderControl::initialize() is called.
To switch back to the default behavior, where the scene graph chooses a graphics API based on the platform and other conditions, set api to QSGRendererInterface::Unknown.
*/
QQuickWindow::setGraphicsApi(QSGRendererInterface::Software); // Qt6.0 开始支持
// ...
QGuiApplication app(argc, argv);
QQuickView view;
// ...
}
注意:QQuickWidget is functional only when the scenegraph is rendering with OpenGL. It will not be functional when using another graphics API, such as Vulkan or Metal. Applications relying on QQuickWidget should force the usage of OpenGL by calling QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi) in their main() function.
5.2.1.2. 在不支持 OpenGL 的机器上运行 QML 程序
Qt QML 程序默认使用 OpenGL 进行渲染,但某些系统由于没有 GPU 或者其他原因,导致不支持 OpenGL,或者 OpenGL 版本无法满足要求,此时需指定 Qt 程序使用的 GPU 后端为 Software。
注意:Qt 的 VideoOutput 不能使用软件渲染即不能使用 Qt Quick Software Adaptation。
5.2.1.3. 切换 OpenGL 模式
Qt 可以选择使用 Desktop 版本的 OpenGL 还是其他,设置方式同样是通过设置环境变量来实现:
QT_OPENGL=desktop
QT_OPENGL 可选参数为:
- desktop:使用 Desktop 版本的 OpenGL
- es2:嵌入式版本的 OpenGL(OpenGL ES)
- angle:使用 angle directX接口
- software:Windows 下依赖 opengl32sw.dll
6. 常用模块与组件
6.1. Rectangle
Rectangle 是一个矩形组件,用于绘制矩形区域。
Rectangle {
width: 640
height: 480
color: "purple"
}
Rectangle 也可用于绘制圆形,只需要将其 radius 属性设置为边长的一半即可。
Rectangle {
width: 640
height: 480
radius: width/2
color: "purple"
}
6.2. QtQuick.Loader
Loader 是 Qt Quick/QML 中用来 动态加载组件 的一个非常实用的类型。它的作用是在运行时加载和卸载 QML 对象,从而节省内存和提高性能,尤其适用于需要按需显示的内容。
Loader {
id: myLoader
source: "MyComponent.qml" // 指定要加载的 QML 文件路径
// sourceComponent: MyComponent // 也可以直接指定 QML 组件
}
- active:控制是否加载组件,设为 false 可卸载已加载内容。
- item:当前加载的实例(只读)
- status:加载状态:
- Loader.Null
- Loader.Ready
- Loader.Loading
- Loader.Error
- onLoaded:加载完成时触发。
- onStatusChanged:状态改变时触发。
6.3. QtQuick.Controls.Button
Button {
id: butDemo
width: 200
height: 100
text: qsTr("Demo")
// 获得焦点时高亮.
highlighted: activeFocus
onClicked: butDemo.text = qsTr("DemoButton")
// 当 Button 获得焦点时可响应回车按钮.
Keys.onReturnPressed: butDemo.text = qsTr("Button")
}
6.4. Dialog 对象
QML 中有三大类 Dialog 对象,这里主要介绍 QtQuick.Controls 中的 Dialog 和 QtQuick.Dialogs。
6.4.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 之外的区域会自动关闭,如果需要阻塞父窗口并且不自动关闭,则需要设定如下属性:
Dialog {
modal : true
closePolicy: Popup.NoAutoClose
}
上面的对话框没有提供任何按钮,可以通过 standardButtons 设定标准按钮:
Dialog {
title: qsTr("Demo")
anchors.centerIn: parent
standardButtons: Dialog.Ok | Dialog.Cancel
modal : true
closePolicy: Popup.NoAutoClose
}
除 Ok Button 和 Cancel Button 外,还有许多系统预定义的 Standard Button 可自行翻阅 QML 帮助获得相关帮助信息。
6.4.2. QtQuick.Dialogs
在使用 QtQuick.Dialogs 的 Dialog 对象时,如果使用 QGuiApplication 来执行则会导致无法加载主题风格,并且对话框无法正确显示图标。如果最初使用 QGuiApplication 创建了 app,则需要进行如下修改:
/**
* .pro 文件中增加 widgets
*/
QT += widgets
/**
* main.cpp 中替换 QGuiApplication 为 QApplication
*/
// #include <QGuiApplication>
#include <QApplication>
int main(int argc, char *argv[])
{
// QGuiApplication app(argc, argv);
QApplication app(argc, argv);
// ...
return app.exec();
}
Dialog 对象默认不显示,当调用 Dialog 的 open() 方法后弹出窗口并阻塞父窗体的执行。
6.4.2.1. FileDialog
FileDialog 为标准文件对话框。
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)
}
}
- fileMode:文件模式
- FileDialog.OpenFile(默认):打开已存在的单个文件
- FileDialog.OpenFiles:打开已存在的多个文件
- FileDialog.SaveFile:用于选择任意文件,文件不是必须存在。
- nameFilters:文件过滤器,用于限制可选择的文件类型,比如:nameFilters: [“Json Files (*.json)"]。
- selectedFile:返回通过文件对话框选择的单个文件,url 类型,可对应到 C++ 的 QUrl 类,可通过 QUrl::toLocalFile() 方法获得本地文件路径。
- selectedFiles:返回通过文件对话框选择的多个文件,是一个 url 列表。
- selectFolder:允许选择文件夹,Qt6 中该属性已经被弃用。
6.4.2.2. FolderDialog
FileDialog 为文件夹对话框。
import QtQuick
import QtQuick.Controls
import QtQuick.Dialogs
Window {
Button {
text: qsTr("选择文件夹")
onClicked: {
dialogFolder.open()
}
}
FolderDialog {
id: dialogFolder
title: qsTr("选择文件夹")
onAccepted: {
switch (title) {
case qsTr("选择文件夹"):
console.log(selectedFolder)
break;
default:
break;
}
}
}
}
- options:
- FolderDialog.DontResolveSymlinks:不解析 symlinks,默认解析。
- FolderDialog.ReadOnly:只读,意味着不允许创建文件夹。
- FolderDialog.DontUseNativeDialog:强制使用 non-native 的快速实现。
- selectedFolder:也是一个 url 类型,C++ 后端处理方式类似于 FileDialog.selectedFile。
6.4.2.3. MessageDialog
MessageDialog 为标准消息对话框。
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
import QtQuick.Dialogs 1.3
Window {
width: 640
height: 480
visible: true
function showInfoDialog(title, info, detail) {
infoDialog.title = title
infoDialog.informativeText = info
infoDialog.detailedText = detail
infoDialog.open()
}
MessageDialog {
id: infoDialog
standardButtons: MessageDialog.Ok
text: ""
}
Button {
text: qsTr("显示消息")
onClicked: {
showInfoDialog(qsTr("标题"), qsTr("信息"), qsTr("细节"))
}
}
}
QtQuick.Dialogs.MessageDialog 的优点是开发速度快,缺点是可定制项目太少,自由度比较小,且在 Qt6.9 下其只在以下平台实现有 Native 实现:
- Android
- iOS
- macOS
如果没有本地实现,则退化为 Qt Quick implementation。
这意味着其某些属性很可能设置无效,比如在 Windows 下 title 属性失效,变为 App 的名称。因此需求不高的场景可以通过在 main.cpp 中自定义应用显示名称的方式变通使用:
QGuiApplication::setApplicationDisplayName("App Display Name");
另外 QtQuick.Dialogs.MessageDialog 不支持图标。
如果有更高的定制化需求,建议使用 QtQuick.Dialogs.Dialog 来实现自定义对话框。
6.5. ComboBox
ComboBox 为标准组合框
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"]
}
}
6.6. Grid
布局组件,可以布局多个行和列,每个子成员必须是相同类型,一般使用 Item 来组织多个不同类型。每个子成员必须设置高度和宽度,否则不被显示。
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")
}
}
}
6.7. ScrollView
ScrollView 会为其所容纳的对象创建滚动条。
6.8. QtQuick.ListView
TODO: header, headerPositioning
TODO: 如果通过设置 header 和 headerPositioning 参数来实现固定 header,且同时伴有 ScrollBar 的情况下,滚动条依然会覆盖到 header 上,效果不理想。此时最好在 ListView 之外使用 Row 嵌套 Label 来实现类似效果。
6.9. GridView
GridView 可以以网格的形式显示模型内容。可以使用 ListModel 或 XmlListModel 作为模型。
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
}
}
}
}
}
6.10. BusyIndicator
用于指示工作状态,设置 BusyIndicator 的 running 属性为 true 将默认显示一个旋转的圆圈;设置 running 属性为 false 则 BusyIndicator 将不显示。
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 进行自定义。
6.11. VirtualKeyboard
一些涉及触屏的应用会涉及到虚拟键盘/软键盘的应用,不同系统平台上往往会提供不同的软键盘工具,但相比之下,Qt 内嵌的 VirtualKeyboard 更加易用,并具有很好的跨平台能力,中文(拼音)、英文以及其他主要语言的支持能力也比较好。
若使用 VirtualKeyboard,只需要在创建“Qt Quick Application”时选中“Use Qt Virtual Keyboard”(旧版本 QtCreator 没有该选项)即可自动向项目添加 VirtualKeyboard 功能。
对比源码不难发现,启用 VirtualKeyboard 后主要在 main.cpp 中增加了:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
// ...
qputenv("QT_IM_MODULE", QByteArray("qtvirtualkeyboard"));
// ...
return app.exec();
}
在 main.qml 中增加了:
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 没有使能中文输入法则最终只能使用英文输入:
import QtQuick.VirtualKeyboard.Settings 2.4
Window {
Component.onCompleted: {
VirtualKeyboardSettings.activeLocales = ["zh_CN", "en_US"]
}
}
如果是使用 Qt 提供的安装包来安装 VirtualKeyboard 模块则默认开启了中文支持,部分系统如 Ubuntu 中使用 apt 进行安装的可能不支持中文,可改用官方安装包来安装 Qt 或使用源码自行编译,注意在编译 VirtualKeyboard 的 qmake 阶段需要增加:
CONFIG+="lang-en_GB lang-zh_CN"
更多关于 VirtualKeyboard 的应用可以参考官方自带示例。
6.12. Multimedia
需要引入:
import QtMultimedia x.xx
主要包含音视频的输入、处理和输出,如:Video、Audio、Camera、VideoOutput、MediaPlayer、SoundEffect 等。
6.12.1. VideoOutput 和 MediaPlayer
Qt 只是提供了便捷易用的接口,实际的编解码功能由后端实现。 Qt5 Linux 下视频后端为 GStreamer,Win 下为 DirectShow,建议使用 LAVFilters(ffmpeg based DirectShow Splitter and Decoders)。Qt6 在 Linux 下好像增加了对 ffmpeg 的支持。
在使用 VideoOutput 和 MediaPlayer 时,有些时候程序编写正确,跑起来也没提示什么错误,但就是不播视频,显示黑屏且没有声音,此时很可能是后端没有安装或安装错误导致。
VideoOutput 和 MediaPlayer 的使用非常简单,MediaPlayer 作为播放器 VideoOutput 作为输出显示,二者配合工作:
MediaPlayer {
id: player
source: "file:./SampleVideo.mp4"
autoPlay: true
}
VideoOutput {
id: videoOutput
source: player
anchors.fill: parent
}
值得注意的是,MediaPlayer 的 source 路径格式,必须是以“file:”开头的本地文件格式(不支持 qrc 资源文件路径)。示例里使用了相对路径,也可以使用绝对路径(如 Win 下:file://C:\\path\\to\\SampleVideo.mp4,或 Linux 下: file:///path/to/SampleVideo.mp4)。在使用相对路径时需注意这个路径是相对 Qt 的调试目录而言。
目前不清楚能否使用网络 URL 格式路径。
6.13. ChartView
ChartView 组件可用于实现图表显示,使用时需增加 Charts 模块。
qmake:
QT += charts
cmake(Qt6):
find_package(Qt6 REQUIRED COMPONENTS Charts)
target_link_libraries(mytarget PRIVATE Qt6::Charts)
一个支持鼠标缩放和移动的简单示例如下:
import QtQuick
import QtCharts
ChartView {
title: "示例参考-值"
antialiasing: true
MouseArea {
anchors.fill: parent
property point clickPos: "0,0"
onDoubleClicked: {
parent.zoomReset()
}
onWheel: {
if (0 < wheel.angleDelta.y)
{
parent.zoomIn()
}
else if (0 > wheel.angleDelta.y)
{
parent.zoomOut()
}
}
onPressed: {
clickPos = Qt.point(mouse.x,mouse.y)
}
onPositionChanged: {
if (containsMouse)
{
var delta = Qt.point(mouse.x-clickPos.x, mouse.y-clickPos.y)
if(0 != delta.x)
{
parent.scrollLeft(delta.x)
}
if(0 != delta.y)
{
parent.scrollUp(delta.y)
}
clickPos = Qt.point(mouse.x,mouse.y)
}
}
}
ValuesAxis {
id: xAxis
min: 0
max: 3.5
tickType: ValueAxis.TicksFixed
tickCount: 10
labelFormat: "%.1f"
}
ValuesAxis {
id: yAxis
min: 0
max: 25
tickType: ValueAxis.TicksFixed
tickCount: 10
labelFormat: "%.1f"
}
AreaSeries {
axisX: xAxis
axisY: yAxis
name: qsTr("示例1")
axisX: valueAxis
upperSeries: LineSeries {
XYPoint { x: 0; y: 3 }
XYPoint { x: 1; y: 4 }
XYPoint { x: 1.5; y: 1.5 }
XYPoint { x: 2; y: 3 }
XYPoint { x: 3; y: 7 }
}
}
LineSeries {
axisX: xAxis
axisY: yAxis
name: qsTr("示例2")
XYPoint { x: 0; y: 10 }
XYPoint { x: 1; y: 15 }
XYPoint { x: 1.5; y: 7.5 }
XYPoint { x: 2; y: 20 }
XYPoint { x: 3; y: 18 }
}
}
- ChartView:提供显示图表的窗口。
- Axis:用于设置坐标轴和网格。
- Series:用于设置图表类型和数据。
- LineSeries:用于绘制折线图。
- ScatterSeries:用于绘制散点图。
- SplineSeries:用于绘制平滑曲线。
6.14. MenuBar/Menu/MenuItem
MenuBar/Menu/MenuItem 用于构建菜单系统。ApplicationWindow 包含菜单区域,可通过其属性 menuBar 来修改或定制。而 Window 并不包含菜单,需要自行构建。
使用 MenuBar/Menu/MenuItem 需要导入:
import QtQuick.Controls
使用示例如下:
import QtQuick
import QtQuick.Controls
// ApplicationWindow {
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello World")
// menuBar: MenuBar { // 若使用 ApplicationWindow 则可以使用该属性
MenuBar {
id: menu
Menu {
title: qsTr("示例菜单1")
MenuItem {
text: qsTr("功能1")
onTriggered: {
console.log("触发功能1")
}
}
MenuItem {
text: qsTr("功能2")
onTriggered: {
console.log("触发功能2")
}
}
}
Menu {
title: qsTr("示例菜单2")
MenuItem {
text: qsTr("功能3")
onTriggered: {
console.log("触发功能3")
}
}
MenuItem {
text: qsTr("功能2")
onTriggered: {
console.log("触发功能4")
}
}
}
}
}
6.15. StackLayout/TabBar/TabButton
StackLayout 用于实现多页面切换,常与 TabBar/TabButton 等导航控件组合使用。
使用 StackLayout 需要导入:
import QtQuick.Layouts
TabBar/TabButton 需要导入:
import QtQuick.Controls
使用示例如下:
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello World")
TabBar {
id: tabDemo
height: 32
anchors.left: parent.left
anchors.right: parent.right
anchors.top: menuMain.bottom
TabButton {
width: 128
text: qsTr("Tab1")
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
}
TabButton {
width: 128
text: qsTr("Tab2")
anchors.top: parent.top
anchors.bottom: parent.bottom
}
}
StackLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: tabDemo.bottom
anchors.bottom: parent.bottom
currentIndex: tabDemo.currentIndex
Item {
width: parent.width
height: parent.height
Text {
text: "Tab1"
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
Item {
width: parent.width
height: parent.height
Text {
text: "Tab2"
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
6.16. SplitView
用于创建可以左右或者上下分割的窗口。
使用 StackLayout 需要导入:
import QtQuick.Controls
使用示例如下:
import QtQuick
import QtQuick.Controls
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello World")
SplitView {
anchors.fill: parent
orientation: Qt.Horizontal
Item {
SplitView.preferredWidth: parent.width/2
SplitView.minimumWidth: 256
Text {
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: "Spli1"
}
}
Item {
SplitView.minimumWidth: 128
Text {
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: "Spli2"
}
}
}
}
Item 下的 preferredWidth 和 minimumWidth 是 SplitView 的附加属性,分别用于设置 Item 宽度的首选值和最小值。
SplitView 的 handle 属性用于定制分割条的样式。
6.17. TableView
TableView 是 QML 中用于显示表格数据的组件。使用时需要导入:
import QtQuick
使用示例如下:
import QtQuick
import QtQuick.Controls
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello World")
TableView {
id: tableView
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
anchors.topMargin: 8
anchors.bottomMargin: 8
clip: true
ScrollBar.vertical: ScrollBar {
id: scrollv
policy: ScrollBar.AlwaysOn
}
columnWidthProvider: function (column) {
return width - scrollv.width
}
onWidthChanged: {
forceLayout()
}
delegate: Rectangle
{
implicitWidth: tableView.columnWidthProvider(column)
implicitHeight: 30
border.width: 1
border.color: "#ADB3BC"
color: "white"
TextField {
width: parent.width
height: parent.height
verticalAlignment: Text.AlignVCenter
wrapMode: Text.NoWrap
readOnly: true
hoverEnabled: true
text: fileName
ToolTip.visible: hovered
ToolTip.text: text
}
}
model: ListModel {
id: modFile
ListElement {
fileName: "First"
}
ListElement {
fileName: "Second"
}
}
}
}
6.18. Qt Data Visualization
Qt Data Visualization 是 Qt 提供的一个用于数据可视化的模块,它允许开发者轻松创建专业级的 3D 图表和数据可视化效果。
使用前需在 CMakeLists.txt 中添加:
find_package(Qt6 REQUIRED COMPONENTS DataVisualization)
target_link_libraries(appdicpAUV PRIVATE Qt6::DataVisualization)
源码中导入:
import QtDataVisualization
并在 main.cpp 中增加
int main(int argc, char *argv[])
{
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
QApplication app(argc, argv);
// ...
return app.exec();
}
来设置 GPU 渲染 API。
或通过设置环境变量的方法实现:
export QSG_RHI_BACKEND="opengl"
类似于 ChartView,Qt Data Visualization 中的每种图表都有对应的 Series 组件用于存储数据。
可以使用 Axis 组件来定制坐标轴,主要有:
- AbstractAxis3D
- CategoryAxis3D
- ValueAxis3D
6.18.1. Scatter3D
Scatter3D 用于绘制三维散点图,其对应的 Series 组件为 Scatter3DSeries。一个 Scatter3D 可以包含多个 Scatter3DSeries。一个简单的示例如下:
import QtQuick
import QtDataVisualization
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello World")
Scatter3D {
id: demoScatter3D
anchors.fill: parent
shadowQuality: AbstractGraph3D.ShadowQualityNone
aspectRatio: 1.0
horizontalAspectRatio: 1.0
msaaSamples: 0
optimizationHints: AbstractGraph3D.OptimizationStatic
axisX: ValueAxis3D {
id: xAxis
min: 0
max: 25
segmentCount: 10
labelFormat: "%.1f"
title: qsTr("X")
titleVisible: true
autoAdjustRange: false
}
axisY: ValueAxis3D {
id: yAxis
min: 0
max: 25
segmentCount: 10
labelFormat: "%.1f"
title: qsTr("Y")
titleVisible: true
autoAdjustRange: false
}
axisZ: ValueAxis3D {
id: zAxis
min: 0
max: 25
segmentCount: 10
labelFormat: "%.1f"
title: qsTr("Z")
titleVisible: true
autoAdjustRange: false
}
Scatter3DSeries {
id: demoSeries
name: "Demo Series"
baseColor: "blue" // 设置点颜色
itemSize: 0.05 // 设置点大小
itemLabelFormat: "Demo Series (@xLabel D, @yLabel C, @zLabel T)"
itemLabelVisible: true
dataProxy: ItemModelScatterDataProxy {
itemModel: demoModel // 绑定到数据模型
xPosRole: "x" // 指定x坐标的角色名
yPosRole: "y" // 指定y坐标的角色名
zPosRole: "z" // 指定z坐标的角色名
rotationRole: "r" // 可选:旋转角度角色
}
}
}
ListModel {
id: demoModel
ListElement { x: 1.0; y: 2.0; z: 3.0; rot: 00; }
ListElement { x: 2.0; y: 3.0; z: 1.0; rot: 45; }
ListElement { x: 3.0; y: 1.0; z: 2.0; rot: 90; }
}
}
其中 ItemModelScatterDataProxy 是 ScatterDataProxy 的子类,相对于 ScatterDataProxy 扩展了绑定 ItemModel 模型数据的能力。
如果直接为 Scatter3DSeries 添加大量数据,将导致前端事件响应延迟、UI 操作卡顿,UI 刷新和事件处理也会降低数据读取速度。此类场景需要在后台添加数据,以提高性能、优化 UI 响应。比如创建一个 QThread,并在该 Thread 中向 Series 添加数据。
这个过程涉及到 QML 和 C++ 的混合编程,首先看一下数据结构:
- Scatter3DSeries/QScatter3DSeries
- ScatterDataProxy/QScatterDataProxy
- QScatterDataItem
- QScatterDataArray
上面每组是一个 QML-C++ 类型对。也就是说在 QML 中的 ItemModelScatterDataProxy 对象可以在 C++ 中作为 QItemModelScatterDataProxy 对象直接使用。
前述示例显示,要向 Scatter3DSeries 添加数据,就要通过 ScatterDataProxy 类型的 dataProxy 属性来实现。
因而可以将 Scatter3DSeries 的 dataProxy 传递给 C++ 后台程序,获得与 ScatterDataProxy 相对应的 QScatterDataProxy 对象指针。之后再借助 QScatterDataProxy 的:
int QScatterDataProxy::addItem(const QScatterDataItem &item)
方法为其添加数据。
QScatterDataItem 是一个数据项,其包含了 x、y、z 三个坐标。可以通过 setX()、setY()、setZ() 方法设置数据坐标,或直接通过 QVector3D 来构建。
这种方法简单且直接,但在每次调用 addItem() 时都会重新分配内存,甚至刷新 UI,导致性能损失和前端响应迟缓。如果想提供比 QScatterDataProxy::addItem() 更高的性能则要用到 QScatterDataArray 类型,其相当于 QList<QScatterDataItem>。
QScatterDataArray 通过 QScatterDataProxy 提供的:
void QScatterDataProxy::resetArray(QScatterDataArray *newArray);
方法将二者绑定。绑定后,在后端使用 QScatterDataArray 的 append() 方法添加 QScatterDataItem 数据项就不会导致前端 UI 刷新。
当全部数据添加完成后,调用:
void QScatterDataProxy::arrayReset()
方法通知前端刷新数据即可。这样前端 UI 响应和后端数据处理互不干扰,从在提高性能的同时改善前端响应能力。
QScatterDataArray 还提供了:
// 没错,确实是 QList::reserve() 而不是 QScatterDataArray::reserve()。原因在上面。
void QList::reserve(qsizetype size)
方法,用于预先分配内存,避免频繁的内存分配,从而避免频繁重新分配内存带来的额外系统开销。
下面用简单的示例对上述高性能 Scatter3D 散点数据显示方法进行演示,该示例包含了:
- CMakeLists.txt
- main.cpp
- BackEnd.h
- BackData.cpp
- Main.qml
四个文件,一些关键的技术说明包含在注释信息中。
CMakeLists.txt 文件内容如下:
cmake_minimum_required(VERSION 3.16)
project(Scatter3DDemo VERSION 0.1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Quick DataVisualization)
qt_standard_project_setup(REQUIRES 6.8)
qt_add_executable(appScatter3DDemo
main.cpp
)
qt_add_qml_module(appScatter3DDemo
URI Scatter3DDemo
VERSION 1.0
QML_FILES
Main.qml
SOURCES BackData.h BackData.cpp
SOURCES BackEnd.h BackEnd.cpp
)
# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
# If you are developing for iOS or macOS you should consider setting an
# explicit, fixed bundle identifier manually though.
set_target_properties(appScatter3DDemo PROPERTIES
# MACOSX_BUNDLE_GUI_IDENTIFIER com.example.appScatter3DDemo
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
MACOSX_BUNDLE TRUE
WIN32_EXECUTABLE TRUE
)
target_link_libraries(appScatter3DDemo
PRIVATE Qt6::Quick
Qt6::DataVisualization
)
include(GNUInstallDirs)
install(TARGETS appScatter3DDemo
BUNDLE DESTINATION .
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
main.cpp 文件内容如下:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickWindow>
int main(int argc, char *argv[])
{
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
QObject::connect(
&engine,
&QQmlApplicationEngine::objectCreationFailed,
&app,
[]() { QCoreApplication::exit(-1); },
Qt::QueuedConnection);
engine.loadFromModule("Scatter3DDemo", "Main");
return app.exec();
}
BackEnd.h 文件内容如下,注意 BackEnd 类型通过 QML_ELEMENT 注册给了 QML 环境:
#ifndef BACKEND_H
#define BACKEND_H
#include <QQmlEngine>
#include <QThread>
#include <QScatterDataProxy>
#include <QScatterDataArray>
class BackEnd : public QThread
{
Q_OBJECT
QML_ELEMENT
public:
explicit BackEnd(QObject *parent = nullptr);
virtual ~BackEnd() {
this->forceExit = true;
this->wait();
/**
* 如果 arry 没有通过 proxy->resetArray 绑定到前端,需要在 BackEnd 析构函数中释放。
* 如果 arry 已经通过 proxy->resetArray 绑定到了前端则由前端 QScatterDataProxy 释放。
*/
if (!this->binded)
delete this->arry;
}
/**
* @brief clear 必须在前端调用 clear() 方法清除数据,以便在数据清除后正确更新前端 UI。
* 如果在后端清除数据,则要等前端正确刷新后才能添加新数据,否则立刻添加数据
* 会导致延迟的前端 UI 刷新动作进行时,QScatterDataArray 中已经有了新添
* 加的数据,进而导致 Scatter3D 图会显示一些“混乱”的“脏”数据点。
*/
Q_INVOKABLE void clear(QScatterDataProxy* proxy)
{
// removeItems() 操作会 Clear 绑定到 QScatterDataProxy 上的 QScatterDataArray
proxy->removeItems(0, proxy->itemCount());
}
Q_INVOKABLE void process(QScatterDataProxy* proxy)
{
this->forceExit = false;
this->proxy = proxy;
if (proxy->array()!=this->arry)
{
/**
* proxy->resetArray() 的执行必须在前端线程中。
* 因为通过 resetArray() 添加到 Scatter3D 中的 QScatterDataArray 将
* 被前端 UI 线程用于显示,如果在后端线程进行操作将导致错误。
*/
proxy->resetArray(this->arry);
}
this->binded = true;
if (!this->isRunning())
this->start();
}
signals:
void processDone(double xmin, double xmax, double ymin, double ymax, double zmin, double zmax);
protected:
void run();
QScatterDataArray* arry;
QScatterDataProxy* proxy;
bool forceExit, binded = false;
};
#endif // BACKEND_H
BackData.cpp 文件内容如下:
#include "BackEnd.h"
#include <QScatterDataItem>
#include <QRandomGenerator>
BackEnd::BackEnd(QObject *parent)
: QThread{parent}
{
this->arry = new QScatterDataArray();
// 为大数据提前分配好空间。
this->arry->reserve(500000);
}
void BackEnd::run()
{
QScatterDataItem data;
for (int i=0; i<100000; i++)
{
float x = 100.0*QRandomGenerator::global()->generateDouble();
float y = 100.0*QRandomGenerator::global()->generateDouble();
float z = 100.0*QRandomGenerator::global()->generateDouble();
if (forceExit)
return;
data = QScatterDataItem(QVector3D(x, y, z));
this->arry->append(data);
// 以下延时用于释放 CPU 资源,否则会导致卡顿。
if (0==(i%100))
QThread::msleep(5);
}
// 先缩放坐标轴,再刷新数据,避免刷新显示数据后缩放坐标轴导致二次绘图。
emit processDone(0.0, 100.0, 0.0, 100.0, 0.0, 100.0);
/**
* 前端响应 processDone 信号后会更新坐标轴范围,进而起到类似 arrayReset() 的
* 效果。但是当新坐标轴坐标范围与原有范围一致时不会被更新,因此 arrayReset() 在
* 这里是必要的。
*/
emit this->proxy->arrayReset();
}
Main.qml 文件内容如下,为了使用 BackEnd,需要 import Scatter3DDemo,Scatter3DDemo 是本项目的名称,在 CMakeLists.txt 中。
import QtQuick
import QtQuick.Controls
import QtDataVisualization
import Scatter3DDemo
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello World")
MenuBar {
id: demoMenu
height: 32
anchors.left: parent.left
anchors.right: parent.right
Menu {
title: qsTr("操作")
MenuItem {
text: qsTr("开始")
onTriggered: {
back.clear(demoSeries.dataProxy);
back.process(demoSeries.dataProxy)
}
}
}
}
BackEnd {
id: back
}
Connections {
target: back
function onProcessDone(xmin, xmax, ymin, ymax, zmin, zmax) {
xAxis.min = xmin
xAxis.max = xmax
yAxis.min = ymin
yAxis.max = ymax
zAxis.min = zmin
zAxis.max = zmax
}
}
Scatter3D {
id: demoScatter3D
anchors.left: parent.left
anchors.right: parent.right
anchors.top: demoMenu.bottom
anchors.bottom: parent.bottom
shadowQuality: AbstractGraph3D.ShadowQualityNone
aspectRatio: 1.0
horizontalAspectRatio: 1.0
msaaSamples: 0
optimizationHints: AbstractGraph3D.OptimizationStatic
axisX: ValueAxis3D {
id: xAxis
min: 0
max: 25
segmentCount: 10
labelFormat: "%.1f"
title: qsTr("X")
titleVisible: true
autoAdjustRange: false
}
axisY: ValueAxis3D {
id: yAxis
min: 0
max: 25
segmentCount: 10
labelFormat: "%.1f"
title: qsTr("Y")
titleVisible: true
autoAdjustRange: false
}
axisZ: ValueAxis3D {
id: zAxis
min: 0
max: 25
segmentCount: 10
labelFormat: "%.1f"
title: qsTr("Z")
titleVisible: true
autoAdjustRange: false
}
Scatter3DSeries {
id: demoSeries
name: "Demo Series"
baseColor: "blue"
itemSize: 0.05
itemLabelFormat: "Demo Series (@xLabel D, @yLabel C, @zLabel T)"
itemLabelVisible: true
}
}
}
6.18.2. Known Issues
- QtDataVisualization QML item crashes app when using Qt6: As OpenGL is not necessarily the default rendering backend anymore in Qt 6.x (it is Metal on macOS and Direct3D on Windows, for example), it is necessary to define the rendering backend explicitly either on your environment variables, or in your application main. It can be defined by adding qputenv(“QSG_RHI_BACKEND”, “opengl”); in the beginning of your main function.
- Some platforms like Android and WinRT cannot handle multiple native windows properly, so only the Qt Quick graphs are available in practice for those platforms.
- Surfaces with non-straight rows and columns do not always render properly.
- Q3DLight class (and Light3D QML item) are currently not usable for anything.
- Changing most of Q3DScene properties affecting subviewports currently has no effect.
- Widget based examples layout incorrectly in iOS.
- Reparenting a graph to an item in another QQuickWindow is not supported.
- Android builds of QML applications importing QtDataVisualization also require “QT += datavisualization” in the pro file. This is because Qt Data Visualization QML plugin has a dependency to Qt Data Visualization C++ library, which Qt Creator doesn’t automatically add to the deployment package.
7. Linux 下为 Qt Quick 应用程序隐藏鼠标指针
QML 中可以使用 MouseArea 来隐藏鼠标指针,但是在程序刚启动时鼠标指针依然可见,只有鼠标动过或者点击过才会消失。因此需要 unclutter 程序来辅助,unclutter 可以在系统空闲时自动隐藏鼠标指针。首先安装 unclutter:
# Ubuntu
sudo apt install unclutter
并修改 /etc/default/unclutter:
# /etc/default/unclutter - configuration file for unclutter
# Set this option to 'true' if you want to start unclutter
# automagically after X has been started for a user.
# Otherwise, set it to 'false'.
START_UNCLUTTER="true"
# Options passed to unclutter, see 'man unclutter' for details.
EXTRA_OPTS="-idle 0 -root"
确保 unclutter 会在系统启动时自动运行,并指定 -idle(空闲时间) 为 0——表示鼠标指针立刻隐藏。
在 Qt 项目的 QML 文件中添加:
MouseArea {
z: 99
anchors.fill: parent
enabled: false
cursorShape: Qt.BlankCursor
}
8. 外部参考资料
- 深入了解JS中的整数
- QML 中的信号与槽
- QML 信号与响应方法的总结
- QML 控件类型:ScrollBar、ScrollIndicator
- QML 类型:GridView
- Qt Quick 常用元素:ComboBox(下拉列表) 与 ProgressBar(进度条)
- 【QML Model-View】ListView-增删改查(二)
- 关于 Q_ENUMS 和 Q_ENUM 的区别和用法
- C++ 共享枚举类型给 QML
- QML Connections: Implicitly defined onFoo properties in Connections are deprecated.
- QML 调用 C++ 方法
- Qml 与 C++ 交互3:Qml 的信号与 C++ 的槽函数连接
- Qt-虚拟键盘
- Qt5软键盘实现中文拼音输入法
- Qt6中加载自定义qml遇到的问题
- QML控件类型:Dialog(Qt Quick Controls 模块)
- Customizing Qt Quick Controls
- 【QT Quick】基础语法:
default
属性 - Qt Quick/QML 中 动态加载组件:
Loader
- QML 使用 ChartView 绘制区域图
- Qt | QChart+QChartView+QLineSeries(折线图)+QBarSeries(柱状图)实战
- QtDataVisualization Surface3D QML item crashes app when using Qt6
- 使用 QML 类型系统注册 C++ 类型
- 在QML中注册C++类型
- Qt Quick Scene Graph Default Renderer
- qml如何绘制三维笛卡尔坐标系并向其中添加折线?
- QML多线程魔法:探索不同方法,提升性能
- QT c++错误提示解决方案记录
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付
