在上一章中,我们试图利用 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程序,这样可以做到动态显示哇?
豆子哥,有些问题想通过邮箱跟您请教下,您邮箱是多少啊?
de*****@*****ok.com
想知道豆子大佬 用qt开发过uwp没有
木有呀,Qt 原本就是业务的
我现在才开始学qt,豆哥这个系列的作品更新完了吗o.0?