--- layout: post title: "QML 应用程序开发技术总结" subtitle: "" description: "记录 VirtualBox 在使用过程中遇到的一些常见问题,并提供解决方案。" excerpt: "记录 VirtualBox 在使用过程中遇到的一些常见问题,并提供解决方案。" date: 2024-03-11 18:02:00 author: "Rick Chan" tags: ["Development", "Environment", "Qt", "QML"] categories: ["Software"] published: true --- - [1. 基础部分](#1-基础部分) - [1.1. 方法/属性名称的大小写](#11-方法属性名称的大小写) - [1.2. 显示顺序](#12-显示顺序) - [1.3. 使用自定义元件](#13-使用自定义元件) - [1.4. 包别名](#14-包别名) - [1.5. 对象 ID](#15-对象-id) - [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-添加图标) - [5.1. 制作图标](#51-制作图标) - [5.2. 添加图标到应用](#52-添加图标到应用) - [6. 绘制圆形](#6-绘制圆形) - [7. 常用组件](#7-常用组件) - [7.1. QtQuick.Loader](#71-qtquickloader) - [7.2. QtQuick.Controls.Button](#72-qtquickcontrolsbutton) - [7.3. Dialog 对象](#73-dialog-对象) - [7.3.1. QtQuick.Controls 中的 Dialog](#731-qtquickcontrols-中的-dialog) - [7.3.2. QtQuick.Dialogs](#732-qtquickdialogs) - [7.3.2.1. FileDialog](#7321-filedialog) - [7.3.2.2. MessageDialog](#7322-messagedialog) - [7.4. ComboBox](#74-combobox) - [7.5. Grid](#75-grid) - [7.6. ScrollView](#76-scrollview) - [7.7. QtQuick.ListView](#77-qtquicklistview) - [7.8. GridView](#78-gridview) - [7.9. BusyIndicator](#79-busyindicator) - [7.10. VirtualKeyboard](#710-virtualkeyboard) - [7.11. Multimedia](#711-multimedia) - [7.11.1. VideoOutput 和 MediaPlayer](#7111-videooutput-和-mediaplayer) - [8. 多文档开发](#8-多文档开发) - [8.1. 多 QML 文件的管理](#81-多-qml-文件的管理) - [8.2. 如何引用自定义 QML 文件](#82-如何引用自定义-qml-文件) - [8.3. 使用另一 QML 文件中的元件或属性](#83-使用另一-qml-文件中的元件或属性) - [8.4. 示例](#84-示例) - [9. QML 与 C++ 交互](#9-qml-与-c-交互) - [9.1. QML 访问 C++ 中声明的类型](#91-qml-访问-c-中声明的类型) - [9.2. C++ 访问 QML 对象](#92-c-访问-qml-对象) - [9.3. 通过信号槽传递自建类型](#93-通过信号槽传递自建类型) - [9.4. QML 与 C++ 交互综合示例](#94-qml-与-c-交互综合示例) - [10. Linux 下为 Qt Quick 应用程序隐藏鼠标指针](#10-linux-下为-qt-quick-应用程序隐藏鼠标指针) - [11. 打包发布与部署](#11-打包发布与部署) - [11.1. Windows 下 QML 程序的打包发布](#111-windows-下-qml-程序的打包发布) - [11.2. 运行环境设置](#112-运行环境设置) - [12. QML 与 OpenGL](#12-qml-与-opengl) - [12.1. 在不支持 OpenGL 的机器上运行 QML 程序](#121-在不支持-opengl-的机器上运行-qml-程序) - [12.2. 切换 OpenGL 模式](#122-切换-opengl-模式) - [13. 外部参考资料](#13-外部参考资料) ## 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 文件中先写的对象显示在下层,后写的对象显示在上层。在显示时,上层的对象覆盖下层对象。 ### 1.3. 使用自定义元件 QML 中元件被定义在包中,通过 import 引用对应的包来使用其中的元件,一个文件夹可以构成一个包。 也可以自定义元件,每个 QML 文件都可以是一个自定义元件,QML 文件的名称就是元件的名称。例如定义一个 SubRec 元件,首先创建 SubRec.qml 文件,内容如下: ```js import QtQuick 2.12 Rectangle { color: "#bad6bf" } ``` 当 QML 文件作为元件使用时,文件名必须以大写字母开头,以表明这是一个 QML 类型。 如果调用 SubRec 的地方与 SubRec.qml 处于同一文件夹中,则认为他们是同一个包中的元件,不需要 import 就可以直接使用。 如果调用 SubRec 的地方与 SubRec.qml 处于不同文件夹中(子文件夹也算不同文件夹),则认为不是同一个包中的元件,需要 import 才可以使用,例如,上面的 SubRec.qml 处于子文件夹 SubComp 中时,使用相对路径方式进行引用,如下: ```js 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 来实现类似别名或者命名空间的功能。 ```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) } } ``` ### 1.5. 对象 ID QML 中可以使用各种元素来构建 UI,比如 Window、Rectangle、Item 等。被使用的元素形成具体的对象,在运行时创建对应实例。对象可以被分配 ID,对象之间可以使用 ID 相互引用,这种引用是“全局”的。例如,在不同 QML 文件中,分别创建主对象和子对象,在子对象中可以直接引用主对象的 ID。 主对象 mainRec 在 main.qml 文件中,内容如下: ```js 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 中,内容如下: ```js 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 指的是具体哪个对象(因此跨文件使用对象时不需要声明),其规则为从当前的使用位置开始,逐级向树的根部搜索,直到搜寻到一个匹配的对象为止。 ## 2. 全局属性 使用 property 关键字可以定义全局属性,该语法格式如下: ```js [default] [required] [readonly] property ``` 属性名称必须以小写字母开头,并且只能包含字母、数字和下划线。 ```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: {} ``` 这是最常见的方法,比如 onClicked: {} 槽。 ### 3.2. 属性变化信号与属性变化信号处理器 ```js property onChanged: {} ``` QML 规范,当属性变化后会发送一个 \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\ 格式命名的响应方法,也可以指令信号触发下一个信号形成信号的连锁反应。 ```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 [([ [, ...]])] ``` 示例如下: ```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. 添加图标 ### 5.1. 制作图标 先安装 ImageMagic 工具,并将其添加到系统 PATH 下方便使用。 使用以下命令将多个不同尺寸的图片打包成一个 ICO 文件。 ```bash magick.exe convert icon-16.png icon-32.png icon-256.png myappico.ico ``` ### 5.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)》。 ## 6. 绘制圆形 设置 Rectangle 的 radius 为边长的一半即可。 ## 7. 常用组件 ### 7.1. QtQuick.Loader TODO: TODO: 参考 XiaYu 项目中的 SubMenuGrp 和 SubMenuBtn 对 default 属性和子组件引用父组件方法、属性进行说明。 ### 7.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") } ``` ### 7.3. Dialog 对象 QML 中有三大类 Dialog 对象,这里主要介绍 QtQuick.Controls 中的 Dialog 和 QtQuick.Dialogs。 #### 7.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 帮助获得相关帮助信息。 #### 7.3.2. QtQuick.Dialogs 在使用 QtQuick.Dialogs 的 Dialog 对象时,如果使用 QGuiApplication 来执行则会导致无法加载主题风格,并且对话框无法正确显示图标。如果最初使用 QGuiApplication 创建了 app,则需要进行如下修改: ```cpp /** * .pro 文件中增加 widgets */ QT += widgets /** * main.cpp 中替换 QGuiApplication 为 QApplication */ // #include #include // QGuiApplication app(argc, argv); QApplication app(argc, argv); ``` Dialog 对象默认不显示,当调用 Dialog 的 open() 方法后弹出窗口并阻塞父窗体的执行。 ##### 7.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) } } ``` ##### 7.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("细节")) } } } ``` ### 7.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"] } } ``` ### 7.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") } } } ``` ### 7.6. ScrollView ScrollView 会为其所容纳的对象创建滚动条。 ### 7.7. QtQuick.ListView TODO: header, headerPositioning TODO: 如果通过设置 header 和 headerPositioning 参数来实现固定 header,且同时伴有 ScrollBar 的情况下,滚动条依然会覆盖到 header 上,效果不理想。此时最好在 ListView 之外使用 Row 嵌套 Label 来实现类似效果。 ### 7.8. 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 } } } } } ``` ### 7.9. 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 进行自定义。 ### 7.10. 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 的应用可以参考官方自带示例。 ### 7.11. Multimedia 需要引入: ```js import QtMultimedia x.xx ``` 主要包含音视频的输入、处理和输出,如:Video、Audio、Camera、VideoOutput、MediaPlayer、SoundEffect 等。 #### 7.11.1. VideoOutput 和 MediaPlayer Qt 只是提供了便捷易用的接口,实际的编解码功能由后端实现。 Qt5 Linux 下视频后端为 GStreamer,Win 下为 DirectShow,建议使用 [LAVFilters](https://github.com/Nevcairiel/LAVFilters)(ffmpeg based DirectShow Splitter and Decoders)。Qt6 在 Linux 下好像增加了对 ffmpeg 的支持。 在使用 VideoOutput 和 MediaPlayer 时,有些时候程序编写正确,跑起来也没提示什么错误,但就是不播视频,显示黑屏且没有声音,此时很可能是后端没有安装或安装错误导致。 VideoOutput 和 MediaPlayer 的使用非常简单,MediaPlayer 作为播放器 VideoOutput 作为输出显示,二者配合工作: ```js 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 格式路径。 ## 8. 多文档开发 ### 8.1. 多 QML 文件的管理 在创建基于 Qt6 的 QML 项目时,不会包含 qml.qrc 文件,系统编译时自动创建该文件,创建的依据是 .pro 文件中的 resources.xxx 字段: ```js resources.files = main.qml resources.prefix = /$${TARGET} ``` 当使用 QtCreator 新建文件向导向工程增加自定义 QML 文件时,QML 文件可能会被错误的增加到 DISTFILES 字段,比如: ```js DISTFILES += \ Demo.qml ``` 此时编译程序会报: ```js failed to load component ... is not a type ``` 错误。解决方法是:手动修改 Demo.qml 到 resources.files 字段中即可。 ### 8.2. 如何引用自定义 QML 文件 参考 [1.3. 使用自定义元件](#13-使用自定义元件) ### 8.3. 使用另一 QML 文件中的元件或属性 可以使用: ```js property alias <别名>:<属性名称/ID> ``` 将一个 QML 文件中的某属性,某子组件的属性或某个子组件变为外部可见。 ### 8.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() } } } ``` ## 9. QML 与 C++ 交互 QML 与 C++ 交互的主要实现方式是: 1. QML 访问 C++ 中声明的类型; 2. QML 访问 C++ 中创建的对象; 3. QML 调用 C++ 中的方法或发射信号; 4. QML 响应 C++ 中产生的信号; 5. C++ 访问 QML 对象。 QML 与 C++ 之间主要通过信号槽机制来传递消息。 ### 9.1. QML 访问 C++ 中声明的类型 QML 使用 C++ 中声明的类型可以为类、结构体或枚举等。若需要将 C++ 类导出给 QML,则需要使用 qmlRegisterType() 方法进行注册: ```cpp qmlRegisterType("package.Type", , , "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 上下文环境中。 ### 9.2. C++ 访问 QML 对象 在 QML 中为对象添加 objectName 属性后,在 C++ 中可使用: ```cpp auto root = engine.rootObjects(); auto qmlObj = root.first()->findChild("object name"); ``` 查找该对象,再连接信号槽等。 大部分情况下在 QML 中访问 C++ 即可实现较完善的功能,QML 传递信息给 C++ 完全可以通过信号槽机制实现。除非需要在 C++ 中动态创建对象并连接到 QML 中的信号槽,否则没必要这样设计。 ### 9.3. 通过信号槽传递自建类型 当使用信号槽机制时,需要注意一点:如果需要通过信号槽传递自建类型数据,需要使用 qRegisterMetaType() 方法进行注册。 ```cpp qRegisterMetaType("Myclass"); ``` ### 9.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 main.qml ``` DemoDirect.h 内容如下 ```cpp #ifndef DEMODIRECT_H #define DEMODIRECT_H #include 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 DemoDirect::DemoDirect(QObject *parent) : QObject{parent} { cppStr = "Hello"; } void DemoDirect::writeCppStr(QString str) { cppStr = str; qDebug()<<"[CPP:DemoDirect:writeCppStr]srt="< #include 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 #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 DemoIndirect::DemoIndirect(class DemoJoint* j, QObject *parent) : QObject{parent} { joint = j; } void DemoIndirect::onQml2Cpp(DemoJoint::DemoEnum op) { qDebug()<<"[CPP:DemoIndirect:onQml2Cpp]op="<cpp2Qml(DemoJoint::DE_OPEN_DONE); break; default: emit joint->cpp2Qml(DemoJoint::DE_CLOSE_DONE); break; } } ``` main.cpp 内容如下: ```cpp #include #include #include #include "DemoJoint.h" #include "DemoDirect.h" int main(int argc, char *argv[]) { // 注册 DemoDirect 类型, 否则无法在 QML 中使用 DemoDirect 创建对象. qmlRegisterType("project.DemoDirect", 1, 0, "DemoDirect"); // 注册 DemoJoint 类型, 否则无法在 QML 中使用 DemoJoint.DemoEnum 类型. qmlRegisterType("project.DemoJoint", 1, 0, "DemoJoint"); // 注册用于在信号槽中使用的类型. qRegisterMetaType("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) } } } ``` ## 10. Linux 下为 Qt Quick 应用程序隐藏鼠标指针 QML 中可以使用 MouseArea 来隐藏鼠标指针,但是在程序刚启动时鼠标指针依然可见,只有鼠标动过或者点击过才会消失。因此需要 unclutter 程序来辅助,unclutter 可以在系统空闲时自动隐藏鼠标指针。首先安装 unclutter: ```bash # Ubuntu sudo apt install unclutter ``` 并修改 /etc/default/unclutter: ```bash # /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 文件中添加: ```js MouseArea { z: 99 anchors.fill: parent enabled: false cursorShape: Qt.BlankCursor } ``` ## 11. 打包发布与部署 ### 11.1. Windows 下 QML 程序的打包发布 Qt 提供了导出 Qt 环境变量的命令行脚本,比如“Qt 5.15.2 (MinGW 8.1.0 64-bit)”,运行该脚本可进入带有 Qt 环境变量的命令行界面,之后可通过如下命令打包程序(编译生成的可执行程序需要拷贝到\): ```bash cd windeployqt [--qmldir ] ``` Qt 自带的打包程序会添加额外的库,如果想进一步减小体积,可手动筛减。 ### 11.2. 运行环境设置 Qt 程序启动时会检查系统环境变量,传入参数等。在部署 Qt 程序时,需要正确设置环境变量以及启动参数。其中环境变量设置在 Linux 中可修改 /etc/environment 或者 ~/.bashrc,甚至是 systemd 服务文件(.service 文件)中配置。 Windows 下可在“系统->高级系统设置->环境变量”中进行设置。 也可以在 Qt 程序源代码最开始的地方使用 qputenv() 函数进行设置,比如: ```cpp int main(int argc, char *argv[]) { qputenv("QT_IM_MODULE", QByteArray("qtvirtualkeyboard")); // ... QGuiApplication app(argc, argv); // ... } ``` ## 12. QML 与 OpenGL ### 12.1. 在不支持 OpenGL 的机器上运行 QML 程序 Qt QML 程序默认使用 OpenGL 进行渲染,但某些系统由于没有 GPU 或者其他原因,导致不支持 OpenGL,或者 OpenGL 版本无法满足要求,此时可指定 Qt 程序使用软件渲染方式(使用 CPU),这可以通过设置环境变量来实现: ```bash QT_QUICK_BACKEND=software # 或 legacy QMLSCENE_DEVICE QMLSCENE_DEVICE=softwarecontext ``` QT_QUICK_BACKEND 可选值为: - rhi - software - openvg 也可在 Qt 程序源码中使用 [static] void QQuickWindow::setSceneGraphBackend(QSGRendererInterface::GraphicsApi api) 函数实现: ```cpp 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. ***注意:Qt 的 VideoOutput 不能使用软件渲染即 Qt Quick Software Adaptation。*** ### 12.2. 切换 OpenGL 模式 Qt 可以选择使用 Desktop 版本的 OpenGL 还是其他,设置方式同样是通过设置环境变量来实现: ```bash QT_OPENGL=desktop ``` QT_OPENGL 可选参数为: - desktop:使用 Desktop 版本的 OpenGL - es2:嵌入式版本的 OpenGL(OpenGL ES) - angle:使用 angle directX接口 - software:Windows 下依赖 opengl32sw.dll ## 13. 外部参考资料 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) 17. [Customizing Qt Quick Controls](https://doc.qt.io/qt-5/qtquickcontrols2-customize.html)