基于Qt的天气预报项目

该项目就是通过 HTTP 接口访问 HTTP 服务器,获取天气数据,然后展示出来

1.整体效果、技术点

本章实现的天气预报项目,整体效果如下:

img

在左上角输入要查询的城市,然后点击查询按钮,就会发送 HTTP请求给服务器,请求回来的天气数据 JSON 格式

通过解析 JSON 可以获取以下信息:

  • 今天的信息
    • 温度、湿度、风向、风力、天气类型(晴、多云、小雨等)、PM2.5、温馨提示、感冒指数、日出日落
  • 未来15天的信息
    • 日期、星期、天气类型(晴、多云、小雨等)、PM25、最高温、最低温

该项目涉及的技术点如下:

1.1样式表的设置

合理地使用样式表,可以使界面更加美观,这里设置的样式表如下:

  • 背景图片
    • 为整个窗体设置一张背景图片
  • 背景色
    • 设置控件背景透明,或者设置一个透明度
  • 圆角
    • 为控件设置圆角
  • 字体颜色和大小
    • 为控件设置合适的字体颜色和字体大小

1.2JSON 数据格式

HTTP 服务端返回的天气数据,是 JSON 格式

使用 Qt 提供的,解析 JSON 相关的类,可以很方便地解析出其中的字段

1.3HTTP 请求

根据 HTTP 服务端提供的接口,发送 HTTP 请求,获取天气数据

1.4事件

为了界面的美观,我们将窗口设置为无标题栏,这样就无法通过右上角的【关闭】按钮,退出程序。

因此增加了右键菜单退出的功能

还重写了鼠标移动事件,让窗口可以跟随鼠标移动

1.5绘图

绘制高低温曲线,根据每天高低温数据,可以绘制一个曲线,更直观地展示温度变化趋势

1.6资源文件

根据不同的天气类型,还可以用不同的图标进行展示,更加直观

而这些图标通常会放到资源文件中,这样它们可以一同被打包进 Qt 的可执行程序中

2.JSON 基础知识

2.1什么是 JSON?

2.1.1 JSON 的引入

在讲解 JSON 之前, 首先想这么一种场景:

通常,客户端和服务端要进行通信,那么通信采用什么数据格式呢?

比如 C++ 写的服务端,创建了一个 Person 对象:

class Person {
    string name;
    string gender;
    int age;
};

怎么将服务端创建的 Person 对象,传递到客户端呢?

直接传 Person 对象肯定是不合适的,因为客户端可能甚至不是 C++ 写的,可能是 Java 写的,Java 不认识 C++ 中的对象。

更有甚者,客户端是一个单片机的设备,是用 C 语言写的,C 语言是面向过程的,压根就没有类和对象的概念

此时就需要一种通用的数据格式,JSON 就应运而生了。

JSON(JavaScript Object Notation),中文名 JS对象表示法,因为它和 JS 中的对象的写法很类似

通常说的 JSON,其实就是 JSON 字符串,本质上是一种特殊格式的字符串。

JSON 是一种轻量级的数据交换格式,客户端和服务端数据交互,基本都是 JSON 格式的

2.1.2JSON 的特点

相对于其他数据传输格式,JSON 有以下特点

(1)便于阅读和书写

除了 JSON 格式,还有一种数据传输格式 XML,相对于 XMLJSON更加便于阅读和书写

(2)独立于编程语言

JSON 完全独立于编程语言,并不是说名字里有 JavaScript,就只能在 JavaScript 中使用,不是这样的

JSONJavaScript 的关系,就类似于:雷锋和雷峰塔、周杰和周杰伦、范伟和范玮琪,可以说没啥关系

几乎在所有的编程语言和开发环境中,都有解析和生成 JSON 字符串的库,比如:

常见的 JSON

C 语言

Jansson、cJSON

C++

jsonCpp、JSON for Modern C++

✅ Java

json-lib、org-json

✅ Android

GSON、FastJson

QT

QJsonXxx

(3)网络传输的标准****数据格式

基于以上的特点,JSON 成为网络传输使用率最高的数据格式

2.2JSON 的两种****数据格式

JSON 有两种数据格式

  • JSON 对象
  • JSON 数组

规则:被大括号包裹的是 JSON 对象; 被中括号包裹的是 JSON 数组

2.2.1JSON 数组

JSON 数组格式

[元素1, 元素2, 元素3, ... 元素n]

类似于 C/C++ 中的数组,元素之间以逗号分隔

不同的是,JSON 数组中的元素可以是不同的数据类型,包括:整形、 浮点、 字符串、 布尔类型、 JSON 数组、 JSON 对象、空值

(1)****JSON 数组中的元素是同一类型

// 元素类型都是数字
[1, 2, 3, 4]

// 元素类型都是字符串
["Spring", "Summer", "Autumn", "Winter"]

(2)****JSON 数组中的元素是不同类型

// 元素类型分别是:整型、浮点、字符串、布尔、空置
[1, 2.5, "hello", true, false, null]

(3)****JSON 数组的嵌套

// 数组中嵌套数组
[
    [1, 2, 3, 4],
    ["Spring", "Summer", "Autumn", "Winter"],
    [1, 2.5, "hello", true, false, null]
]

(4)****JSON 数组嵌套 JSON 对象

// 数组中嵌套对象
[
    {
        "name": "Tom", 
        "age": 18, 
        "gender": "male"
    }, 
    {
        "name": "Tom", 
        "age": 18, 
        "gender": "male"
    }
]

2.2.2JSON 对象

JSON 对象格式

{ "key1" : value1, "key2" : value2, "key3" : value3 }

JSON 对象内部使用键值对的方式来组织;

键和值之间使用冒号分隔,多个键值之间使用逗号分隔;

键是字符串类型,值的类型可以是:整形、 浮点、 字符串、 布尔类型、 JSON 数组、 JSON 对象、 空值

(1)最简单的 JSON 对象

{
    "name": "Tom", 
    "age": 18, 
    "gender": "male"
}

(2)****JSON 对象和 JSON 数组嵌套

JSON 对象中,还可以嵌套 JSON 对象和 JSON 数组

{
    "name": "China",
    "info": {
        "capital": "beijing",
        "asian": true,
        "founded": 1949
    },
    "provinces": [{
        "name": "shandong",
        "capital": "jinan"
    }, {
        "name": "zhejiang",
        "capital": "hangzhou"
    }]
}

2.3 JSON 在线解析

JSON 本质就是一种特殊格式的字符串

实际工作中,这个 JSON 字符串可能是自己手写的,也可能是来自网络接收的数据

如下是一段 JSON 字符串,它可能是我们自己写的,也可能是服务端返回的

它是压缩格式,也就是没有换行和缩进,不方便判断格式是否正确

{"name":"China","info":{"capital":"beijing","asian":true,"founded":1949},"provinces":[{"name":"shandong","capital":"jinan"},{"name":"zhejiang","capital":"hangzhou"}]}

那么有没有一种简单的方式,来校验这个 JSON 的格式是否正确呢?

答:JSON 在线解析工具

在浏览器中,搜索【JSON 在线解析】,有很多网站提供 JSON 在线解析功能,如下:

img

3.Qt 中使用 JSON

Qt 5.0 开始提供了对 JSON 的支持,使用 Qt 提供的 JSON 类,可以很方便地生成 JSON 字符串,以及解析 JSON 字符串。

3.1JSON 相关的类

Qt 提供的与 JSON 相关的类,主要有四个:

  • QJsonObject
  • QJsonArray
  • QJsonValue
  • QJsonDocument

下面依次介绍这几个类

3.1.1 QJsonObject

QJsonObject 封装了 JSON 中的对象,可以存储多个键值对。

