第十四章:存储

第十四章:存储

存储

本章将讨论在Qt Quick里如何存储和检索数据。Qt Quick仅提供了有限的几种直接存储本地数据的方式。这咱场景下,它的角色更象浏览器。在很多项目中,存储数据是由C++ 后端处理的,并需要将功能函数暴露给Qt Quick前端。Qt Quick并不提供象Qt C++ 那样直接对本地文件系统的读写访问方法。所以后端工程师的职责是写一个这样的插件,或是通过网络访问本地数据服务器,来提供这样的数据访问能力。
每个应用都需要持久化存储或大或小的信息。这可以存在本地文件系统或远程服务器上。有些信息是可以结构化并简化(比如配置信息),有些信息可能大些并复杂一些,比如文档文件,而有些信息是更大的结构化信息并且需要某咱类型的数据库连接。这里主要涵盖Qt Quick内置的存储数据及能力,及通过网络存取数据的能力。

配置

Qt 自带一个Settings元素来加载和保存设置。但它仍然在试验模块中,这意味着将来其API可能会有较大变化。要注意这一点。
下面是一个小例子,对一个基本的矩形应用了颜色。每次用户点击窗体,会随机生成一个新的颜色。当应用关闭并重新启动后,会看到上次的颜色。默认颜色应该在顶层矩形上定义和初始化的颜色。

import QtQuick
import Qt.labs.settings 1.0

Rectangle {
    id: root

    width: 320
    height: 240
    color: '#fff' // default color
    Settings {
        property alias color: root.color
    }
    MouseArea {
        anchors.fill: parent
        // random color
        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
    }
}

也可以使用category属性,将配置信息分组到不同的节中。

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

这些设置是根据应用程序名称、组织和域来存储的。这些信息通常在C++ 的main 函数中设置。

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

如果你写的是纯QML的应用,你可以使用全局属性来设置同名的配置,这些全局属性是:Qt.application.name, Qt.application.organization, Qt.application.domain

本地存储—SQL

Qt Quick支持本地存储API,就象页面浏览本地存储那样的API。API要导入"import QtQuick.LocalStorage 2.0"才可用。
通常,内容将被存储到一个命名唯一的Sqlite数据库文件,其存储路径是依赖于给定的数据库名称与版本的系统特定位置。不能列出或删除已存在的数据库。你可以通过QQmlEngine::offlineStoragePath()来找出存储位置。
使用API首先要创建数据库对象,然后在数据库上创建事务。每个事务可以包括一个或多个SQL 查询。当事务中的一个查询语句执行失败了,事务将被回滚。
比如,从一个简单的记事本表里读取一个text列,可以象如下使用本地存储:

import QtQuick
import QtQuick.LocalStorage 2.0

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

疯狂的矩形

下面的例子假定想要存储矩形在屏幕上的位置。

这是例子的主要代码。它包含一个名为crazy的矩形,矩形可被拖动并以文本显示当前位置的xy值。

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
        }
    }
    // ...

你可以随意拖动矩形。当你尖闭应用然后重启时,跟重启前相比,矩形位于相同的位置。
现在我们想将矩形的x/y位置存储到SQL 数据库中。为此,需要添加init, read , store数据库函数。这些函数在组件完成和销毁时被调用。

import QtQuick
import QtQuick.LocalStorage 2.0

Item {
    // reference to the database object
    property var db

    function initDatabase() {
        // initialize the database object
    }

    function storeData() {
        // stores data to DB
    }

    function readData() {
        // reads and applies data from DB
    }

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

    Component.onDestruction: {
        storeData()
    }
}

可以将数据库相关的代码提取为单独的JS库文件,来完成所有的相关逻辑。如果逻辑较为复杂,这也是较推荐的方法。
在数据库初始化函数,我们创建数据库对象并确保SQL表格被创建。注意数据库函数做了不少友好的输出,以便可以在控制台来跟踪它的执行情况。

function initDatabase() {
    // initialize the database object
    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() {
    // reads and applies data from DB
    print('readData()')
    if(!db) { return }
    db.transaction(function(tx) {
        print('... read crazy object')
        const result = tx.executeSql('select * from data where name="crazy"')
        if(result.rows.length === 1) {
            print('... update crazy geometry')
            // get the value column
            const value = result.rows[0].value
            // convert to JS object
            const obj = JSON.parse(value)
            // apply to object
            crazy.x = obj.x
            crazy.y = obj.y
        }
    })
}

我们期待数据以JSON格式字串形式存储于数据列。这不是典型的SQL的样子,但在JS代码中运行良好。所以,我们并非将x/y属性值存在于表中,而是使用JSON的stringify/parse方法将其存储为纯粹的JS对象。最后,我们获得了一个可用的JS对象,这个对象有x和y属性,可以被用于crazy矩形。
存储数据时,需要区分插入和更新的场景。当记录已经存在时要用更新update语句,当在表中没有名为crazy的记录时,要用插入insert语句。

function storeData() {
    // stores data to DB
    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"')
        // prepare object to be stored as JSON
        var obj = { x: crazy.x, y: crazy.y }
        if(result.rows.length === 1) { // use update
            print('... crazy exists, update it')
            result = tx.executeSql('UPDATE data set value=? where name="crazy"', [JSON.stringify(obj)])
        } else { // use insert
            print('... crazy does not exists, create it')
            result = tx.executeSql('INSERT INTO data VALUES (?,?)', ['crazy', JSON.stringify(obj)])
        }
    })
}

相对于选取整个数据集,我们可以使用SQLite的count函数,如下:SELECT COUNT(*) from data where name = "crazy",它可能返回满足查询语句条件的数据行的数量。这是很普通的SQL代码。作为一个附加的特性,我们在查询语句中使用来绑定SQL值。
现在你可以拖动矩形,当退出应用时,数据库会存储矩形位置信息的x/y值,并应用于下次程序启动时的矩形位置信息。

posted @ 2022-03-27 20:44  sammy621  阅读(158)  评论(0编辑  收藏  举报