在上一章中,我们试图利用 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(); ... }
我们不去解释source
、text
属性,因为它们只是简单的 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 数组,视图也并不知道。因此,我们只能将数据重新赋值,才能够让视图发现自己的数据发生了变化,从而刷新试图内容。
程序运行结果如下:
尽管这只是一个非常简单的插件,但是这个插件已经能够复用,也能够被不同的应用再次扩展。但是,插件也带来了一定程度的复杂度:你需要为应用程序开发不同的插件。这需要在我们前面所说的几种扩展机制之间进行权衡。在较大型的应用中,我们经常会实现一个简单的 QML 运行时,将需要与本地数据进行交互的部分全部封装在插件中。此时,整个项目就是使用了 QML 扩展插件的纯 QML 工程。这种层次结构将业务逻辑与界面分离,具有极大地灵活性,能够自如地应对未来 UI 的改变。
QML 插件提供了一个 C++ 后端与 QML 前端的清晰地分层机制。在开发 QML 插件时,你需要时刻考虑在 QML 中如何使用你的插件。最好能够经常创建一些 QML 文件,专门用来测试插件 API,以防 C++ 插件提供的 API 发生了改变。这种 API 测试工具其实也是 QML 使用插件的过程,将来在插件真正开发完毕时,只需要修改下插件的import
语句即可。
附件:
- fileio 插件:fileio.zip
- CityUI 应用程序:CityUI.zip
17 评论
豆子你好,C++扩展QML(续)这篇文章我还有一个地方不太明白,在FileIO::read()函数里emit textChanged(m_text);这个信号发出去后,是在哪里接收的呢,我下载这个附件后,把这条语句注释掉似乎也没什么影响?在QML文件里用尝试connection接收也没有什么反应
这个信号用于通知 QML 的绑定。如果没有这个信号,绑定就不能使用,因为它不能知道什么时候发生了改变。
豆哥加油啊!这个系列还在持续更新方便我们这些刚刚接触Qt的菜鸟,对于现在QT5的QML很是迷茫,很不习惯,希望博主多讲讲这方面的。总感觉现在的Qt编程有点像现在web开发的架构了
QML 只是使用了 JS 作为编程语言的一部分,方便一些有 web 开发基础的开发人员参与进来。这部分我也不是很熟悉,只能慢慢学习了。
豆哥,写的很不错,先大大的赞一个!最近自己在学习qml,一直感觉无从下手,看了这个学到了很多。豆哥,有没有什么好的学习qml的网站或者博客书籍类似的推荐啊?
目前只知道一个 http://qmlbook.org/ 还不错的
博主的文章写得真不错!但是网站的Qt学习之路的链接顺序能按照从前往后的顺序排列就好了,现在总得从上往下翻好久才能找着要看的。
看完了所有的qml相关的,想请教博主一个问题:开发一个消耗内存读写以及大量数据运算的软件,是否qml不太合适?之前用widget、dialog等写出了软件,发现界面比较难看。了解了qml后,发现点击按钮后响应函数(调用c++函数进行运算)这一步有点棘手。
是的,大量计算的话一般还是考虑 C++ 实现比较合适
请问一下,qt怎么引用ImageMagick 库的?能帮我介绍一下吗?
豆子哥,你好,我最近在嵌入式设备上使用qt5.7遇到一个问题,使用qt无法创建中文文件夹,可以创建英文的,系统已经安装中文字体,而且在qt写的软件界面上可以显示中文,请问你有什么看法,我已经困扰很久了。补充:之前用x11+qt5可以创建中文文件夹,现在用的是eglfs+qt5。
豆子哥,您好。请问如何实现qt application与qt quick之间的交互呢?也就是如何将qt application界面运行的结果,如何直接传递给qt quick程序,这样可以做到动态显示哇?
豆子哥,有些问题想通过邮箱跟您请教下,您邮箱是多少啊?
devbean@outlook.com
想知道豆子大佬 用qt开发过uwp没有
木有呀,Qt 原本就是业务的
我现在才开始学qt,豆哥这个系列的作品更新完了吗o.0?