其中,键为字符串类型,值为 QJsonValue 类型。

  • 创建一个 QJsonObject 对象
QJsonObject::QJsonObject();
  • 将键值对添加到 QJsonObject 对象中
QJsonObject::iterator insert(const QString &key, const QJsonValue &value);
  • 获取 QJsonObject 对象中键值对的个数
int QJsonObject::count() const;
int QJsonObject::size() const;
int QJsonObject::length() const;
  • 通过 key 得到 value
QJsonValue QJsonObject::value(const QString &key) const;
QJsonValue QJsonObject::operator[](const QString &key) const;
  • 检查 key 是否存在
iterator QJsonObject::find(const QString &key);
bool QJsonObject::contains(const QString &key) const;
  • 遍历 key
QStringList QJsonObject::keys() const;

3.1.2 QJsonArray

QJsonArray 封装了 Json 中的数组。数组中元素的类型统一为 QJsonValue 类型

  • 创建一个 QJsonArray
QJsonArray::QJsonArray();
  • 添加数组元素
// 添加到头部和尾部
void QJsonArray::append(const QJsonValue &value);
void QJsonArray::prepend(const QJsonValue &value);

// 插入到 i 的位置之前
void QJsonArray::insert(int i, const QJsonValue &value); 

// 添加到头部和尾部
void QJsonArray::push_back(const QJsonValue &value);
void QJsonArray::push_front(const QJsonValue &value);
  • 获取 QJsonArray 中元素个数
int QJsonArray::count() const;
int QJsonArray::size() const;
  • 获取元素的值
// 获取头部和尾部
QJsonValue QJsonArray::first() const;
QJsonValue QJsonArray::last() const;

// 获取指定位置
QJsonValue QJsonArray::at(int i) const;
QJsonValueRef QJsonArray::operator[](int i);
  • 删除元素
// 删除头部和尾部
void QJsonArray::pop_back();
void QJsonArray::pop_front();

void QJsonArray::removeFirst();
void QJsonArray::removeLast();

// 删除指定位置
void QJsonArray::removeAt(int i);
QJsonValue QJsonArray::takeAt(int i);

3.1.3 QJsonValue

它封装了 JSON 支持的六种数据类型,分别为:

数据类型 Qt 类
布尔类型 QJsonValue::Bool
浮点类型(包括整形) QJsonValue::Double
字符串类型 QJsonValue::String
数组类型 QJsonValue::Array
对象类型 QJsonValue::Object
空值类型 QJsonValue::Null

可以通过以下方式构造 QJsonValue 对象

// 字符串
QJsonValue(const char *s);
QJsonValue(QLatin1String s);
QJsonValue(const QString &s);

// 整形 and 浮点型
QJsonValue(qint64 v);
QJsonValue(int v);
QJsonValue(double v);

// 布尔类型
QJsonValue(bool b);

// Json对象
QJsonValue(const QJsonObject &o);

// Json数组
QJsonValue(const QJsonArray &a);

// 空值类型
QJsonValue(QJsonValue::Type type = Null);

如果已经得到了一个 QJsonValue 对象,如何判断其内部封装的是什么类型的数据呢?

答:使用以下判断函数

// 是否是字符串类型
bool isString() const;

// 是否是浮点类型(整形也是通过该函数判断)
bool isDouble() const;

// 是否是布尔类型
bool isBool() const;

// 是否是Json对象
bool isObject() const;

// 是否是Json数组
bool isArray() const;

// 是否是未定义类型(无法识别的类型)
bool isUndefined() const;

// 是否是空值类型
bool isNull() const;

通过以上判断函数,获取到其内部数据的实际类型之后,如果有需求就可以再次将其转换为对应的基础数据类型,对应的 API 函数如下:

// 转换为字符串类型
QString toString() const;
QString toString(const QString &defaultValue) const;

// 转换为浮点类型
double toDouble(double defaultValue = 0) const;
// 转换为整形
int toInt(int defaultValue = 0) const;

// 转换为布尔类型
bool toBool(bool defaultValue = false) const;

// 转换为Json对象
QJsonObject toObject(const QJsonObject &defaultValue) const;
QJsonObject toObject() const;

// 转换为Json数组
QJsonArray toArray(const QJsonArray &defaultValue) const;
QJsonArray toArray() const;

3.1.4 QJsonDocument

它封装了一个完整的 JSON 文档。

它可以从 UTF-8 编码的基于文本的表示,以及 Qt 本身的二进制格式,读取和写入该文档。

QJsonObjectQJsonArray 这两个对象是不能直接转换为字符串类型的,需要通过 QJsonDocument 类来完成二者的转换

下面介绍转换的步骤

(1)****QJsonObject / QJsonArray => 字符串

// 1. 创建 QJsonDocument 对象
// 以 QJsonObject 或者 QJsonArray 为参数来创建 QJsonDocument 对象
QJsonDocument::QJsonDocument(const QJsonObject &object);
QJsonDocument::QJsonDocument(const QJsonArray &array);

// 2. 将 QJsonDocument 对象中的数据进行序列化
// 通过调用 toXXX() 函数就可以得到文本格式或者二进制格式的 Json 字符串了。
QByteArray QJsonDocument::toBinaryData() const;                            // 二进制格式的json字符串
QByteArray QJsonDocument::toJson(JsonFormat format = Indented) const;      // 文本格式

// 3. 使用得到的字符串进行数据传输,或者保存到文件

(2)字符串 => QJsonObject / QJsonArray

通常,通过网络接收或者读取磁盘文件,会得到一个 JSON 格式的字符串,之后可以按照如下步骤,解析出 JSON 字符串中的一个个字段

// 1. 将 JSON 字符串转换为 QJsonDocument 对象
[static] QJsonDocument QJsonDocument::fromBinaryData(const QByteArray &data, DataValidation validation = Validate);
[static] QJsonDocument QJsonDocument::fromJson(const QByteArray &json, QJsonParseError *error = Q_NULLPTR);

// 2. 将文档对象转换为 json 数组 / 对象

// 2.1 判断文档对象中存储的数据,是 JSON 数组还是 JSON 对象
bool QJsonDocument::isArray() const;
bool QJsonDocument::isObject() const
    
// 2.2 之后,就可以转换为 JSON 数组或 JSON 对象
QJsonObject QJsonDocument::object() const;
QJsonArray QJsonDocument::array() const;

// 3. 调用 QJsonArray / QJsonObject 类提供的 API 获取存储在其中的数据

3.2 构建 JSON 字符串

在网络传输时,通常是传输的 JSON 字符串

传输之前,首先要生成 JSON 字符串,接下来使用 Qt 提供的工具类,来生成如下格式的 JSON 字符串

{
    "name": "China",
    "info": {
        "capital": "beijing",
        "asian": true,
        "founded": 1949
    },
    "provinces": [{
        "name": "shandong",
        "capital": "jinan"
    }, {
        "name": "zhejiang",
        "capital": "hangzhou"
    }]
}

接下来封装一个 writeJson() 函数实现以上功能,如下:

void writeJson() {
    QJsonObject rootObj;

    // 1. 插入 name 字段
    rootObj.insert("name", "China");

    // 2. 插入 info 字段
    QJsonObject infoObj;
    infoObj.insert("capital", "beijing");
    infoObj.insert("asian", true);
    infoObj.insert("founded", 1949);
    
    rootObj.insert("info", infoObj);

    // 3. 插入 prvince 字段
    QJsonArray provinceArray;

    QJsonObject provinceSdObj;
    provinceSdObj.insert("name", "shandong");
    provinceSdObj.insert("capital", "jinan");

    QJsonObject provinceZjObj;
    provinceZjObj.insert("name", "zhejiang");
    provinceZjObj.insert("capital", "hangzhou");

    provinceArray.append(provinceSdObj);
    provinceArray.append(provinceZjObj);

    rootObj.insert("provinces", provinceArray);

    // 4.转换为 json 字符串
    QJsonDocument doc(rootObj);
    QByteArray json = doc.toJson();

    // 5.1 打印json字符串
    qDebug() << QString(json).toUtf8().data();

    // 5.2 将json字符串写到文件
    QFile file("d:\\china.json");
    file.open(QFile::WriteOnly);
    file.write(json);
    file.close();
}

