Qt 学习之路 2(92):QML 存储

对于很多应用程序,存储数据的能力是必须的。比如,你需要保存下用户设置的参数等。Qt/C++ 提供了强大的QSettings类,用于将用户数据保存在本地文件或操作系统提供的数据结构中(比如 Windows 的注册表)。但是,Qt Quick 只提供了有限的直接访问本地数据的能力。它没有提供像 C++ 那样,能够直接读写操作系统本地文件的功能,这有点类似于浏览器。因此,在很多应用中,读写文件只能通过 C++ 完成:使用 Qt Quick 实现前端界面,C++ 完成后端实际存储的功能。

另一方面,几乎所有应用程序都需要存储或多或少的数据。这些数据可以存储在本地文件中,也可以存储在本地或者远程的服务器。有些数据很简单(例如很多设置信息都是以键值对的形式存储),另外一些则非常复杂(例如我们想要保存一本书的全部信息,包括书名、作者、出版社、出版年、内容简介,甚至封面信息等)。针对这类数据,Qt Quick 提供了自己的解决方案。

前面我们说到,Qt/C++ 提供了强大的QSettings类。QSettings可以帮助我们以独立于操作系统的方式,将程序数据存储到本地。它利用的是操作系统相关的存储结构,或者是以一种通用的 INI 文件保存。

Qt 5.2 起,QML 引入了一个新的类Settings,顾名思义,它就是QSettings的 QML 版本。值得注意的是,直到目前最新的 Qt 5.5.1,Settings依然是试验性质 API,所以,它的 API 可能会在未来版本中有所变化。使用Settings需要添加import Qt.labs.settings 1.0语句。

下面我们创建一个带有颜色的矩形。用户点击这个矩形时,都会生成一个随机的颜色。当应用程序关闭时,当前颜色会保存在本地;重新打开程序,矩形会显示上一次最后的颜色。当程序第一次启动时,矩形会显示默认颜色 #000000;第二次启动时,将会从Settings读取到存储的值并自动绑定到矩形的属性。这是通过属性别名实现的。

import QtQuick 2.0
import Qt.labs.settings 1.0

Rectangle {
    id: root
    width: 320; height: 240
    color: '#000000'
    Settings {
        id: settings
        property alias color: root.color
    }
    MouseArea {
        anchors.fill: parent
        onClicked: root.color = Qt.hsla(Math.random(), 0.5, 0.5, 1.0);
    }
}

上面的实现中,每次值改变,Settings都会直接存储在本地。很多时候,我们并不希望这种实现,而是希望在我们需要的时候保存即可。为了达到这一目的,我们不能使用属性别名,而是需要提供一个额外的辅助函数,在恰当的时刻调用即可:

Rectangle {
    id: root
    color: settings.color
    Settings {
        id: settings
        property color color: '#000000'
    }
    function storeSettings() { // executed maybe on destruction
        settings.color = root.color
    }
}

Settings同样支持按组分类存储:

Settings {
    category: 'window'
    property alias x: window.x
    property alias y: window.x
    property alias width: window.width
    property alias height: window.height
}

类似QSettingsSettings同样根据应用名字、组织名字和域名存储数据。这种区分主要用于操作系统提供的数据结构,例如 Windows 平台的注册表的键值。这些信息需要在 C++ 代码中设置:

int main(int argc, char** argv) {
    ...
    QCoreApplication::setApplicationName("Awesome Application");
    QCoreApplication::setOrganizationName("Awesome Company");
    QCoreApplication::setOrganizationDomain("org.awesome");
    ...
}

Settings适合保存简单的键值对信息,对于复杂的结构化数据显得力不从心。HTML 5 增加了 localStorage API,用于浏览器存储结构化数据。Qt Quick 借鉴了 localStorage API,提供了类似的解决方案,名字也被称为 LocalStorage。为了使用该 API,需要添加语句import QtQuick.LocalStorage 2.0

LocalStorage 使用 SQLite 数据库保存数据。这个数据库的文件按照给定的数据库名字和版本保存在系统的指定位置,使用唯一 ID 标识。但是,系统并不允许列出或删除已创建的数据库。可以使用 C++ 的QQmlEngine::offlineStoragePath()函数查看数据库文件存储路径。

为了使用 SQLite 数据库,首先使用 API 创建数据库对象,然后开始一个事务。每一个事务都可以包含一条或多条 SQL 语句。事务中出现任何失败时,整个事务都会回滚。例如,我们要从一个简单的 notes 表中读取数据,就可以使用下面的代码:

import QtQuick 2.2
import QtQuick.LocalStorage 2.0

Item {
    Component.onCompleted: {
        var db = LocalStorage.openDatabaseSync("MyExample", "1.0", "Example database", 10000);
        db.transaction( function(tx) {
            var result = tx.executeSql('select * from notes');
            for(var i = 0; i < result.rows.length; i++) {
                    print(result.rows[i].text);
                }
            }
        });
    }
}

