我们知道JSON数据格式被广泛使用在很多的应用中,它可以帮我们保存我们应用的设置数据等。在QML中的ListView或GridView中,我们使用ListModel来显示它里面的数据。这个数据可以来源于xml或JSON。在ListView或GridView中,我们也可以动态修改ListModel中的数据。那么我们将如何保存这个数据呢?本篇文章也同样适用于xml格式的保存。这个练习就留个开发者自己了。当然保存ListModel中的数据也可以使用到SQLite数据库。这个因人而已!
为了方便我们的设计,我们使用了Ubuntu SDK中提供的“QML App with C++ plugin (qmake)”。这个项目的模版只适用于15.04及以上的target(在14.10中不被支持)。
在plugin中,我们设计了如下的fileio.cpp文件:
fileio.cpp
#include <QStandardPaths> #include "fileio.h" FileIO::FileIO(QObject *parent) : QObject(parent) { } FileIO::~FileIO() { } void FileIO::read() { if(m_path.isEmpty()) { return; } // QFile file(m_path.toLocalFile()); QFile file(m_path.path()); if(!file.exists()) { qWarning() << "Does not exits: " << m_path.toLocalFile(); return; } if(file.open(QIODevice::ReadOnly)) { QTextStream stream(&file); m_text = stream.readAll(); emit textChanged(m_text); qDebug() << "Text has been successfully read!"; } } void FileIO::write() { if(m_source.isEmpty()) { return; } qDebug() << "filename: " << m_path.fileName(); qDebug() << "path: " << m_path.path(); QFile file(m_path.path()); qDebug() << "File path: " << file.fileName(); if(file.open(QIODevice::WriteOnly)) { QTextStream stream(&file); stream << m_text; qDebug() << "Successfully write to file"; } else { qWarning() << "Failed to write to the file: " << m_path; } } QString FileIO::source() const { return m_source; } QString FileIO::text() { qDebug() << "Going to read the text"; read(); return m_text; } void FileIO::setSource(QString source) { if (m_source == source) return; m_source = source; emit sourceChanged(source); // at the same time update the path m_path = QUrl(getFilePath(source)); } void FileIO::setText(QString text) { if (m_text == text) return; m_text = text; write(); emit textChanged(text); } QString FileIO::getFilePath(const QString filename) const { // QString APP_ID = getenv("APP_ID"); // QString app_pkgname = APP_ID.split('_')[0]; // QString path = getenv("XDG_DATA_HOME") + // "/" + app_pkgname + "/" + filename; // qDebug() << "path: " << path; // return path; QString writablePath = QStandardPaths:: writableLocation(QStandardPaths::DataLocation); qDebug() << "writablePath: " << writablePath; QString absolutePath = QDir(writablePath).absolutePath(); qDebug() << "absoluePath: " << absolutePath; // We need to make sure we have the path for storage QDir dir(absolutePath); if ( dir.mkdir(absolutePath) ) { qDebug() << "Successfully created the path!"; } QString path = absolutePath + "/" + filename; qDebug() << "path: " << path; return path; }
在这里特别值得指出的是,由于Ubuntu应用的security,每个应用只有自己的独特的目录可以访问。在这个文件中,我们使用了Qt API QStandardPaths来获得应用的私有目录来访问。在以前的文章“如何使用Ubuntu手机平台中的照相机API来存储照片”中,我们也曾尝试使用环境变量的方法来获取这个目录,但是这些环境变量在电脑Desktop的环境中没有设置。
我们的主程序Main.qml也非常简单:
Main.qml
import QtQuick 2.4 import Ubuntu.Components 1.2 import Savejson 1.0 import "savedata.js" as Data /*! \brief MainView with a Label and Button elements. */ MainView { // objectName for functional testing purposes (autopilot-qt5) objectName: "mainView" // Note! applicationName needs to match the "name" field of the click manifest applicationName: "savejson.liu-xiao-guo" /* This property enables the application to change orientation when the device is rotated. The default is false. */ //automaticOrientation: true width: units.gu(60) height: units.gu(85) Page { id: mainPage title: i18n.tr("savejson") property string path: "" FileIO { id: fileio source: "sample.json" } // The model: ListModel { id: fruitModel objectName: "fruitModel" ListElement { name: "Apple"; cost: 2.45 image: "pics/apple.jpg" description: "Deciduous" } ListElement { name: "Banana"; cost: 1.95 image: "pics/banana.jpg" description: "Seedless" } ListElement { name: "Cumquat"; cost: 3.25 image: "pics/cumquat.jpg" description: "Citrus" } ListElement { name: "Durian"; cost: 9.95 image: "pics/durian.jpg" description: "Tropical Smelly" } } Component { id: listDelegate ListItem { id: delegateItem width: listView.width; height: units.gu(10) onPressAndHold: ListView.view.ViewItems.dragMode = !ListView.view.ViewItems.dragMode Image { id: pic height: parent.height - units.gu(1) width: height anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: units.gu(0.5) source: image } Column { id: content anchors.top: parent.top anchors.left: pic.right anchors.leftMargin: units.gu(2) anchors.topMargin: units.gu(1) width: parent.width - pic.width - units.gu(1) height: parent.height spacing: units.gu(1) Label { text: name } Label { text: description } Label { text: '$' + Number(cost).toFixed(2) font.bold: true } } trailingActions: ListItemActions { actions: [ Action { iconName: "add" onTriggered: { console.log("add is triggered!"); fruitModel.setProperty(index, "cost", cost + 0.25); } }, Action { iconName: "remove" onTriggered: { console.log("remove is triggered!"); fruitModel.setProperty(index, "cost", Math.max(0,cost-0.25)); } }, Action { iconName: "delete" onTriggered: { console.log("delete is triggered!"); fruitModel.remove(index) } } ] } color: dragMode ? "lightblue" : "lightgray" ListView.onAdd: SequentialAnimation { PropertyAction { target: delegateItem; property: "height"; value: 0 } NumberAnimation { target: delegateItem; property: "height"; to: delegateItem.height; duration: 250; easing.type: Easing.InOutQuad } } ListView.onRemove: SequentialAnimation { PropertyAction { target: delegateItem; property: "ListView.delayRemove"; value: true } NumberAnimation { target: delegateItem; property: "height"; to: 0; duration: 250; easing.type: Easing.InOutQuad } // Make sure delayRemove is set back to false so that the item can be destroyed PropertyAction { target: delegateItem; property: "ListView.delayRemove"; value: false } } } } ListView { id: listView anchors.fill: parent anchors.margins: 20 model: fruitModel delegate: listDelegate ViewItems.onDragUpdated: { if (event.status === ListItemDrag.Moving) { model.move(event.from, event.to, 1); } } moveDisplaced: Transition { UbuntuNumberAnimation { property: "y" } } } Row { anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.bottomMargin: units.gu(1) spacing: units.gu(1) Button { id: save text: "Save JSON" onClicked: { console.log("Going to save data!") var data = fruitModel; console.log("model data: " + JSON.stringify(fruitModel, null, 4)); var res = Data.serialize(data); console.log("res: " + res); fileio.text = res; } } Button { id: load text: "Load JSON" onClicked: { var json = JSON.parse(fileio.text); console.log("count: " + json.fruits.length); fruitModel.clear(); var count = json.fruits.length; for (var i in json.fruits) { var fruit = json.fruits[ i ]; console.log("name: " + fruit.name); console.log("image: " + fruit.image ); console.log("description: " + fruit.description); console.log("cost: " + fruit.cost); fruitModel.append( fruit ); } } } } } }
运行我们的应用,我们的界面如下:
我们创建了一个ListView列表。在列表中,我们可以通过“+”及“-”来修改水果的价钱,我们也可以删除一个水果。当然,我们也可以长按列表并移动列表中的项来重新排序我们的水果顺序。
最重要的是,我们可以通过FileIO来存储或读取我们所需要的JSON文件。为了方便,我们设置了一个“sample.json”。它在电脑中的路径为“:~/.local/share/savejson.liu-xiao-guo$”。存储的JSON文件例子为:
{ "fruits": [ { "name": "Banana", "image": "pics/banana.jpg", "description": "Seedless", "cost": 2.2 }, { "name": "Cumquat", "image": "pics/cumquat.jpg", "description": "Citrus", "cost": 3.25 }, { "name": "Durian", "image": "pics/durian.jpg", "description": "Tropical Smelly", "cost": 9.95 } ] }
在我们修改完设置后,我们可以选择退出应用。在下次启动应用后,我们可以选择“Load JSON”按钮,我们可以看到上次修改的内容被成功地重新装载到应用中。
整个项目的源码在: git clone https://gitcafe.com/ubuntu/savejson.git