运行程序,就可以在控制台输出构建出的 JSON 字符串,如下:

img

并且,将生成的 JSON 字符串,保存到了 china.json 文件:

img

3.3 解析 JSON 字符串

通常接收网络数据的格式是 JSON 格式

在接收完毕之后,需要解析出其中的每一个字段,根据各个字段的值做相应的显示或者其他处理

接下来使用 Qt 提供的工具类,读取文件中的 JSON 字符串,把其中的字段解析出来:

文件内容:

{
    "name": "China",
    "info": {
        "capital": "beijing",
        "asian": true,
        "founded": 1949
    },
    "provinces": [{
        "name": "shandong",
        "capital": "jinan"
    }, {
                "name": "zhejiang",
                "capital": "hangzhou"
    }]
}

接下来封装一个 fromJson() 函数实现以上功能,如下:

void fromJson() {
    QFile file("d:\\china.json");
    file.open(QFile::ReadOnly);
    QByteArray json = file.readAll();
    file.close();

    QJsonDocument doc = QJsonDocument::fromJson(json);
    if (!doc.isObject()) {
        qDebug() << "not an object";
        return;
    }

    QJsonObject obj = doc.object();
    QStringList keys = obj.keys();
    for (int i = 0; i < keys.size(); ++i) {
        // 获取key-value
        QString key = keys[i];
        QJsonValue value = obj.value(key);

        if (value.isBool()) {
            qDebug() << key << ":" << value.toBool();
        } else if (value.isString()) {
            qDebug() << key << ":" << value.toString();
        } else if (value.isDouble()) {
            qDebug() << key << ":" << value.toInt();
        } else if (value.isObject()) {
            qDebug() << key << ":";

            QJsonObject infoObj = value.toObject();
#if 0
            // 简便起见,直接使用键来取值
            QString capital = infoObj["capital"].toString();
            bool asian = infoObj["asian"].toBool();
            int founded = infoObj["founded"].toInt();

            qDebug() << "    "
                     << "capital"
                     << ":" << capital;
            qDebug() << "    "
                     << "asian"
                     << ":" << asian;
            qDebug() << "    "
                     << "founded"
                     << ":" << founded;
#else
            QStringList infoKeys = infoObj.keys();
            for (int i = 0; i < infoKeys.size(); i++) {
                QJsonValue v = infoObj.value(infoKeys[i]);
                if (v.isBool()) {
                    qDebug() << "    " << infoKeys[i] << ":" << v.toBool();
                } else if (v.isDouble()) {
                    qDebug() << "    " << infoKeys[i] << ":" << v.toInt();
                } else if (v.isString()) {
                    qDebug() << "    " << infoKeys[i] << ":" << v.toString();
                }
            }
#endif
        } else if (value.isArray()) {
            qDebug() << key;

            QJsonArray provinceArray = value.toArray();
#if 1
            // 简便起见,直接使用键来取值
            for (int i = 0; i < provinceArray.size(); i++) {
                QJsonObject provinceObj = provinceArray[i].toObject();

                QString name = provinceObj["name"].toString();
                QString capital = provinceObj["capital"].toString();

                qDebug() << "    "
                         << "name"
                         << ":" << name << ", capital"
                         << ":" << capital;
            }
#else
            for (int i = 0; i < provinceArray.size(); i++) {
                QJsonObject provinceObj = provinceArray[i].toObject();
                QStringList provinceKeys = provinceObj.keys();
                for (int j = 0; j < provinceKeys.size(); j++) {
                    // 简便起见,不再进行类型判断
                    QJsonValue infoValue = provinceObj.value(provinceKeys[j]);
                    qDebug() << "   " << provinceKeys[j] << ":" << infoValue.toString();
                }
            }
#endif
        }
    }
}

运行程序,在控制台输出解析出的字段

img

4.HTTP 基础知识

4.1 HTTP 必知必会

既然是实现 HTTP 协议的天气预报,那么 HTTP 相关的知识,必须掌握

HTTP:超文本传输协议(HyperText Transfer Protocol)

HTTP 是浏览器端 Web 通信的基础。

关于 HTTP 的介绍,不论是书本还是网上的博客,介绍的过于学术化,这里以实战出发,介绍几个关键的点:

4.1.1 两种架构

  • B/S 架构
    • Browser/Server,浏览器/服务器架构
    • B:浏览器,比如 FirefoxInternet ExplorerGoogle ChromeSafariOpera
    • S:服务器,比如 Apachenginx
  • C/S 架构

Client/Server,客户端/服务器架构

B/S 架构相对于 C/S 架构,客户机上无需安装任何软件,使用浏览器即可访问服务器

因此,越来越多的 C/S 架构正被 B/S 架构所替代

4.1.2 基于请求响应的模式

img

HTTP 协议永远都是客户端发起请求,服务器做出响应

也就是说,请求必定是先从客户端发起的,服务器端在没有接收到请求之前不会发送任何响应

这就无法实现这样一种场景:服务端主动推送消息给客户端

4.1.3 无状态

当浏览器第一次发送请求给服务器时,服务器做出了响应;

当浏览器第二次发送请求给服务器时,服务器同样可以做出响应,但服务器并不知道第二次的请求和第一次来自同一个浏览器

也就是说,服务器是不会记住你是谁的,所以是无状态的。

而如果要使 HTTP 协议有状态,就可以使浏览器访问服务器时,加入 cookie

这样,只要你在请求时有了这个 cookie,服务器就能够通过 cookie 知道,你就是之前那个浏览器

这样的话,HTTP 协议就有状态了。

4.1.4 请求报文

请求报文由四部分组成:

请求行 + 请求头(请求首部字段) + 空行 + 实体

(1)请求行

