NotePublic/Software/Development/Environment/Qt/QML/QML_应用程序开发技术总结.md

44 KiB
Raw Blame History

layout title subtitle description excerpt date author tags categories published
post QML 应用程序开发技术总结 记录 VirtualBox 在使用过程中遇到的一些常见问题,并提供解决方案。 记录 VirtualBox 在使用过程中遇到的一些常见问题,并提供解决方案。 2024-03-11 18:02:00 Rick Chan
Development
Environment
Qt
QML
Software
true

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 指的是具体哪个对象(因此跨文件使用对象时不需要声明),其规则为从当前的使用位置开始,逐级向树的根部搜索,直到搜寻到一个匹配的对象为止。

2. 全局属性

使用 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"
}

3. 信号与槽

在 QML 中连接信号槽主要有以下 5 种方法。

3.1. 信号与信号处理器

signal()

on<Signal>: {}

这是最常见的方法,比如 onClicked: {} 槽。

3.2. 属性变化信号与属性变化信号处理器

property

on<Property>Changed: {}

QML 规范,当属性变化后会发送一个 <Property>Changed 信号。从 C 环境创建属性时需要注册 NOTIFY 信号,其命名格式也需要遵循该规范。

3.3. 附加属性与附加信号处理器

Attached Signal(附加属性)

XX元素.on<附加属性>: {}

最常见的是 Component.onCompleted{} 槽。

3.4. Connections 建立信号与槽的连接

Connections {
    target: 发送者
    发送者信号处理器{}
}

前面的信号连接都嵌入在某个对象中,当信号与槽的连接不依赖具体对象时,需要将其嵌入到 Connections 对象中。Connections 的 target 指向信号的发送者,一个 Connections 可以绑定同一个 target 的多个信号处理机。

Connections {
    target: whomSentTheSignal

    function onSignal0(arg) {
        console.log(arg)
    }

    function onSignal1(arg) {
        console.log(arg)
    }
    // ...
}

3.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
    }
}

3.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)
    }
}

4. 界面加载完成信号

许多时候需要在 QML 程序界面完成加载后做一些事情,比如设置一些属性的初始值。这时候就需要用到 Component 的 completed() 信号:

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 文件。

magick.exe convert icon-16.png icon-32.png icon-256.png myappico.ico

5.2. 添加图标到应用

在 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.

更详细内容见《Setting the Application Icon》。

6. 绘制圆形

设置 Rectangle 的 radius 为边长的一半即可。

7. 常用组件

7.1. QtQuick.Loader

TODO: TODO: 参考 XiaYu 项目中的 SubMenuGrp 和 SubMenuBtn 对 default 属性和子组件引用父组件方法、属性进行说明。

7.2. 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")
}

7.3. Dialog 对象

QML 中有三大类 Dialog 对象,这里主要介绍 QtQuick.Controls 中的 Dialog 和 QtQuick.Dialogs。

7.3.1. QtQuick.Controls 中的 Dialog

QtQuick.Controls 中的 Dialog 比较原始,属性需要自行定义和实现,所以它的自由度也比较高。该 Dialog 包含了页眉Header、页脚Footer和内容Content三部分每个部分都可以单独设定。

  • footerItem

    对话框页脚项。页脚项目位于底部,并调整为对话框的宽度。 默认值为空。

    注意:将 DialogButtonBox 指定为对话框页脚会自动将其 accepted() 和 rejected() 信号连接到 Dialog 中的相应信号。

    注意:将 DialogButtonBox、ToolBar 或 TabBar 指定为对话框页脚会自动将相应的 DialogButtonBox::position、ToolBar::position 或 TabBar::position 属性设置为 Footer。

  • headerItem

    对话框标题项。标题项位于顶部,并调整为对话框的宽度。默认值为空。

    注意:将 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")
    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则需要进行如下修改

/**
 * .pro 文件中增加 widgets
 */
QT += widgets

/**
 * main.cpp 中替换 QGuiApplication 为 QApplication
 */
// #include <QGuiApplication>
#include <QApplication>
// QGuiApplication app(argc, argv);
QApplication app(argc, argv);

Dialog 对象默认不显示,当调用 Dialog 的 open() 方法后弹出窗口并阻塞父窗体的执行。

7.3.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)
    }
}
7.3.2.2. MessageDialog

MessageDialog 为标准消息对话框。

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 为标准组合框

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 来组织多个不同类型。每个子成员必须设置高度和宽度,否则不被显示。

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 作为模型。

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 将不显示。

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 中增加了:

qputenv("QT_IM_MODULE", QByteArray("qtvirtualkeyboard"));

在 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 的应用可以参考官方自带示例。

7.11. Multimedia

需要引入:

import QtMultimedia x.xx

主要包含音视频的输入、处理和输出Video、Audio、Camera、VideoOutput、MediaPlayer、SoundEffect 等。

7.11.1. VideoOutput 和 MediaPlayer

Qt 只是提供了便捷易用的接口,实际的编解码功能由后端实现。 Qt5 Linux 下视频后端为 GStreamerWin 下为 DirectShow建议使用 LAVFiltersffmpeg 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 格式路径。

8. 多文档开发

8.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 字段中即可。

8.2. 如何引用自定义 QML 文件

参考 1.3. 使用自定义元件

8.3. 使用另一 QML 文件中的元件或属性

可以使用:

property alias <别名>:<属性名称/ID>

将一个 QML 文件中的某属性,某子组件的属性或某个子组件变为外部可见。

8.4. 示例

假设存在 DemoQml/Demo.qml 文件,该文件内容如下:

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 文件的相关字段如下:

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
        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() 方法进行注册:

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 上下文环境中。

9.2. C++ 访问 QML 对象

在 QML 中为对象添加 objectName 属性后,在 C++ 中可使用:

auto root = engine.rootObjects();
auto qmlObj = root.first()->findChild<QObject*>("object name");

查找该对象,再连接信号槽等。

大部分情况下在 QML 中访问 C++ 即可实现较完善的功能QML 传递信息给 C++ 完全可以通过信号槽机制实现。除非需要在 C++ 中动态创建对象并连接到 QML 中的信号槽,否则没必要这样设计。

9.3. 通过信号槽传递自建类型

当使用信号槽机制时,需要注意一点:如果需要通过信号槽传递自建类型数据,需要使用 qRegisterMetaType() 方法进行注册。

qRegisterMetaType<MyClass>("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 内容如下:

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)
        }
    }
}

10. 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
}

11. 打包发布与部署

11.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 自带的打包程序会添加额外的库,如果想进一步减小体积,可手动筛减。

11.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);
    // ...
}

12. QML 与 OpenGL

12.1. 在不支持 OpenGL 的机器上运行 QML 程序

Qt QML 程序默认使用 OpenGL 进行渲染,但某些系统由于没有 GPU 或者其他原因,导致不支持 OpenGL或者 OpenGL 版本无法满足要求,此时可指定 Qt 程序使用软件渲染方式(使用 CPU这可以通过设置环境变量来实现

QT_QUICK_BACKEND=software
# 或 legacy QMLSCENE_DEVICE
QMLSCENE_DEVICE=softwarecontext

QT_QUICK_BACKEND 可选值为:

  • rhi
  • software
  • openvg

也可在 Qt 程序源码中使用 [static] void QQuickWindow::setSceneGraphBackend(QSGRendererInterface::GraphicsApi api) 函数实现:

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 还是其他,设置方式同样是通过设置环境变量来实现:

QT_OPENGL=desktop

QT_OPENGL 可选参数为:

  • desktop使用 Desktop 版本的 OpenGL
  • es2嵌入式版本的 OpenGLOpenGL ES
  • angle使用 angle directX接口
  • softwareWindows 下依赖 opengl32sw.dll

13. 外部参考资料

  1. 深入了解JS中的整数
  2. QML 中的信号与槽
  3. QML 信号与响应方法的总结
  4. QML 控件类型ScrollBar、ScrollIndicator
  5. QML 类型GridView
  6. Qt Quick 常用元素ComboBox(下拉列表) 与 ProgressBar(进度条)
  7. 【QML Model-View】ListView-增删改查(二)
  8. 关于 Q_ENUMS 和 Q_ENUM 的区别和用法
  9. C++ 共享枚举类型给 QML
  10. QML Connections: Implicitly defined onFoo properties in Connections are deprecated.
  11. QML 调用 C++ 方法
  12. Qml 与 C++ 交互3Qml 的信号与 C++ 的槽函数连接
  13. Qt-虚拟键盘
  14. Qt5软键盘实现中文拼音输入法
  15. Qt6中加载自定义qml遇到的问题
  16. QML控件类型DialogQt Quick Controls 模块)
  17. Customizing Qt Quick Controls