12. 数据交换和存储

上一章讲到了Qt中文本的读写,涉及到部分的数据的存储, 这一章我们更加深入的来讲讲qt中的数据加载、交换和存储。

Qt提供对XML和JSON的支持

XML和JSON

可扩展标记语言,标准通用标记语言的子集,简称XML。是一种用于标记电子文件使其具有结构性的标记语言, XML被设计为传输和存储数据,其焦点是数据的内容,XML的本质为带标签的纯文本。Qt提供API来读取和解析XML流,以及写入这些流。

JSON是一种对源自Javascript的对象数据进行编码的格式,现在已广泛用作Internet上的数据交换格式。 Qt中的JSON支持提供了易于使用的C++ API来解析,修改和保存JSON数据。 它还支持以二进制格式保存此数据,该格式可以直接“mmap”访问,并且访问速度非常快。

XML和JSON主要用于数据交换,而数据主要存储在数据仓库,也就是我们通常所说的数据库。

数据库

数据库的概念实际包括两层意思:数据库既是用来存储数据的仓库,也是一个按数据结构来存储和管理数据的计算机软件系统。

  • 数据库是一个实体,它是能够合理保管数据的“仓库”,用户在该“仓库”中存放要管理的事务数据,“数据”和“库”两个概念结合成为数据库。

  • 数据库是数据管理的新方法和技术,它能更合适的组织数据、更方便的维护数据、更严密的控制数据和更有效的利用数据。

常用的数据库软件:Oracle、MySQL(MongoDB)、Microsoft SQL Server、SQlite。

虽说这些数据库软件各不相同,但是这些数据库都是使用SQL语句来进行数据的增删改查的。 Qt使用数据库插件的形式来支持不同的数据库软件,封装了统一的操作接口来对数据仓库中的数据进行操作。

12.1. XML

下面就是一段实际的xml数据:

embed_qt_develop_tutorial_code/app_bin/data/xml/books.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xbel>
<xbel version="1.0">
    <folder folded="no">
        <title>Qt Resources</title>
        <bookmark href="http://qt.io/">
            <title>Qt home page</title>
        </bookmark>
        <bookmark href="https://www.qt.io/partners/">
            <title>Qt Partners</title>
        </bookmark>
        <bookmark href="https://www.qt.io/qt-training/">
            <title>Training</title>
        </bookmark>
        <folder folded="yes">
            <title>Community Resources</title>
            <bookmark href="http://www.qtcentre.org/content/">
                <title>Qt Centre</title>
            </bookmark>
            <bookmark href="http://www.qtforum.org/">
                <title>QtForum.org</title>
            </bookmark>
        </folder>
    </folder>
</xbel>
  • 第一行是 XML 声明。它定义 XML 的版本 (1.0) 和所使用的编码 (UTF-8)。

  • 第二行标明了文档格式 xbel,XML 的一种。

  • 第三行标明了 xbel 版本,也是文档的根元素,xbel就是元素的标签,标签和关闭标签必须成对出现也就是有 <xbel> 就必须有 </xbel>

  • 后面以此就是xbel根元素的子元素。

XML 文档是一种树结构,它从“根元素”开始逐渐扩展到“子元素”,所有元素标签和关闭标签都必须成对出现; 标签对大小写敏感,标签可以拥有属性值,比如 version=”1.0”,属性值要加引号。

我们大概来了解一下XML的语法规则,参考 https://www.w3school.com.cn/xml/index.asp

Qt提供QXmlStreamReader和QXmlStreamWriter类来完成XML的基本读取解析和写入。

12.1.1. QXmlStreamReader

QXmlStreamReader提供了一个简单的流API来解析格式正确的XML。 这是先将完整的XML数据加载到DOM树中,数据来源可以是QIODevice也可以是QByteArray。