请求行里面有:

  • 请求方法:比如 GETPOST
  • 资源对象(URI
  • 协议名称和版本号(HTTP/1.1
POST /custom/a2873925c34ecbd2/web/cstm?stm=1649149234039 HTTP/1.1

POST                                                    即请求方法
/custom/a2873925c34ecbd2/web/cstm?stm=1649149234039     即 URL
HTTP/1.1                                                即协议和版本

(2)请求头

请求首部字段,请求头由于告诉服务器该请求的一些信息,起到传递额外信息的目的

Host: api.growingio.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Content-Length: 264
Origin: https://leetcode-cn.com
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: cross-site
Referer: https://leetcode-cn.com/

(3)空行

用来区分开请求头和请求实体

(4)请求实体

请求实体即真正所需要传输的数据

4.1.5 响应报文

响应报文同样是由四部分组成:

状态行 + 响应头 + 空行 + 消息体

(1)状态行

状态行主要由三部分组成:

  • HTTP 版本
  • 状态码(表示相应的结果)
  • 原因短语(解释)
HTTP/2 200 OK

HTTP/2         协议和版本
200            状态码 - 200代表OK,表示请求成功,404代表NOT FOUND,表示请求失败,所请求资源未在服务器上发现。
OK             原因短语

(2)响应头

响应报文首部,和请求报文首部一样,响应报文首部同样是为了传递额外信息,例如:

date: Tue, 05 Apr 2022 10:48:17 GMT              // 响应时间
content-type: application/json                   // 响应格式
content-length: 127                              // 长度
strict-transport-security: max-age=31536000
X-Firefox-Spdy: h2

(3)空行

用来区分开响应实体和响应首部

(4)响应实体

真正存储响应信息的部分

4.1.6 请求方式

HTTP` 常用的请求方式有很多中,最常用的是 `GET` 和 `POST

二者最主要的区别就是:

GET 请求的参数位于 URL 中,会显示在地址栏上

POST 请求的参数位于 request body 请求体中。

因此,GET 请求的安全性不如 POST 请求,并且 GET 请求的参数有长度限制,而 POST 没有

4.2 Postman

HTTP 包含客户端和服务端,在讲解 Postman 之前,试想下面的两种情况

  • 服务端开发完毕,而客户端还未完成开发。此时服务端开发人员能否对自己写的服务端程序进行测试呢?
  • 客户端开发人员访问服务端出错,比如无法访问服务端,有没有第三方的测试工具做进一步验证呢?

此时,就用到 Postman

Postman 是一个接口测试工具,使用 HTTP 开发的人几乎是无人不知的

它主要是用来模拟各种 HTTP 请求的(比如 GET 请求、POST 请求等)

在做接口测试的时候,Postman 相当于客户端,它可模拟用户发起的各类 HTTP 请求,将请求数据发送至服务端,并获取对应的响应结果

接下来,以获取天气北京的天气为例,来介绍 Postman 的基本使用

4.2.1 安装

首先要安装 Postman

登录 www.postman.com 根据自己的系统,下载对应的版本,并一步步傻瓜式安装即可

安装成功后,会提示要求注册一个账号:

img

在此,既可以根据提示免费注册一个账号,也可以不注册,点击左下角的【Skip】直接忽略即可

4.2.2 发送请求、获取响应

安装成功后,就可以使用它来发送请求了

这里以获取北京的天气为例

获取北京天气的 URL 为:http://t.weather.itboy.net/api/weather/city/101010100

其中,101010100 是北京的城市编码,是 9 位的

img

上图的序号就是操作的具体步骤:

① 点击 “+”,即可打开一个用于发送请求的标签页

② 选择请求的方法(GET/POST),并输入请求的 URL

③ 点击【Params】,可以添加参数

④ 点击【Headers】,可以添加请求头

⑤ 点击【Send】,发送该请求到服务器

Body,响应体,也就是服务器返回的实际数据。响应体中选择右侧的【JSON】格式,并选择【Pretty】,可以对 JSON 数据做美化显示

Headers,响应头,也就是服务器返回的响应头数据

5 Qt 实现 HTTP 请求

以上是使用 Postman 来发送 HTTP 请求,那么在 Qt 中如何实现 HTTP 请求呢?

Qt 框架提供了专门的类,可以方便地实现 HTTP 请求

5.1 创建 “网络访问管理” 对象

首先需要创建一个 QNetworkAccessManager 对象,这是 Qt 中进行 HTTP 请求的开端

mNetAccessManager = new QNetworkAccessManager(this);

5.2 关联信号槽

在发送 HTTP 请求之前,先关联信号槽

// 获取到数据之后
connect(mNetAccessManager, &QNetworkAccessManager::finished, this, &MainWindow::onReplied);

当请求结束,获取到服务器的数据时,mNetAccessManager 会发射一个 finished 信号,该信号携带一个 QNetworkReply 的参数

服务器返回的所有数据就封装在其中,通过 QNetworkReply 类提供的各种函数,就可以获取响应头,响应体等各种数据。

5.3 发送请求

接下来就可以发送 HTTP 请求了

QUrl url("http://t.weather.itboy.net/api/weather/city/101010100");
mNetAccessManager->get(QNetworkRequest(url));

根据请求的地址构建出一个 QUrl 对象,然后直接调用 QNetworkAccessManagerget 函数,即可发送一个 GET 请求

5.4 接收数据

由于上面绑定了信号槽,服务器返回数据后,自动调用我们自定义的槽函数 onReplied

如下是 onReplied 函数的标准写法:

void MainWindow::onReplied(QNetworkReply* reply)
{
    // 响应的状态码为200, 表示请求成功
    int status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();

    qDebug() << "operation:" << reply->operation();         // 请求方式
    qDebug() << "status code:" << status_code;              // 状态码
    qDebug() << "url:" << reply->url();                     // url
    qDebug() << "raw header:" << reply->rawHeaderList();    // header

    if ( reply->error() != QNetworkReply::NoError || status_code != 200 ) {
        QMessageBox::warning(this, "提示", "请求数据失败!", QMessageBox::Ok);
    } else {
        //获取响应信息
        QByteArray reply_data = reply->readAll();
        QByteArray byteArray  = QString(reply_data).toUtf8();
        qDebug() << "read all:" << byteArray.data();

        // parseJson()
    }

    reply->deleteLater();
}

可见,QNetworkReply 中封装了服务器返回的所有数据,包括响应头、响应的状态码、响应体等。

6 新建工程,右键退出

本节课开始,从零一步步实现这个天气预报项目

6.1 新建工程

新建一个工程,命名为 WeatherForcast,窗口类继承自 QMainWindow,如下:

img

6.2 添加资源文件

由于后边会窗口背景图片,以及各种天气的图标,这里创建一个资源文件,把这些图片图标添加到工程中来

在工程中添加资源文件之前已经详细讲解过了,这里不再赘述

添加之后的效果如下:

img

6.3 设置窗口固定大小,无边框

首先,在属性窗口中,设置窗口的大小 800*450

img

然后,在代码中增加以下两行代码,即可设置窗口为固定大小,且无边框:

MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    setWindowFlag(Qt::FramelessWindowHint);    // 无边框
    setFixedSize(width(), height());           // 固定窗口大小
}

6.4 右键退出

由于没有了边框,就无法点击右上角的 × 退出程序,因此添加右键菜单的退出功能

要实现右键菜单,只需重写父类的 contextMenuEvent 虚函数即可:

void QWidget::contextMenuEvent(QContextMenuEvent *event)

具体步骤为:

首先,在 mainwidow.h 头文件中,声明该虚函数,并添加菜单

class MainWindow : public QMainWindow {
    
protected:
    void contextMenuEvent(QContextMenuEvent* event);
    
private:
    QMenu*   mExitMenu;    // 右键退出的菜单
    QAction* mExitAct;     // 退出的行为
};

然后,来到 mainwindow.cpp 源文件,做如下实现:

#include <QContextMenuEvent>

MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
    // 右键菜单:退出程序
    mExitMenu = new QMenu(this);
    mExitAct  = new QAction();
    mExitAct->setText(tr("退出"));
    mExitAct->setIcon(QIcon(":/res/close.ico"));
    mExitMenu->addAction(mExitAct);

    connect(mExitAct, &QAction::triggered, this, [=]() {
        qApp->exit(0);
    });
}

// 重写父类的虚函数
// 父类中默认的实现是忽略右键菜单事件
// 重写之后,处理右键菜单事件
void MainWindow::contextMenuEvent(QContextMenuEvent* event)
{
    mExitMenu->exec(QCursor::pos());
    
    // 调用accept 表示,这个事件我已经处理,不需要向上传递了
    event->accept();
}

此时,在窗口上右键,就可以弹出右键菜单,并且点击其中的退出菜单项可以退出程序

7 界面布局

上一节的窗口,还是空白的没有任何的控件,本节拖拽对应的控件,完成界面的布局,最终布局效果:

界面布局的过程比较繁琐,需要花很多的时间来调整,我就不带大家从零布局了,布局的源码会提供给大家。

但是把一些需要注意的点列出如下:

7.1 删除菜单栏、状态栏

