首页 Qt 学习之路 2 Qt 学习之路 2(94):使用 C++ 扩展 QML(续)

Qt 学习之路 2(94):使用 C++ 扩展 QML(续)

16 12

上一章中,我们试图利用 C++ 扩展 QML,实现一个名为 FileIO 的插件。我们已经完成了初步的框架,下面就是要向之前的框架中补充代码了。

FileIO的实现并不复杂,最终创建的 API 应该类似于这样:

class FileIO : public QObject {
    ...
    Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
    Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
    ...
public:
    Q_INVOKABLE void read();
    Q_INVOKABLE void write();
    ...
}

我们不去解释sourcetext属性,因为它们只是简单的 setter 和 getter 函数。

read()函数用于以读方式打开文件,然后使用一个文本流读取文件中的数据:

void FileIO::read()
{
    if (m_source.isEmpty()) {
        return;
    }
    QFile file(m_source.toLocalFile());
    if (!file.exists()) {
        qWarning() << "Does not exits: " << m_source.toLocalFile();
        return;
    }
    if (file.open(QIODevice::ReadOnly)) {
        QTextStream stream(&file);
        m_text = stream.readAll();
        emit textChanged(m_text);
    }
}

当文本发生改变时,我们可以使用emit textChanged(m_text)发出一个信号,通知外界有关信息。事实上,这是必要的步骤,否则我们就不能使用 QML 的数据绑定机制了。

write()函数也是类似的,它会以写的方式打开文件,使用流将内容写入文件:

void FileIO::write()
{
    if(m_source.isEmpty()) {
        return;
    }
    QFile file(m_source.toLocalFile());
    if(file.open(QIODevice::WriteOnly)) {
        QTextStream stream(&file);
        stream << m_text;
    }
}

插件编译之后,需要调用make install命令,自动将其复制到 qml 文件夹,否则的话,QML 引擎就不能找到这个模块了。如果没有make命令(比如 Windows 平台),则可以手动将编译生成的插件和 qmldir 文件复制到对应文件夹下。默认情况下,QML 引擎会在应用所在目录以及 Qt 系统目录两个位置搜索 QML 模块,如果没有的话就会报错。其中,Qt 系统目录是在 Qt 安装目录的 qml 文件夹下。模块目录结构必须满足 URI 格式。例如,我们的插件 URI 是 org.example.io,那么对应目录应该是 org/example/io,因此在复制文件时,需要将生成的 dll 和 qmldir 文件复制到 org/example/io 目录下即可。

注意,我们的插件仅仅是演示性质的,并不适合于大规模应用。这是因为我们使用阻塞方式读写文件:在读写时,Qt 的整个 UI 线程都会被阻塞。因此,FileIO 插件只适合于简单的小文件读写场景。

现在我们就可以试着使用这个插件了。下面,我们会从一个 JSON 文件中读取城市数据,将其显示到表格中。我们会使用两个工程,一个是扩展插件(名为 fileio),用于提供从文件中读写文本的能力;一个是应用上述插件的项目,用于在表格中显示从文件读取的数据,然后将修改重新写入文件。这些数据来自一个 cities.json 文件。

JSON 就是按照特定规则存储的纯文本数据,可以很轻易地转换成 JS 对象。我们使用 FileIO 读取 JSON 格式数据,通过JSON.parse()函数将其转换成 JS 对象,作为表格的数据模型。当我们修改了表格数据时,则需要将修改过的数据重新写入这个 JSON 文件。该文件的部分内容如下所示:

[
    {
        "area": "1928",
        "city": "Shanghai",
        "country": "China",
        "flag": "22px-Flag_of_the_People's_Republic_of_China.svg.png",
        "population": "13831900"
    },
    ...
]

接下来,我们使用 Qt Creator 创建 QtQuick Application 项目,将 Qt Creator 生成的 main.qml 修改为:

import QtQuick 2.4
import QtQuick.Controls 1.3
import QtQuick.Window 2.2
import QtQuick.Dialogs 1.2

ApplicationWindow {
    id: root
    title: qsTr("City UI")
    width: 640
    height: 480
    visible: true
}

ApplicationWindow可以包含工具栏、菜单栏和状态栏等。这里,我们只使用菜单栏创建一些标准菜单项。

为了更好的复用,我们需要使用Action类型。类似于QAction,这种类型允许我们创建可重用动作。打开、保存和退出动作都是标准动作。现在,我们只创建不包含逻辑的动作:

...
Action {
    id: save
    text: qsTr("&Save")
    shortcut: StandardKey.Save
    onTriggered: { }
}

Action {
    id: open
    text: qsTr("&Open")
    shortcut: StandardKey.Open
    onTriggered: {}
}

Action {
    id: exit
    text: qsTr("E&xit")
    onTriggered: Qt.quit();
}

menuBar: MenuBar {
    Menu {
        title: qsTr("&File")
        MenuItem { action: open }
        MenuItem { action: save }
        MenuSeparator { }
        MenuItem { action: exit }
    }
}

...

FileDialog {
    id: openDialog
    onAccepted: { }
}

这里,我们创建了三个Action组件;菜单栏添加了一个 File 菜单项,并将这三个Action对象添加进来。最后,我们准备了一个文件对话框FileDialog,这将使我们能够在窗口选择文件。声明对话框并不会将其显示出来,我们还需要调用其open()函数。

前面说过,我们准备的 JSON 数据应该显示在一个表格中。这里我们使用了TableView组件。TableView声明了四列:city,country,area 和 population。每一个列都是一个标准的TableViewColumn组件。

TableView {
    id: view
    anchors.fill: parent
    TableViewColumn {
        role: 'city'
        title: "City"
        width: 120
    }
    TableViewColumn {
        role: 'country'
        title: "Country"
        width: 120
    }
    TableViewColumn {
        role: 'area'
        title: "Area"
        width: 80
    }
    TableViewColumn {
        role: 'population'
        title: "Population"
        width: 80
    }
}

现在,我们基本准备好了程序的界面。下面的工作是,利用 FileIO 扩展插件,从选择的文件中读取数据,将数据加载到表格。

首先是打开的动作。在“open”动作中,我们需要“打开”文件对话框。当用户通过文件对话框选择了一个文件后,会调用文件对话框的onAccepted()函数。在这个函数中,我们就要调用readDocument()函数。这个函数将用户选择的文件的 URL 传递给我们的 FileIO 对象,然后调用其read()函数。FileIO 读取的文本则直接传递给JSON.parse()函数,将 JSON 转换成 JS 对象之后,作为表格的模型。

Action {
    id: open
    ...
    onTriggered: {
        openDialog.open()
    }
}

...

FileDialog {
    id: openDialog
    onAccepted: {
        root.readDocument()
    }
}

function readDocument() {
    io.source = openDialog.fileUrl
    io.read()
    view.model = JSON.parse(io.text)
}


FileIO {
    id: io
}

为了保存数据,我们则需要在“保存”动作中调用saveDocument()函数。该函数将表格的模型作为 JS 对象传递给JSON.stringify()函数,后者则将对象转换成 JSON 字符串。该字符串则会被 FileIO 写入文件。JSON.stringify()函数的第二个参数null表示不经过其它处理,直接进行转换;第三个参数4则表示要用 4 个空格进行缩进。

Action {
    id: save
    ...
    onTriggered: {
        saveDocument()
    }
}

function saveDocument() {
    var data = view.model
    io.text = JSON.stringify(data, null, 4)
    io.write()
}

FileIO {
    id: io
}

为了让程序更漂亮一些,我们还希望在界面显示每个国家的国旗。国旗的数据已经在 JSON 中提供了。所以我们只需要组织界面。为了显示国旗,我们需要创建一个自定义代理:

TableViewColumn {
    delegate: Item {
        Image {
            anchors.centerIn: parent
            source: 'flags/' + styleData.value
        }
    }
    role: 'flag'
    title: "Flag"
    width: 40
}

TableViewColumn将 JS 模型中的flag属性作为styleData.value传入代理。代理则负责将国旗显示出来。

接下来,我们创建类似的代理,为每一行添加删除按钮,供用户改变数据:

TableViewColumn {
    delegate: Button {
        iconSource: "remove.png"
        onClicked: {
            var data = view.model
            data.splice(styleData.row, 1)
            view.model = data
        }
    }
    width: 40
}

为了删除数据,我们调用 JS 的splice()函数。因为模型就是 JS 数组类型的,所以我们可以直接调用这个函数。注意,当删除一行之后,我们还得将数据重新赋值给视图。由于 JS 数组并不像 Qt 的QAbstractItemModel那么智能:它的数据发生改变时并不会通知视图。所以,我们改变了 JS 数组,视图也并不知道。因此,我们只能将数据重新赋值,才能够让视图发现自己的数据发生了变化,从而刷新试图内容。

程序运行结果如下:

CityUI