流读取器的基本概念是将XML文档作为令牌流(stream of tokens)报告,类似于SAX。 QXmlStreamReader和SAX之间的主要区别在于如何报告这些XML令牌。 使用SAX,应用程序必须提供处理程序(回调函数),以便在解析器方便时从解析器接收所谓的XML事件。 使用QXmlStreamReader,应用程序代码本身会驱动循环并根据需要从读取器中逐个提取令牌,通过调用readNext()来完成, 在该API中,读取器从输入流中读取数据,直到完成下一个标记为止,此时它返回tokenType()。

Qt提供了一组方便的函数(包括isStartElement()和text())来检查令牌,以获取有关已读取内容的信息。 这些API最大优点是可以使用它构建递归下降解析器,这意味着您可以轻松地将XML解析代码拆分为不同的方法或类。 这也使得在解析XML时很容易跟踪应用程序自身的状态。

QXmlStreamReader 应用示例
1
2
3
4
5
6
7
8
9
QXmlStreamReader xml;
...
while (!xml.atEnd()) {
    xml.readNext();
    ... // do processing
}
if (xml.hasError()) {
    ... // do error handling
}

QXmlStreamReader是一个良好的XML解析器1.0,它不包括外部解析实体。 只要没有错误发生,就可以确保应用程序代码确保流读取器提供的数据满足W3C的格式正确的XML标准。 例如,可以确定所有标签确实都正确嵌套和关闭,对内部实体的引用已被正确的替换文本替换, 并且属性已根据DTD的内部子集进行了规范化或添加。

12.1.2. QXmlStreamWriter

QXmlStreamWriter是对应QXmlStreamReader用来写入XML数据的类。 QXmlStreamWriter提供如下接口:

  • writeStartDocument() 开始写入XML数据,写入以XML版本号和编码信息;

  • writeEndDocument() 结束XML数据写入并关闭所有打开的标签;

  • writeStartElement() 开始写入标签;

  • writeAttribute() 和 writeTextElement() 写入标签内容;

  • writeEndElement() 结束标签写入。