本项目不需要菜单栏、状态栏,删除即可

7.2 布局结构

centralWidget 下再添加一个 widget,之后所有的控件都是放在这个 widget 窗口之下

img

7.3 设置窗口背景

设置 widget 样式表,如下:

QWidget#widget{
        border-image: url(:/res/background.png);
}

QLabel {
        font: 25 10pt "微软雅黑";
        border-radius: 4px;
        background-color: rgba(60, 60, 60, 100);
        color: rgb(255, 255, 255);
}

这样:

  • 整个窗口的背景就设置为资源文件中的 background.png
  • 设置了 widget 窗口中的所有标签的字体、边框圆角、背景色、前景色
  • 如果要单独设置某个标签的样式,直接将要设置的样式,设置到对应标签上即可
    • 比如 “空气质量” 的 “优” 的标签的样式被单独设置为:background-color: rgba(0, 255, 255, 100);

7.4 栅格****布局

这里用到了多个栅格布局:

  • “今天” 的湿度、PM2.5、风力风向、空气质量
  • “六天” 的天气类型中的图标和文本、星期和日期的文本

7.5 标签显示图片

有两种方式设置标签显示图片:

  • 属性窗口
  • 代码动态设置图片

属性窗口中,设置图片的方法如下:

img

当然,对于 “天气类型”,比如:阴、晴、小雨、中雨等,需要在代码中,根据服务器返回的数据,来动态设置

7.6 弹簧

为了界面的对齐和美观,可以添加一些水平和垂直的弹簧

7.7 修改控件名称

为了便于在代码中引用对应的控件,修改各控件名字,见名知义

8 窗口随鼠标移动

由于窗口无边框,无法通过标题栏来移动窗口。但是可以重写父类的鼠标事件,来实现窗口随鼠标移动

首先,在 mainwindow.h 声明两个鼠标事件

class MainWindow : public QMainWindow {
    
protected:
    void mousePressEvent(QMouseEvent* event);
    void mouseMoveEvent(QMouseEvent* event);
    
private:
    QPoint   mOffset;      // 窗口移动时, 鼠标与窗口左上角的偏移
};

然后,在 mainwindow.cpp 中,实现如下:

void MainWindow::mousePressEvent(QMouseEvent* event)
{
    qDebug() << "窗口左上角:" << this->pos() << ", 鼠标坐标点:" << event->globalPos();
    mOffset = event->globalPos() - this->pos();
}

void MainWindow::mouseMoveEvent(QMouseEvent* event)
{
    this->move(event->globalPos() - mOffset);
}

注意:

move() 函数移动窗口时,其实是设置窗口左上角的位置,因此上面定义了一个全局变量 mOffset,来记录鼠标按下的位置

9 请求天气数据

上面已经讲解了 Qt 中实现 HTTP 请求的类,这里就开始真正使用这些类, 来请求网络的数据。

9.1 添加网络模块

如果要使用 HTTP 获取数据,需要在 .pro 文件中,添加网络模块:

QT += core gui network

9.2 声明网络对象

mainwindow.h 的头文件中,声明用于 HTTP 通信的 QNetworkAccessManager 指针对象

#include <QNetworkAccessManager>
#include <QNetworkReply>

class MainWindow : public QMainWindow {
private:
    QNetworkAccessManager* mNetAccessManager;
};

9.3 声明槽函数

mainwindow.h 的头文件中,声明 onReplied() 槽函数,用于处理服务器返回的数据

class MainWindow : public QMainWindow {
private slots:
    void onReplied(QNetworkReply* reply);
};

9.4 关联信号槽

QNetworkAccessManagerfinished() 信号,与自定义的 onReplied() 槽函数进行关联

MainWindow::MainWindow(QWidget* parent) : QWidget(parent), ui(new Ui::MainWindow)
{
    // 关联信号槽
    mNetAccessManager = new QNetworkAccessManager(this);
    connect(mNetAccessManager, &QNetworkAccessManager::finished, this, &MainWindow::onReplied);
}

9.5 发送 http 请求

首先,在 mainwindow.h 声明一个用于发送 HTTP 请求的成员函数:

class MainWindow : public QMainWindow {

protected:
    void getWeatherInfo(QString cityCode);
};

然后,调用 getWeatherInfo() 来发送 HTTP 请求

MainWindow::MainWindow(QWidget* parent) : QWidget(parent), ui(new Ui::MainWindow)
{
    // 直接在构造中,去请求天气数据
    getWeatherInfo("101010100");    // 101010100 是北京的城市编码
}

void MainWindow::getWeatherInfo(QString cityCode)
{
    QUrl    url("http://t.weather.itboy.net/api/weather/city/" + cityCode);
    mNetAccessManager->get(QNetworkRequest(url));
}

getWeatherInfo() 接收一个城市编码的参数

北京的城市编码是 101010100,暂且记住,下节课会讲解如何通过城市名获取城市编码

9.6 接收服务端数据

HTTP 请求完毕,服务器返回数据时,mNetAccessManager 就会发射一个 finished() 信号,进而调用 onReplied() 槽函数

void MainWindow::onReplied(QNetworkReply* reply)
{
    // 响应的状态码为200, 表示请求成功
    int status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();

    qDebug() << "operation:" << reply->operation();       // 请求方式
    qDebug() << "status code:" << status_code;            // 状态码
    qDebug() << "url:" << reply->url();                   // url
    qDebug() << "raw header:" << reply->rawHeaderList();  // header

    // 如果指定的城市编码不存在,就会报错:
    // "Error transferring http://t.weather.itboy.net/api/weather/city/000000000
    // - server replied: Not Found"
    if ( reply->error() != QNetworkReply::NoError || status_code != 200 ) {
        qDebug("%s(%d) error: %s", __FUNCTION__, __LINE__, reply->errorString().toLatin1().data());
        QMessageBox::warning(this, "天气", "请求数据失败!", QMessageBox::Ok);
    } else {
        //获取响应信息
        QByteArray byteArray = reply->readAll();
        qDebug() << "read all:" << byteArray.data();
        //        parseJson(byteArray);
    }

    reply->deleteLater();
}

服务器返回的所有数据,都封装在 QNetworkReply 中,包括响应头、状态码、响应体等。

服务器返回的天气的数据格式为 JSON 格式

将接收到的 JSON 数据拷贝到在线解析的工具中,就可以很清晰地看到返回的数据的组织结构

这里仅仅是讲接收到的数据打印了出来,下节课开始再解析 JSON,然后显示到控件上

10 解析天气数据

获取到服务端的数据之后,就可以使用 Qt 提供的 JSON 相关的类,来将其中的一个个字段解析出来,并展示到界面上了

10.1 定义两个类

由于界面上主要显示的是 “今天” 的天气,以及“最近六天” 的天气,因此我们新建一个 weatherdata.h,并定义两个类:

  • Today
    • 用于显示今天的所有天气参数,也就是屏幕左侧的数据
  • Day
    • 用于显示六天的天气参数,也就是屏幕右侧的数据

这样,可以方便地将解析出的数据保存到类的成员变量

#ifndef WEATHERDATA_H
#define WEATHERDATA_H

#include <QString>

class Today
{
public:
    Today()
    {
        date = "2022-10-20";
        city = "广州";

        ganmao = "感冒指数";

        wendu = 0;
        shidu = "0%";
        pm25 = 0;
        quality = "无数据";

        type = "多云";

        fl = "2级";
        fx = "南风";

        high = 30;
        low = 18;
    }

    QString date;
    QString city;

    QString ganmao;

    int wendu;
    QString shidu;
    int pm25;
    QString quality;

    QString type;

    QString fx;
    QString fl;

    int high;
    int low;
};