尽管这只是一个非常简单的插件,但是这个插件已经能够复用,也能够被不同的应用再次扩展。但是,插件也带来了一定程度的复杂度:你需要为应用程序开发不同的插件。这需要在我们前面所说的几种扩展机制之间进行权衡。在较大型的应用中,我们经常会实现一个简单的 QML 运行时,将需要与本地数据进行交互的部分全部封装在插件中。此时,整个项目就是使用了 QML 扩展插件的纯 QML 工程。这种层次结构将业务逻辑与界面分离,具有极大地灵活性,能够自如地应对未来 UI 的改变。

QML 插件提供了一个 C++ 后端与 QML 前端的清晰地分层机制。在开发 QML 插件时,你需要时刻考虑在 QML 中如何使用你的插件。最好能够经常创建一些 QML 文件,专门用来测试插件 API,以防 C++ 插件提供的 API 发生了改变。这种 API 测试工具其实也是 QML 使用插件的过程,将来在插件真正开发完毕时,只需要修改下插件的import语句即可。

附件:

16 评论

木目星 2016年3月20日 - 11:29

豆子你好,C++扩展QML(续)这篇文章我还有一个地方不太明白,在FileIO::read()函数里emit textChanged(m_text);这个信号发出去后,是在哪里接收的呢,我下载这个附件后,把这条语句注释掉似乎也没什么影响?在QML文件里用尝试connection接收也没有什么反应

回复
豆子 2016年3月20日 - 16:33

这个信号用于通知 QML 的绑定。如果没有这个信号,绑定就不能使用,因为它不能知道什么时候发生了改变。

回复
呜喵君 2016年3月30日 - 21:54

豆哥加油啊!这个系列还在持续更新方便我们这些刚刚接触Qt的菜鸟,对于现在QT5的QML很是迷茫,很不习惯,希望博主多讲讲这方面的。总感觉现在的Qt编程有点像现在web开发的架构了

回复
豆子 2016年3月30日 - 23:11

QML 只是使用了 JS 作为编程语言的一部分,方便一些有 web 开发基础的开发人员参与进来。这部分我也不是很熟悉,只能慢慢学习了。

回复
kyle 2016年3月31日 - 15:18

豆哥,写的很不错,先大大的赞一个!最近自己在学习qml,一直感觉无从下手,看了这个学到了很多。豆哥,有没有什么好的学习qml的网站或者博客书籍类似的推荐啊?

回复
豆子 2016年4月7日 - 10:56

目前只知道一个 http://qmlbook.org/ 还不错的

回复
余赈 2016年4月1日 - 22:15

博主的文章写得真不错!但是网站的Qt学习之路的链接顺序能按照从前往后的顺序排列就好了,现在总得从上往下翻好久才能找着要看的。

回复
huishou 2016年5月26日 - 21:48

看完了所有的qml相关的,想请教博主一个问题:开发一个消耗内存读写以及大量数据运算的软件,是否qml不太合适?之前用widget、dialog等写出了软件,发现界面比较难看。了解了qml后,发现点击按钮后响应函数(调用c++函数进行运算)这一步有点棘手。

回复
豆子 2016年5月26日 - 22:02

是的,大量计算的话一般还是考虑 C++ 实现比较合适

回复
Yann.wang 2016年7月13日 - 09:18

请问一下,qt怎么引用ImageMagick 库的?能帮我介绍一下吗?

回复
BrillZhang 2017年4月4日 - 13:20

豆子哥,你好,我最近在嵌入式设备上使用qt5.7遇到一个问题,使用qt无法创建中文文件夹,可以创建英文的,系统已经安装中文字体,而且在qt写的软件界面上可以显示中文,请问你有什么看法,我已经困扰很久了。补充:之前用x11+qt5可以创建中文文件夹,现在用的是eglfs+qt5。

回复
齐勇 2017年12月28日 - 15:53

豆子哥,您好。请问如何实现qt application与qt quick之间的交互呢?也就是如何将qt application界面运行的结果,如何直接传递给qt quick程序,这样可以做到动态显示哇?

回复
2019年9月13日 - 15:35

豆子哥,有些问题想通过邮箱跟您请教下,您邮箱是多少啊?

回复
豆子 2019年9月19日 - 15:29 回复
SHiver 2021年2月5日 - 00:35

想知道豆子大佬 用qt开发过uwp没有

回复
豆子 2021年2月23日 - 17:10

木有呀,Qt 原本就是业务的

回复

发表评论

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2