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

上一章中,我们试图利用 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语句即可。

附件:

14 Comments

  1. 木目星 2016年3月20日
    • 豆子 2016年3月20日
  2. 呜喵君 2016年3月30日
    • 豆子 2016年3月30日
  3. kyle 2016年3月31日
  4. 余赈 2016年4月1日
  5. huishou 2016年5月26日
    • 豆子 2016年5月26日
  6. Yann.wang 2016年7月13日
  7. BrillZhang 2017年4月4日
  8. 齐勇 2017年12月28日
  9. 2019年9月13日

Leave a Reply