class Day
{
public:
    Day()
    {
        date = "2022-10-20";
        week = "周五";

        type = "多云";

        high = 0;
        low = 0;

        fx = "南风";
        fl = "2级";

        aqi = 0;
    }

    QString date;
    QString week;

    QString type;

    int high;
    int low;

    QString fx;
    QString fl;

    int aqi;
};

#endif  // WEATHERDATA_H

然后,在 mainwindow.h 中定义两个成员变量 mTodaymDay[6],并声明成员函数 parseJson() 来解析 JSON 数据

class MainWindow : public QMainWindow {

protected:
    void parseJson(QByteArray& byteArray);

private:
    Today mToday;
    Day   mDay[6];
};

10.2 解析数据

parseJson() 中解析数据,解析出的数据就保存到两个成员变量 mTodaymDay[6] 中:

void MainWindow::parseJson(QByteArray& byteArray)
{
    QJsonParseError err;
    QJsonDocument doc = QJsonDocument::fromJson(byteArray, &err);
    if ( err.error != QJsonParseError::NoError ) {
        qDebug("%s(%d): %s", __FUNCTION__, __LINE__, err.errorString().toLatin1().data());
        return;
    }

    QJsonObject rootObj = doc.object();
    qDebug() << rootObj.value("message").toString();
    QString message = rootObj.value("message").toString();
    if ( !message.contains("success") ) {
        QMessageBox::warning(this, "天气", "请求数据失败!", QMessageBox::Ok);
        return;
    }

    // 1. 解析日期和城市
    mToday.date = rootObj.value("date").toString();
    mToday.city = rootObj.value("cityInfo").toObject().value("city").toString();

    QJsonObject objData = rootObj.value("data").toObject();

    // 2. 解析 yesterday
    QJsonObject objYestody = objData.value("yesterday").toObject();
    mDay[0].week = objYestody.value("week").toString();
    mDay[0].date = objYestody.value("ymd").toString();

    mDay[0].type = objYestody.value("type").toString();

    QString s;
    s = objYestody.value("high").toString().split(" ").at(1);
    s = s.left(s.length() - 1);
    mDay[0].high = s.toInt();

    s = objYestody.value("low").toString().split(" ").at(1);
    s = s.left(s.length() - 1);
    mDay[0].low = s.toInt();

    mDay[0].fx = objYestody.value("fx").toString();
    mDay[0].fl = objYestody.value("fl").toString();

    mDay[0].aqi = objYestody.value("aqi").toDouble();

    // 3. 解析 forecast 中5天的数据
    QJsonArray forecastArr = objData.value("forecast").toArray();
    for ( int i = 0; i < 5; i++ ) {
        QJsonObject objForcast = forecastArr[i].toObject();
        mDay[i + 1].week = objForcast.value("week").toString();
        mDay[i + 1].date = objForcast.value("ymd").toString();

        mDay[i + 1].type = objForcast.value("type").toString();

        QString s;
        s = objForcast.value("high").toString().split(" ").at(1);
        s = s.left(s.length() - 1);
        mDay[i + 1].high = s.toInt();

        s = objForcast.value("low").toString().split(" ").at(1);
        s = s.left(s.length() - 1);
        mDay[i + 1].low = s.toInt();

        mDay[i + 1].fx = objForcast.value("fx").toString();
        mDay[i + 1].fl = objForcast.value("fl").toString();

        mDay[i + 1].aqi = objForcast.value("aqi").toDouble();
    }

    // 4. 解析今天的数据
    mToday.ganmao = objData.value("ganmao").toString();

    mToday.wendu = objData.value("wendu").toString().toInt();
    mToday.shidu = objData.value("shidu").toString();
    mToday.pm25 = objData.value("pm25").toDouble();
    mToday.quality = objData.value("quality").toString();

    // 5. forecast中的第一个数组, 也是今天的数据
    mToday.type = mDay[1].type;

    mToday.fx = mDay[1].fx;
    mToday.fl = mDay[1].fl;

    mToday.high = mDay[1].high;
    mToday.low = mDay[1].low;

    // 6. 更新 UI
    //    updateUI();
}

11 更新 UI 界面

上一节将解析出的天气数据放到了mTodaymDay[6] 中,这样就可以很方便地将数据展示到界面上。

11.1 创建控件数组

更新6天的数据时,为了方便使用循环来更新控件的显示,将同一组控件放到一个数组中。

首先,在 mainwindow.h 中,添加几个列表,如下:

class MainWindow : public QMainWindow
{
private:
    // 星期和日期
    QList<QLabel*> mWeekList;
    QList<QLabel*> mDateList;

    // 天气和天气图标
    QList<QLabel*> mTypeList;
    QList<QLabel*> mTypeIconList;

    // 天气指数
    QList<QLabel*> mAqiList;

    // 风向和风力
    QList<QLabel*> mFxList;
    QList<QLabel*> mFlList;
};

然后,在 mainwindow.cpp 的构造中,初始化列表,如下:

MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
    // UI初始化
    // 将控件放到数组里面,方便使用循环进行处理
    // 星期和日期
    mWeekList << ui->lblWeek0 << ui->lblWeek1 << ui->lblWeek2 << ui->lblWeek3 << ui->lblWeek4 << ui->lblWeek5;
    mDateList << ui->lblDate0 << ui->lblDate1 << ui->lblDate2 << ui->lblDate3 << ui->lblDate4 << ui->lblDate5;

    // 天气和天气图标
    mTypeList << ui->lblType0 << ui->lblType1 << ui->lblType2 << ui->lblType3 << ui->lblType4 << ui->lblType5;
    mTypeIconList << ui->lblTypeIcon0 << ui->lblTypeIcon1 << ui->lblTypeIcon2 << ui->lblTypeIcon3 << ui->lblTypeIcon4 << ui->lblTypeIcon5;

    // 天气指数
    mAqiList << ui->lblQuality0 << ui->lblQuality1 << ui->lblQuality2 << ui->lblQuality3 << ui->lblQuality4 << ui->lblQuality5;

    // 风向和风力
    mFxList << ui->lblFx0 << ui->lblFx1 << ui->lblFx2 << ui->lblFx3 << ui->lblFx4 << ui->lblFx5;
    mFlList << ui->lblFl0 << ui->lblFl1 << ui->lblFl2 << ui->lblFl3 << ui->lblFl4 << ui->lblFl5;
}

11.2 更新

首先,在 mainwindow.h 中声明一个成员函数 updateUI() 用于将解析出的数据,更新到界面显示

class MainWindow : public QMainWindow {

protected:
    void updateUI();
};

然后,在 mainwindow.cpp 中实现,如下:

void MainWindow::updateUI()
{
    // 1. 更新日期和城市
    ui->lblDate->setText(QDateTime::fromString(mToday.date, "yyyyMMdd").toString("yyyy/MM/dd") + " " + mDay[1].week);
    ui->lblCity->setText(mToday.city);

    // 2. 更新今天
    ui->lblTypeIcon->setPixmap(mTypeMap[mToday.type]);
    ui->lblTemp->setText(QString::number(mToday.wendu) + "°");
    ui->lblType->setText(mToday.type);
    ui->lblLowHigh->setText(QString::number(mToday.low) + "~" + QString::number(mToday.high) + "°C");

    ui->lblGanMao->setText("感冒指数:" + mToday.ganmao);

    ui->lblWindFx->setText(mToday.fx);
    ui->lblWindFl->setText(mToday.fl);

    ui->lblPM25->setText(QString::number(mToday.pm25));

    ui->lblShiDu->setText(mToday.shidu);

    ui->lblQuality->setText(mToday.quality);

    // 3. 更新六天
    for ( int i = 0; i < 6; i++ ) {
        // 3.1 更新星期和日期
        mWeekList[i]->setText("周" + mDay[i].week.right(1));

        //设置 昨天/今天/明天 的星期显示 - 不显示星期几,而是显示
        //“昨天”、“今天”、“明天”
        ui->lblWeek0->setText("昨天");
        ui->lblWeek1->setText("今天");
        ui->lblWeek2->setText("明天");

        QStringList ymdList = mDay[i].date.split("-");
        mDateList[i]->setText(ymdList[1] + "/" + ymdList[2]);

        // 3.2 更新天气类型
        mTypeIconList[i]->setPixmap(mTypeMap[mDay[i].type]);
        mTypeList[i]->setText(mDay[i].type);

        // 3.3 更新空气质量
        if ( mDay[i].aqi >= 0 && mDay[i].aqi <= 50 ) {
            mAqiList[i]->setText("优");
            mAqiList[i]->setStyleSheet("background-color: rgb(121, 184, 0);");
        } else if ( mDay[i].aqi > 50 && mDay[i].aqi <= 100 ) {
            mAqiList[i]->setText("良");
            mAqiList[i]->setStyleSheet("background-color: rgb(255, 187, 23);");
        } else if ( mDay[i].aqi > 100 && mDay[i].aqi <= 150 ) {
            mAqiList[i]->setText("轻度");
            mAqiList[i]->setStyleSheet("background-color: rgb(255, 87, 97);");
        } else if ( mDay[i].aqi > 150 && mDay[i].aqi <= 200 ) {
            mAqiList[i]->setText("中度");
            mAqiList[i]->setStyleSheet("background-color: rgb(235, 17, 27);");
        } else if ( mDay[i].aqi > 200 && mDay[i].aqi <= 300 ) {
            mAqiList[i]->setText("重度");
            mAqiList[i]->setStyleSheet("background-color: rgb(170, 0, 0);");
        } else {
            mAqiList[i]->setText("严重");
            mAqiList[i]->setStyleSheet("background-color: rgb(110, 0, 0);");
        }

        // 3.4 更新风力/风向
        mFxList[i]->setText(mDay[i].fx);
        mFlList[i]->setText(mDay[i].fl);
    }
}

11.3 天气图标的处理

由于在资源文件中,添加中文名称的图标,会报错,而请求回来的天气类型又都是中文,比如“晴”、“小雨” 等

因此,在 mainwindow.h 中 定义了一个 MAP,用于英文到中文的转换:

class MainWindow : public QMainWindow
{
private:
    QMap<QString, QString> mTypeMap;
};

并且在构造中,以天气类型为 key,以图标资源的路径为 value,初始化 MAP,如下:

MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    mTypeMap.insert("暴雪", ":/res/type/BaoXue.png");
    mTypeMap.insert("暴雨", ":/res/type/BaoYu.png");
    mTypeMap.insert("暴雨到大暴雨", ":/res/type/BaoYuDaoDaBaoYu.png");
    mTypeMap.insert("大暴雨", ":/res/type/DaBaoYu.png");
    mTypeMap.insert("大暴雨到特大暴雨", ":/res/type/DaBaoYuDaoTeDaBaoYu.png");
    mTypeMap.insert("大到暴雪", ":/res/type/DaDaoBaoXue.png");
    mTypeMap.insert("大雪", ":/res/type/DaXue.png");
    mTypeMap.insert("大雨", ":/res/type/DaYu.png");
    mTypeMap.insert("冻雨", ":/res/type/DongYu.png");
    mTypeMap.insert("多云", ":/res/type/DuoYun.png");
    mTypeMap.insert("浮沉", ":/res/type/FuChen.png");
    mTypeMap.insert("雷阵雨", ":/res/type/LeiZhenYu.png");
    mTypeMap.insert("雷阵雨伴有冰雹", ":/res/type/LeiZhenYuBanYouBingBao.png");
    mTypeMap.insert("霾", ":/res/type/Mai.png");
    mTypeMap.insert("强沙尘暴", ":/res/type/QiangShaChenBao.png");
    mTypeMap.insert("晴", ":/res/type/Qing.png");
    mTypeMap.insert("沙尘暴", ":/res/type/ShaChenBao.png");
    mTypeMap.insert("特大暴雨", ":/res/type/TeDaBaoYu.png");
    mTypeMap.insert("undefined", ":/res/type/undefined.png");
    mTypeMap.insert("雾", ":/res/type/Wu.png");
    mTypeMap.insert("小到中雪", ":/res/type/XiaoDaoZhongXue.png");
    mTypeMap.insert("小到中雨", ":/res/type/XiaoDaoZhongYu.png");
    mTypeMap.insert("小雪", ":/res/type/XiaoXue.png");
    mTypeMap.insert("小雨", ":/res/type/XiaoYu.png");
    mTypeMap.insert("雪", ":/res/type/Xue.png");
    mTypeMap.insert("扬沙", ":/res/type/YangSha.png");
    mTypeMap.insert("阴", ":/res/type/Yin.png");
    mTypeMap.insert("雨", ":/res/type/Yu.png");
    mTypeMap.insert("雨夹雪", ":/res/type/YuJiaXue.png");
    mTypeMap.insert("阵雪", ":/res/type/ZhenXue.png");
    mTypeMap.insert("阵雨", ":/res/type/ZhenYu.png");
    mTypeMap.insert("中到大雪", ":/res/type/ZhongDaoDaXue.png");
    mTypeMap.insert("中到大雨", ":/res/type/ZhongDaoDaYu.png");
    mTypeMap.insert("中雪", ":/res/type/ZhongXue.png");
    mTypeMap.insert("中雨", ":/res/type/ZhongYu.png");
}

此时,就可以将请求回来的实际数据,展示到界面上了。

请求不同的城市天气,可以看到界面的展示,是跟着变化的,如下可以请求北京和广州的天气:

MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    //    getWeatherInfo("101010100");  // 101010100 是北京的城市编码
    getWeatherInfo("101280101");  // 101280101 是广州的城市编码
}

解析出数据之后,还会绘制温度曲线,这个在下节讲解。

12 获取城市编号

上面已经看到了,获取天气数据的接口,需要传递一个 9 位的城市编码,而作为用户在界面上通常是输入城市名称或者县城名称

那么这就需要将城市名称转换为对应的城市编码,已经有别人已经做好了一个 JSON 文档:

✅ citycode-2019-08-23.json

里面就是记录了城市名称和编码的对应关系,全国很多的城市和县城,因此该文件有 2 万多行:

[
  {
    "id": 1,
    "pid": 0,
    "city_code": "101010100",
    "city_name": "北京",
    "post_code": "100000",
    "area_code": "010",
    "ctime": "2019-07-11 17:30:06"
  },
  {
    "id": 24,
    "pid": 0,
    "city_code": "101020100",
    "city_name": "上海",
    "post_code": "200000",
    "area_code": "021",
    "ctime": "2019-07-11 17:30:08"
  },
  {
    "id": 75,
    "pid": 5,
    "city_code": "101280101",
    "city_name": "广州",
    "post_code": "510000",
    "area_code": "020",
    "ctime": "2019-07-11 17:30:21"
  }
  ...
]

接下来我们就来实现一个工具类:WeatherTool.h, 用来实现城市名称转城市编码

#ifndef WEATHERTOOL_H
#define WEATHERTOOL_H

#include <QCoreApplication>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QJsonValue>
#include <QMap>