下面的例子中,我们假设需要保存场景中矩形的位置。

import QtQuick 2.2

Item {
    width: 400
    height: 400

    Rectangle {
        id: crazy
        objectName: 'crazy'
        width: 100
        height: 100
        x: 50
        y: 50
        color: "#53d769"
        border.color: Qt.lighter(color, 1.1)
        Text {
            anchors.centerIn: parent
            text: Math.round(parent.x) + '/' + Math.round(parent.y)
        }
        MouseArea {
            anchors.fill: parent
            drag.target: parent
        }
    }
}

我们可以使用鼠标将这个矩形到处拖动。当应用关闭、重新打开时,矩形会出现在关闭时的位置。现在,我们希望将矩形的坐标保存在 SQL 数据库中。为了达到这一目的,我们需要在组件创建完成时初始化一个数据库、读取数据库中的数据,在组件销毁时将其坐标写入数据库。

import QtQuick 2.2
import QtQuick.LocalStorage 2.0

Item {
    // 数据库对象的引用
    property var db;

    function initDatabase() {
        // 初始化数据库对象
    }

    function storeData() {
        // 将数据保存到数据库
    }

    function readData() {
        // 从数据库读取数据并使用数据
    }


    Component.onCompleted: {
        initDatabase();
        readData();
    }

    Component.onDestruction: {
        storeData();
    }
}

由于这些代码都是业务逻辑相关的,当然可以将这些数据库相关代码放到一个单独的 JS 文件中。事实上,将它们放在独立的 JS 文件中更好一些,不过这里我们就不涉及这些软件工程方面的问题了。

initDatabase函数中,我们需要完成数据库的初始化,同时要保证数据表存在:

function initDatabase() {
    print('initDatabase()')
    db = LocalStorage.openDatabaseSync("CrazyBox", "1.0", "A box who remembers its position", 100000);
    db.transaction( function(tx) {
        print('... create table')
        tx.executeSql('CREATE TABLE IF NOT EXISTS data(name TEXT, value TEXT)');
    });
}

接下来,程序会从数据库读取已有数据。这里需要有一个判断:数据库中是否真的有数据。这里,我们仅仅通过数据的条数来简单判断一下。

function readData() {
    print('readData()')
    if (!db) { return; }
    db.transaction( function(tx) {
        print('... read crazy object')
        var result = tx.executeSql('select * from data where name="crazy"');
        if (result.rows.length === 1) {
            print('... update crazy geometry')
            // 读取数据
            var value = result.rows[0].value;
            // 转换成 JS 对象
            var obj = JSON.parse(value)
            // 将数据应用到矩形对象
            crazy.x = obj.x;
            crazy.y = obj.y;
        }
    });
}

我们并没有将组件的坐标值按照 x 和 y 分开存储,而是以 JSON 的格式保存。对于 SQL 而言,这并不算一个好主意,但是能够很好的适用于 JS 代码。所以,为了简单起见,我们利用 JSON 相关函数,将对象转换成 JSON 格式之后才真正写入数据库。在读取时,要反过来将读取到的 JSON 转换成对象之后,才能应用到矩形。

为了保存数据,我们需要区分究竟应该执行插入还是更新。如果已有数据,需要更新;如果没有数据,则需要插入:

function storeData() {
    print('storeData()')
    if (!db) { return; }
    db.transaction( function(tx) {
        print('... check if a crazy object exists')
        var result = tx.executeSql('SELECT * from data where name = "crazy"');
        // 创建一个包含需要保存的数据的对象,之后需要将这个对象转换成 JSON
        var obj = { x: crazy.x, y: crazy.y };
        if (result.rows.length === 1) { // 已有数据,更新
            print('... crazy exists, update it')
            result = tx.executeSql('UPDATE data set value=? where name="crazy"', [JSON.stringify(obj)]);
        } else { // 没有数据,插入
            print('... crazy does not exists, create it')
            result = tx.executeSql('INSERT INTO data VALUES (?,?)', ['crazy', JSON.stringify(obj)]);
        }
    });
}

上面的代码在检查是否存在数据时,检索出整条记录,我们也可以通过SELECT COUNT(*) from data where name = "crazy"语句,仅仅返回检索条数,来获得更好的性能。关于 SQL 已经超出了本文的范畴,这里不再赘述。在UPDATEINSERT语句中,我们使用了?作为占位符。

下面可以执行程序,看程序是如何运行的。

有关 QML 的存储,我们已经介绍了两种最主要的方式。如果这些还是不能满足你的需求,那么我们还有最后一招,能够满足你的各种奇葩需求:使用 C++ 访问你想访问的任何存储系统。我们会在后面详细介绍如何使用 C++ 扩展 Qt Quick。

7 Comments

  1. 步云渊 2016年2月4日
    • 步云渊 2016年2月4日
  2. 高立飞 2016年3月6日
    • 豆子 2016年3月7日
  3. Houmin 2016年7月9日
  4. visualzxb 2017年6月15日

Leave a Reply