QXmlStreamWriter 应用示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
QXmlStreamWriter stream(&output);
stream.setAutoFormatting((true);
stream.writeStartDocument();
......
stream.writeStartElement("bookmark");
stream.writeAttribute("href", "http://qt-project.org/");
stream.writeTextElement("title", "Qt Project");
stream.writeEndElement();// bookmark
......
stream.writeEndDocument();

12.2. JSON

我们先通过接口测试软件Postman,来访问天气接口(网上找的),看看究竟JSON格式数据长什么样子。

json001

最终返回数据如下,根据下面的数据,我们就大概知道了天气的情况:

json 数据
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{"data":
    {"yesterday":
        {"date":"30日星期五","high":"高温 31℃","fx":"南风","low":"低温 20℃","fl":"<![CDATA[2级]]>","type":"阴"},
        "city":"东莞","forecast":[
            {"date":"1日星期六","high":"高温 32℃","fengli":"<![CDATA[2级]]>","low":"低温 22℃","fengxiang":"南风","type":"多云"},
            {"date":"2日星期天","high":"高温 31℃","fengli":"<![CDATA[2级]]>","low":"低温 24℃","fengxiang":"南风","type":"多云"},
            {"date":"3日星期一","high":"高温 29℃","fengli":"<![CDATA[3级]]>","low":"低温 24℃","fengxiang":"东南风","type":"雷阵雨"},
            {"date":"4日星期二","high":"高温 29℃","fengli":"<![CDATA[3级]]>","low":"低温 24℃","fengxiang":"南风","type":"多云"},
            {"date":"5日星期三","high":"高温 30℃","fengli":"<![CDATA[2级]]>","low":"低温 24℃","fengxiang":"北风","type":"雷阵雨"}
        ],
        "ganmao":"感冒易发期,外出请适当调整衣物,注意补充水分。","wendu":"31"
        },
    "status":1000,
    "desc":"OK"
}

json相较XML更加轻量级,读写速度更快, JSON 语法是 JavaScript 对象表示法语法的子集,其语法规则如下:

  • 数据在名称/值对中

  • 数据由逗号分隔

  • 花括号保存对象

  • 方括号保存数组

Qt提供QJsonDocument类对JSON文档进行读取和写入。 首先QJsonDocument :: fromJson()将JSON文档从其基于文本的表示形式转换为QJsonDocument(toJson()将其转换回文本), 解析器非常快速高效,并将JSON转换为Qt使用的二进制表示形式 然后通过isArray()和isObject()查询文档是否包含数组或对象,并通过array()或object()检索文档中包含的数组或对象,然后对其进行读取或操作。

在示例程序中用了另外一个简单JSON类来处理JSON数据。

json 类 embed_qt_develop_tutorial_code/Data/Weather/src/json.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#ifndef JSON_H
#define JSON_H

#include <QVariant>
#include <QString>

namespace QtJson {
    typedef QVariantMap JsonObject;
    typedef QVariantList JsonArray;

    QVariant clone(const QVariant &data);
    void insert(QVariant &v, const QString &key, const QVariant &value);
    void append(QVariant &v, const QVariant &value);
    QVariant parse(const QString &json);
    QVariant parse(const QString &json, bool &success);
    QByteArray serialize(const QVariant &data);
    QByteArray serialize(const QVariant &data, bool &success);
    QString serializeStr(const QVariant &data);
    QString serializeStr(const QVariant &data, bool &success);
    void setDateTimeFormat(const QString& format);
    void setDateFormat(const QString& format);
    QString getDateTimeFormat();
    QString getDateFormat();

    class Object : public QVariant {
        template<typename T>
        Object& insertKey(Object* ptr, const QString& key) {
            T* p = (T*)ptr->data();
            if (!p->contains(key)) p->insert(key, QVariant());
            return *reinterpret_cast<Object*>(&p->operator[](key));
        }
        template<typename T>
        void removeKey(Object *ptr, const QString& key) {
            T* p = (T*)ptr->data();
            p->remove(key);
        }
    public:
        Object() : QVariant() {}
        Object(const Object& ref) : QVariant(ref) {}

        Object& operator=(const QVariant& rhs) {
            setValue(rhs);
            return *this;
        }
        Object& operator[](const QString& key) {
            if (type() == QVariant::Map)
                return insertKey<QVariantMap>(this, key);
            else if (type() == QVariant::Hash)
                return insertKey<QVariantHash>(this, key);

            setValue(QVariantMap());

            return insertKey<QVariantMap>(this, key);
        }
        const Object& operator[](const QString& key) const {
            return const_cast<Object*>(this)->operator[](key);
        }
        void remove(const QString& key) {
            if (type() == QVariant::Map)
                removeKey<QVariantMap>(this, key);
            else if (type() == QVariant::Hash)
                removeKey<QVariantHash>(this, key);
        }
    };
}

#endif //JSON_H

应用示例通过异步接收天气API返回的JSON数据,拿到JSON数据之后调用SltWeatherReply进行解析。 在该函数中,将对应的JSON对象、数组分别转换我们需要的类型,显示在UI上。

解析天气json数据 embed_qt_develop_tutorial_code/Data/Weather/src/weatherwidget.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
void WeatherWidget::SltWeatherReply(const QByteArray &jsonData)
{
    bool bOk = false;
    QtJson::JsonObject result = QtJson::parse(jsonData, bOk).toMap();
    if (bOk) {
        // 当前天气接口 http://wthrcdn.etouch.cn/weather_mini?city=%1
        if(result.value("desc").toString()!="OK")
        {
            qDebug() << "weather data error";
            return ;
        }

        QStringList strTodayData;
        QtJson::JsonObject jsonCity = result.value("data").toMap();
        QtJson::JsonObject jsonyesterday = jsonCity.value("yesterday").toMap();

        QString CityName = jsonCity.value("city").toString();
        QString Info = jsonCity.value("ganmao").toString();
        double Curtem = jsonCity.value("wendu").toDouble();

        strTodayData << CityName;
        strTodayData << jsonCity.value("wendu").toString();

        QtJson::JsonArray jsonForecast = jsonCity.value("forecast").toList();
        for (int i = 0; i < jsonForecast.size(); i++) {
                QtJson::JsonObject jsonObj = jsonForecast.at(i).toMap();
                QStringList strForeacast;
                strForeacast << jsonObj.value("date").toString();
                strForeacast << jsonObj.value("type").toString();

                strForeacast << getTemperature(jsonObj.value("high").toString(),jsonObj.value("low").toString());

                // 添加当天的天气
                if (0 == i) {
                    strTodayData << QDateTime::currentDateTime().toString("dddd");
                    strTodayData << jsonObj.value("type").toString();
                    strTodayData << getTemperature(jsonObj.value("high").toString(),jsonObj.value("low").toString());

                    m_weatherView->setWeatherData(strTodayData);
                }

                m_weatherItems.insert(i, new QtListWidgetItem(i, strForeacast));
            }

        m_labelDate->setText(QTime::currentTime().toString("hh:mm") + tr("更新"));
        m_weatherReport->SetItems(m_weatherItems);
    }
    else {
        qDebug() << "get weather failed";
    }
}

上面提到的天气数据就是存储在数据库中的, 我们访问天气API,实际上就是向服务器程序发起请求,服务器程序接收到请求,再去访问数据库得到数据, 然后再将数据包装成JSON格式,返回给我们。

实际上,我们的程序也可以直接访问数据库,通过SQL语句直接操纵数据库的数据。 当然市面上存在各种各样的数据库,Qt不可能每一种数据库都去写封装一套操作库,所以Qt就以数据库插件的形式来拓展对数据库的管理。

通过插件呢我们就可以直接读写数据库了。数据库的知识还是蛮多的我们就单独写了一章, Qt使用SQLite

12.3. 例程说明

野火提供的Qt Demo已经开源,仓库地址在:

文档所涉及的示例代码也提供下载,仓库地址在:

本章例程在 embed_qt_develop_tutorial_code/Data 下面 XMLWeather

12.3.1. 编程思路

本章包含多个例程,分别演示XML、JSON格式数据的读写。

XML例程为Qt官方例程,工程下面有5个子程序。

  • dombookmarks 演示了QDomDocument读写XBEL文件;

  • streambookmarks 演示了QXmlStreamReader、QXmlStreamWriter读写XBEL文件;

  • htmlinfo 演示了解析html文件,输出标签’title’、’a’、’p’的内容;

  • rsslisting 演示了从网络获取rss.xml文件并解析;

  • xmlstreamlint 演示了输出xml流。

Weather为野火demo获取天气示例程序。

程序通过QNetworkAccessManager访问某天气接口,获取天气数据。 天气数据为JSON格式,调用JSON类解析数据最终显示在UI上。

12.3.2. 代码讲解

12.3.3. 编译构建

build001
  • Ubuntu 选择 Desktop Qt 5.11.3 GCC 64bit 套件,编译运行

  • LubanCat 选择 ebf_lubancat,只编译

提示

当两种构建套件进行切换时,请重新构建项目或清除之前项目。针对我们的工程还需要手动重新构建QtUI和Skin。

build002

12.3.4. 运行结果

12.3.4.1. PC实验

直接点击编译并运行程序,结果如下:

result001

12.3.4.2. LubanCat实验

通过SCP或者NFS将编译好的程序拷贝到LubanCat上

NFS环境搭建 参考这里

SCP命令如下:

# scp传输文件
# scp 文件名 服务器上的某个用户@服务器ip:/文件保存路径
scp filename server_user_name@192.168.0.205:server_file_path
# 从服务器拉取文件
# scp 服务器上的某个用户@服务器ip:/服务器文件存放路径 拉取文件保存路径
scp server_user_name@192.168.0.229:server_file_path local_path

编译好的程序在 embed_qt_develop_tutorial_code/app_bin/data 目录中,通过scp命令将编译好的程序拉到LubanCat。

scp root@192.168.0.174:/home/embed_qt_develop_tutorial_code/app_bin/data/Weather /usr/local/qt-app/

在LubanCat运行程序,使用run_myapp.sh配置好环境,并执行 Weather 。

sudo /usr/local/qt-app/run_myapp.sh /usr/local/qt-app/Weather
result002