class WeatherTool
{
private:
    static void initCityMap()
    {
        // 默认将城市列表放在了 E 盘根目录下
        QString filePath = "E:/citycode-2019-08-23.json";

        QFile file(filePath);
        file.open(QIODevice::ReadOnly | QIODevice::Text);
        QByteArray json = file.readAll();
        file.close();

        QJsonParseError err;
        QJsonDocument doc = QJsonDocument::fromJson(json, &err);
        if ( err.error != QJsonParseError::NoError ) {
            qDebug("%s(%d) parse json failed: %s", __FUNCTION__, __LINE__, err.errorString().toStdString().data());
            return;
        }

        // 文件应该是一个 JSON 数组
        if ( !doc.isArray() ) {
            qDebug("%s(%d) parse json failed: not an array", __FUNCTION__, __LINE__);
            return;
        }

        QJsonArray citys = doc.array();
        for ( int i = 0; i < citys.size(); i++ ) {
            QString code = citys[i].toObject().value("city_code").toString();
            QString city = citys[i].toObject().value("city_name").toString();
            // 省份没有 city_code
            if ( code.size() > 0 ) {
                mCityMap.insert(city, code);
            }
        }
    }

public:
    static QString getCityCode(QString cityName)
    {
        if ( mCityMap.isEmpty() ) {
            initCityMap();
        }

        // 有的城市没有加 “市”,比如 “北京”
        // 县,都是加了 “县”的
        QMap<QString, QString>::const_iterator it = mCityMap.find(cityName);

        if ( it == mCityMap.end() ) {
            it = mCityMap.find(cityName + "市");
        }

        if ( it != mCityMap.end() ) {
            return it.value();
        }

        return "";
    }

private:
    // 声明一个静态成员变量
    static QMap<QString, QString> mCityMap;
};

// 初始化静态成员变量
QMap<QString, QString> WeatherTool::mCityMap = {};

#endif  // WEATHERTOOL_H

此时,getWeatherInfo() 函数,就可以修改为如下:

void MainWindow::getWeatherInfo(QString cityName)
{
    QString cityCode = WeatherTool::getCityCode(cityName);
    if ( cityCode.isEmpty() ) {
        QMessageBox::warning(this, "天气", "请检查输入是否正确!", QMessageBox::Ok);
        return;
    }

    QString base = "http://t.weather.itboy.net/api/weather/city/";
    QUrl url(base + cityCode);
    mNetAccessManager->get(QNetworkRequest(url));
}

接下来实现,点击搜索按钮,来获取指定城市的天气数据

void MainWindow::on_btnSearch_clicked()
{
    QString cityName = ui->leCity->text();
    getWeatherInfo(cityName);
}

这样就实现了,通过输入城市名称,来获取天气数据!

13 绘制温度曲线

6 天的数据,使用曲线连接起来,使得天气趋势一目了然

13.1 安装事件****过滤器

为高低温的标签 lblHighCurvelblLowCurve 安装事件过滤器

MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 安装事件过滤器
    // 参数指定为 this, 也就是当前窗口对象 MainWindow
    ui->lblHighCurve->installEventFilter(this);
    ui->lblLowCurve->installEventFilter(this);
}

13.2 重写 eventFilter() 函数

重写 MainWindoweventFilter() 函数

这样就可以在 eventFilter() 函数中拦截发向 lblHighCurvelblLowCurve 这两个控件的事件

具体实现如下:

首先,在 mainwindow.h 中声明 eventFilter(),并声明两个成员函数:

  • paintHighCurve() - 绘制高温曲线
  • paintLowCurve() - 绘制低温曲线
class MainWindow : public QMainWindow
{
public:
    bool eventFilter(QObject* watched, QEvent* event);
    
protected:
    // 绘制高低温曲线
    void paintHighCurve();
    void paintLowCurve();
}

然后,在 mainwindow.cpp 中实现:

bool MainWindow::eventFilter(QObject* watched, QEvent* event)
{
    if ( watched == ui->lblHighCurve && event->type() == QEvent::Paint ) {
        paintHighCurve();
    }
    if ( watched == ui->lblLowCurve && event->type() == QEvent::Paint ) {
        paintLowCurve();
    }
    return QWidget::eventFilter(watched, event);
}

这样,当解析完数据,调用高低温标签的 update() 函数,就可以实现曲线的绘制:

void MainWindow::parseJson(QByteArray& byteArray)
{
        ...

    // 绘制温度曲线
    ui->lblHighCurve->update();
    ui->lblLowCurve->update();
}

具体流程:

  • 调用标签的 update() 函数
  • 框架发送 QEvent::Paint 事件 给标签
  • 事件被 MainWindow 拦截,进而调用其 eventFilter() 函数
  • eventFilter() 中,调用 paintHighCurve()paintLowCurve() 来真正绘制曲线

13.3 绘制曲线

以下是绘制高温曲线的代码:

void MainWindow::paintHighCurve()
{
    QPainter painter(ui->lblHighCurve);

    // 抗锯齿
    painter.setRenderHint(QPainter::Antialiasing, true);

    // 1. 获取 x 轴坐标
    int pointX[6] = {0};
    for ( int i = 0; i < 6; i++ ) {
        pointX[i] = mWeekList[i]->pos().x() + mWeekList[i]->width() / 2;
    }

    // 2. 获取 y 轴坐标

    //  int temp[6]   = {0};
    int tempSum = 0;
    int tempAverage = 0;

    // 2.1 计算平均值
    for ( int i = 0; i < 6; i++ ) {
        tempSum += mDay[i].high;
    }

    tempAverage = tempSum / 6;  // 最高温平均值

    qDebug() << "paintHighCurve" << tempAverage;

    // 2.2 计算 y 轴坐标
    int pointY[6] = {0};
    int yCenter = ui->lblHighCurve->height() / 2;
    for ( int i = 0; i < 6; i++ ) {
        pointY[i] = yCenter - ((mDay[i].high - tempAverage) * INCREMENT);
    }

    // 3. 开始绘制
    // 3.1 初始化画笔
    QPen pen = painter.pen();
    pen.setWidth(1);                    //设置画笔宽度为1
    pen.setColor(QColor(255, 170, 0));  //设置颜色
    painter.save();

    painter.setPen(pen);
    painter.setBrush(QColor(255, 170, 0));  //设置画刷颜色

    // 3.2 画点、写文本
    for ( int i = 0; i < 6; i++ ) {
        painter.drawEllipse(QPoint(pointX[i], pointY[i]), POINT_RADIUS, POINT_RADIUS);
        painter.drawText(QPoint(pointX[i] - TEXT_OFFSET_X, pointY[i] - TEXT_OFFSET_Y), QString::number(mDay[i].high) + "°");
    }

    // 3.3 绘制曲线
    for ( int i = 0; i < 5; i++ ) {
        if ( i == 0 ) {
            pen.setStyle(Qt::DotLine);  //虚线
            painter.setPen(pen);
        } else {
            pen.setStyle(Qt::SolidLine);  // 实线
            painter.setPen(pen);
        }
        painter.drawLine(pointX[i], pointY[i], pointX[i + 1], pointY[i + 1]);
    }

    painter.restore();
}

13.4 程序说明

(1)****宏定义

定义了以下几个宏,而不是在代码中直接写死:

 // 温度曲线相关的宏
 #define INCREMENT     3   // 温度每升高/降低1度,y轴坐标的增量
 #define POINT_RADIUS  3   // 曲线描点的大小
 #define TEXT_OFFSET_X 12  // 温度文本相对于点的偏移
 #define TEXT_OFFSET_Y 10  // 温度文本相对于点的偏移

(2)绘图相关

绘制曲线时,有很多的小细节,就不详细展开了,比如:

  • 画笔、画刷的颜色
  • 划线、画圆
  • 实线、虚线

实现效果

纯代码实现请联系博主

posted @ 2024-11-23 17:21  逸風明  阅读(1)  评论(0编辑  收藏  举报