【C++/QT】天气预报项目(下)(附完整代码和可执行文件)
项目覆盖 Qt 多模块知识,实现了 “请求 - 解析 - 显示 - 交互” 的完整闭环,兼具功能性与用户体验,是 Qt 桌面应用开发的典型实践。
天气预报项目(下)
四、核心功能实现
首先,我们来基于QT框架,介绍一下天气预报项目的核心业务是如何运行起来的:
- main()函数实例化Widget对象,调用Widget类的构造函数。
int main(int argc, char *argv[])
{
......
Widget w;//实例化Widget对象
......
}
- 构造函数内绑定信号与槽,并发起HTTP请求
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
......
manager = new QNetworkAccessManager(this);// 创建QNetworkAccessManager对象,用于管理网络请求
QUrl urlTianQi("http:www.xxxxxx.com");// 创建QUrl对象,存储天气API的请求地址
QNetworkRequest res(urlTianQi);// 创建QNetworkRequest对象,用于设置网络请求的相关信息(URL)
reply = manager->get(res);//调用QNetworkAccessManager的get()方法发送GET请求,返回一个QNetworkReply指针用于处理响应
connect(manager,&QNetworkAccessManager::finished,this,&Widget::readHttpReply);//绑定信号与槽,当get请求结束后,会触发readHttpReply函数
......
}
- 在readHttpReply函数中处理请求结果,若请求成功,则解析数据
void Widget::readHttpReply(QNetworkReply *reply)
{
......
QByteArray data = reply->readAll();//读取请求数据
parseWeatherJsonDataNew(data);//解析
......
}
- 更新解释到的数据到UI界面的QLabel中
void Widget::parseWeatherJsonDataNew(QByteArray rawData)
{
......
updateUI();//更新UI
......
}
- 在updateUI()中处理数据
以上就是我们天气预报项目核心业务的主要流程,除此之外,本文还将介绍项目的其他辅助功能,这些功能虽然不算该项目的核心业务,但他们确实我们在做其他应用时很实用的可以提升用户体验的通用技巧,包括轻量的用户弹窗,QLabel显示动态图片,以及鼠标拖动窗口移动等。
1. 七日天气HTTP请求
(1)HTTP协议介绍
概念:互联网中最核心的应用层协议之一,用于实现客户端(如浏览器、手机 App)与服务器之间的数据通信,也是万维网(WWW)能够正常运行的基础。它定义了客户端如何向服务器请求资源、服务器如何响应请求的规范,本质是一种 “请求 - 响应” 模式的无状态协议。
核心特性:
- 无状态:服务器不会保存客户端的任何历史请求信息。每次请求都是独立的,服务器无法通过前一次请求的上下文来处理当前请求。
- 请求 - 响应:①客户端(如浏览器)发起请求(Request),指定要获取的资源或要执行的操作;②服务器接收请求后,处理并返回响应(Response),包含状态码(如 “成功”“找不到资源”)和数据;③响应完成后,根据连接策略决定是否关闭 TCP 连接。
工作流程:
- DNS域名解析:客户端需要将域名(www.baidu.com)转换为服务器的 IP 地址(如 93.184.216.34),通过 DNS 服务器完成解析。
- 建立 TCP 连接:客户端与服务器的目标 IP 建立 TCP 连接(遵循 “三次握手”),HTTP 协议基于 TCP 实现可靠的数据传输。
- 发送 HTTP 请求:客户端通过已建立的 TCP 连接,向服务器发送 HTTP 请求(包含请求行、请求头、请求体)。
- 服务器处理请求:服务器接收请求后,根据请求内容(如请求的资源路径、方法)进行处理(如读取文件、查询数据库)。
- 返回 HTTP 响应:服务器将处理结果封装为 HTTP 响应(包含状态行、响应头、响应体),通过 TCP 连接返回给客户端。
- 关闭 / 复用 TCP 连接:客户端接收响应后解析数据。
(2)Qt网络模块核心类
QNetworkAccessManager:网络请求的核心管理器,能够发起请求,管理请求队列。
QNetworkRequest:封装网络请求的信息,包括 URL、请求头、缓存策略等。QNetworkReply:网络请求的响应对象,由 QNetworkAccessManager 在请求完成后返回。核心方法包括提供响应数据(readAll、bytesAvailable)、获取响应状态(error、attribute(QNetworkRequest::HttpStatusCodeAttribute))、进度跟踪(downloadProgress、uploadProgress)等。
(3)源码详解
在了解了Qt网络模块的核心类后,接着来了解如何使用他们来发起一个HTTP请求:
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
......
manager = new QNetworkAccessManager(this);// 创建QNetworkAccessManager对象,用于管理网络请求
QUrl urlTianQi("http://gfeljm.tianqiapi.com/api?unescape=1&version=v9&appid=48435331&appsecret=A24ayNLj&cityid=101110206");// 创建QUrl对象,存储天气API的请求地址
QNetworkRequest res(urlTianQi);// 创建QNetworkRequest对象,用于设置网络请求的相关信息(URL)
reply = manager->get(res);//调用QNetworkAccessManager的get()方法发送GET请求,返回一个QNetworkReply指针用于处理响应
connect(manager,&QNetworkAccessManager::finished,this,&Widget::readHttpReply);//绑定信号与槽,当get请求结束后,会触发readHttpReply函数
......
}
如上所示代码,发起一个HTTP请求简而言之就是使用QNetworkRequest类构造一个请求对象,接着使用QNetworkAccessManager类的get方法来发起一个HTTP请求,同时使用QNetworkReply来接收请求响应。当请求完成后会触发finished信号,因此我们绑定信号与槽,在槽函数readHttpReply中处理请求数据:
// 处理网络请求的响应数据
// 参数reply:指向QNetworkReply对象的指针,包含服务器返回的响应信息
void Widget::readHttpReply(QNetworkReply *reply)
{
// 获取HTTP响应状态码
// QNetworkRequest::HttpStatusCodeAttribute用于获取状态码属性
int resCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// 检查网络请求是否成功且HTTP状态码为200(表示请求成功)
// reply->error() == QNetworkReply::NoError:判断网络层面无错误
// resCode == 200:判断应用层面服务器成功处理请求
if(reply->error() == QNetworkReply::NoError && resCode == 200)
{
// 读取服务器返回的所有数据,存储在QByteArray中
// QByteArray适合存储二进制数据或文本数据
QByteArray data = reply->readAll();
// 调用自定义的JSON解析函数,处理获取到的天气数据
// 将原始字节数据传递给解析函数进行处理
parseWeatherJsonDataNew(data);
}
else
{
// 若请求失败,创建一个错误提示对话框
QMessageBox msg;
// 设置对话框标题为"错误"
msg.setWindowTitle("错误");
// 设置对话框图标为 critical(错误图标)
msg.setIcon(QMessageBox::Critical);
// 设置对话框显示的文本内容
msg.setText("网络请求失败!");
// 设置对话框的标准按钮为"确定"
msg.setStandardButtons(QMessageBox::Ok);
// 显示对话框,exec()会阻塞直到用户点击按钮
msg.exec();
}
reply->deleteLater(); // 确保在适当的时候释放资源,避免内存泄漏
}
我们可以看到,在槽函数中只是调用了readAll函数读取了数据,接下来就调用了parseWeatherJsonDataNew函数来解析JSON数据。
2. 天气数据JSON包解析
在进行JSON包解析前首先介绍一下JSON的基本结构。
(1)JSON基本结构
概念:一种轻量级、跨语言的数据交换格式,凭借语法简洁、可读性强、解析效率高的特点,广泛应用于前后端数据交互、API 响应(如天气接口返回数据)等场景。其核心结构基于键值对集合和有序值列表,所有数据类型均围绕这两种基础结构组合而成。
设计原则:
- 纯文本格式:不包含二进制数据,所有内容均为可打印字符,便于传输和阅读;
- 键值关联:数据通过 “键(Key)- 值(Value)” 对应,清晰表达数据含义(如 {“tempMax”: 28})
- 层级嵌套:支持值的嵌套(如 “对象中包含数组,数组中包含对象”),可描述复杂数据关系
对象结构(Object):键值对的无序集合
- 作用:表示一个 “实体”(如 “单天天气数据”“用户信息”),存储多个相关的键值对;
- 语法特征:用大括号 {} 包裹,内部是若干个 “键:值” 对,键值对之间用逗号 , 分隔
- 键(Key)规则:必须是双引号包裹的字符串
- 值(Value)规则:可对应 JSON 支持的任意数据类型(字符串、数字、布尔、null、对象、数组)
示例如下:
{
"date": "2024-05-20", // 键:"date",值:字符串类型(日期)
"weather": "晴", // 键:"weather",值:字符串类型(天气状况)
"tempMax": 28, // 键:"tempMax",值:数字类型(最高温度)
"tempMin": 18, // 键:"tempMin",值:数字类型(最低温度)
"isRain": false, // 键:"isRain",值:布尔类型(是否下雨)
"wind": { // 键:"wind",值:对象类型(嵌套风的详情)
"dir": "东风",
"force": 3
},
"notice": null // 键:"notice",值:null(无特殊提示)
}
数组结构(Array):有序的值列表
- 作用:表示一组 “同类数据” 的集合(如 “七日天气列表”“多个用户信息”),强调数据的顺序性(索引从 0 开始);
- 语法特征:用方括号 [] 包裹,内部是若干个值,值之间用逗号 , 分隔
- 值(Value)规则:可对应 JSON 支持的任意数据类型,且数组内的值允许不同类型
示例如下:
[
{ "date": "2024-05-20", "weather": "晴", "tempMax": 28 }, // 索引0:第一天
{ "date": "2024-05-21", "weather": "多云", "tempMax": 26 }, // 索引1:第二天
{ "date": "2024-05-22", "weather": "雨", "tempMax": 22 }, // 索引2:第三天
// ... 后续4天数据省略
]
(2)返回的JSON数据包

如上图所示为响应的JSON数据包,其中七天的天气状况在data这个数组结构中,其中该数组又是由7个对象结构(Object)组成,接下来我们来看看对象结构的内容:

在该结构中可以看到我们UI界面上需要的大部分内容,包括日期、天气状况、温度范围、风力和空气质量等,这一部分是我们JSON数据包中重点需要解析的部分。
(3)JSON数据包处理
要保存7天的天气数据,最适合的数据结构就是数组,其中每个元素保存一天的天气数据,根据我们的UI配置可知,对于每天我们需要的数据有日期、星期、温度、天气状况、最高温度、最低温度,风力、空气质量等。为此,可以创建一个Day类,将以上数据设为成员变量,接着在Widget类中加入days[7]成员变量,Day的数据类型如以下代码所示:
#ifndef DAY_H
#define DAY_H
#include <QString>
class Day
{
public:
Day();
QString mDate;//保存日期
QString mWeek;//保存星期
QString mCity;//保存当前城市
QString mTemp;//保存当天温度
QString mWeathType;//保存当天天气状况
QString mTempLow;//保存最低温度
QString mTempHigh;//保存最高温度
QString mTips;//保存感冒指数(穿衣指数)
QString mFx;//保存当天风向
QString mFl;//保存当天风力
QString mPm25;//保存PM2.5指数
QString mHu;//保存当天湿度
QString mAirq;//保存当天空气质量
};
#endif // DAY_H
在介绍解析JSON数据包函数前首先给大家介绍一下Qt中JSON处理类QJsonDocument、QJsonObjectQJsonArray。
- QJsonDocument
概念:整个 JSON 文档的容器,用于封装完整的 JSON 数据(可以是一个 JSON 对象或 JSON 数组),并提供解析和序列化功能。
功能:(1)解析 JSON 数据(QJsonDocument::fromJson(jsonData)):将原始字节数据(QByteArray)解析为 JSON 文档对象。(2)生成 JSON 数据(doc.toJson(QJsonDocument::Indented)):将内存中的 JSON 对象或数组序列化为字节数据(QByteArray),便于存储或网络传输。(3)判断文档类型(isObject()/isArray()):区分当前文档封装的是 JSON 对象(isObject())还是 JSON 数组(isArray())。
- QJsonObject
概念:用于表示 JSON 中的对象类型(键值对集合),键是字符串,值可以是字符串、数字、布尔值、null、其他QJsonObject或QJsonArray。
功能:(1)操作键值对:添加、获取、修改、删除键值对。(2)嵌套结构:支持包含其他QJsonObject或QJsonArray,构建复杂 JSON 结构。
- QJsonArray
概念:表示 JSON 中的数组类型(有序的值列表),元素可以是字符串、数字、布尔值、null、QJsonObject或QJsonArray。
功能:(1)操作元素:添加、获取、修改、删除数组元素。(2)遍历元素:通过索引访问或迭代器遍历。
在了解了Qt中JSON数据处理的核心类后,接下来我们来看parseWeatherJsonDataNew函数:
// 解析新格式的天气JSON数据
// 参数: rawData - 包含天气数据的JSON格式原始字节数组
void Widget::parseWeatherJsonDataNew(QByteArray rawData)
{
// 将原始字节数组解析为QJsonDocument对象(Qt中用于处理JSON文档的类)
QJsonDocument jsonDoc = QJsonDocument::fromJson(rawData);
// 检查JSON文档是否有效且是一个JSON对象
if( !jsonDoc.isNull() && jsonDoc.isObject()){
// 将JSON文档转换为JSON对象(根对象)
QJsonObject jsonRoot = jsonDoc.object();
// 从根对象中获取城市名称,并存储到days数组的第一个元素中
days[0].mCity = jsonRoot["city"].toString();
// 从根对象的"aqi"子对象(aqi键对应的值又是一个Object)中获取PM2.5值,存储到days数组的第一个元素中
// toObject()将"aqi"字段转换为JSON对象,然后获取其"pm25"字段的值
days[0].mPm25 = jsonRoot["aqi"].toObject()["pm25"].toString();
// 检查根对象是否包含"data"字段,并且该字段是一个数组(该字段是一个数组结构,数组的每个元素为Object)
if( jsonRoot.contains("data") && jsonRoot["data"].isArray()){
// 将"data"字段转换为QJsonArray对象,以便遍历
QJsonArray weaArray = jsonRoot["data"].toArray();
// 遍历天气数据数组中的每一天数据
for(int i = 0; i < weaArray.size(); i++){
// 将数组中的第i个元素转换为JSON对象,代表一天的天气数据
QJsonObject obj = weaArray[i].toObject();
// 从JSON对象中获取日期,并存储到days数组的第i个元素
days[i].mDate = obj["date"].toString();
// 从JSON对象中获取星期几,并存储
days[i].mWeek = obj["week"].toString();
// 从JSON对象中获取天气类型(如晴、雨等),并存储
days[i].mWeathType = obj["wea"].toString();
// 从JSON对象中获取温度,并存储
days[i].mTemp = obj["tem"].toString();
// 从JSON对象中获取最低温度,并存储
days[i].mTempLow = obj["tem2"].toString();
// 从JSON对象中获取最高温度,并存储
days[i].mTempHigh = obj["tem1"].toString();
// 从"win"数组的第一个元素获取风向,并存储
days[i].mFx = obj["win"].toArray()[0].toString();
// 从JSON对象中获取风力,并存储
days[i].mFl = obj["win_speed"].toString();
// 从JSON对象中获取空气质量等级,并存储
days[i].mAirq = obj["air_level"].toString();
// 从"index"数组的第4个元素(索引3)中获取天气提示信息
days[i].mTips = obj["index"].toArray()[3].toObject()["desc"].toString();
// 从JSON对象中获取湿度,并存储
days[i].mHu = obj["humidity"].toString();
}
}
}
// 解析完成后更新UI显示
updateUI();
}
以上就是提取UI所需数据的过程,总的来说就是要提取数据就得先使用QJsonDocument类来解析QbyteArray数据,然后通过QJsonObject类来根据键获取对应得值。再此过程中,由于JSON具有嵌套特性,包括值的类型可以为Object(如aqi键对应的值又为一个Object),值的类型可以为数组(如data键),因此我们需要使用toObject()或者toArray()来进行数据类型转换,最终转换成QJsonObject再进行数据提取。
3. 更新UI界面
将7天的天气数据解析到days数组中后,接下来就是更新这些数据到对应的Label中去了,听起来这很简单,但仍有一些难点,下面我将列出我在实际操作中遇到的一些问题,大家在阅读源码时需要重点注意这些问题:
- 城市名显示。解析到days中的城市并没有对应的所属范围(县、区、市),例如解析到的数据为西安,但实际我们要显示西安市,解析到的数据是海淀,但是我们要显示海淀区…
- 天气图标的显示。我们解析的只有天气类型,而显示图标需要响应的资源文件路径。
- 空气质量显示。需要根据不同的空气质量设置Label标签为不同的背景色。
// 更新UI界面,将解析后的天气数据显示到各个控件上
void Widget::updateUI()
{
// 声明QPixmap对象,用于处理天气图标
QPixmap pixmap;
// 在当前日期标签上显示第一天的星期信息
ui->labelCurrentDate->setText(days[0].mWeek);
// 根据城市类型标志(县/市/区),在城市标签上显示完整城市名称
switch (cityCodeUtils.cityChangeFlag) {
case CityCodeUtils::Xian:
// 若为县,则在城市名后添加"县"字
ui->labelCity->setText(days[0].mCity+"县");
break;
case CityCodeUtils::Shi:
// 若为市,则在城市名后添加"市"字
ui->labelCity->setText(days[0].mCity+"市");
break;
case CityCodeUtils::Qu:
// 若为区,则在城市名后添加"区"字
ui->labelCity->setText(days[0].mCity+"区");
break;
case CityCodeUtils::ShiOrXian:
ui->labelCity->setText(ui->lineEditCity->text());
break;
default:
// 默认情况不做处理
break;
}
// 在温度标签上显示当前温度
ui->labelTmp->setText(days[0].mTemp);
// 在温度范围标签上显示最低温度和最高温度,格式如"10℃~20℃"
ui->labelTmpRange->setText(days[0].mTempLow+"℃"+"~"
+days[0].mTempHigh+"℃");
// 在天气类型标签上显示当前天气类型(如晴、雨等)
ui->labelWeatherType->setText(days[0].mWeathType);
// 根据天气类型从映射表中获取对应的图标,并设置到天气图标标签上
ui->labelWeatherIcon->setPixmap(mTypeMap[days[0].mWeathType]);//days[0].mWeathType为对应的天气状况
// 在感冒指数标签上显示天气提示信息
ui->labelGanmao->setText(days[0].mTips);
// 在风向标签上显示当前风向
ui->labelFXType->setText(days[0].mFx);
// 若风向为"无持续风向",则简化显示为"无"
if(days[0].mFx == "无持续风向")
{
ui->labelFXType->setText("无");
}else{
// 否则显示原始风向
ui->labelFXType->setText(days[0].mFx);
}
// 在风力标签上显示当前风力
ui->labelFXType2->setText(days[0].mFl);
// 在PM2.5标签上显示PM2.5数值
ui->labelPM25Data->setText(days[0].mPm25);
// 在湿度标签上显示湿度信息
ui->labelShiduData->setText(days[0].mHu);
// 在空气质量标签上显示空气质量等级
ui->labelAirData->setText(days[0].mAirq);
// 循环更新未来6天的天气信息(包括当天)Widget04区域内容
for(int i = 0;i<6;i++){
// 将日期字符串按"-"分割(如"2023-10-01"分割为["2023", "10", "01"])
QStringList dateList = days[i].mDate.split("-");
// 在日期标签上显示月-日(如"10-01")
mDateList[i]->setText(dateList.at(1)+"-"+dateList.at(2));
// 在星期标签上显示星期信息
mDayList[i]->setText(days[i].mWeek);
// 查找天气类型中是否包含"转"字(如"晴转多云")
int index = days[i].mWeathType.indexOf("转");
if(index != -1)
{
// 若包含"转",则取"转"之前的天气类型对应的图标(如”晴转多云“取"晴")
pixmap = mTypeMap[days[i].mWeathType.left(index)];
}else
{
// 若不包含"转",则直接使用当前天气类型对应的图标
pixmap = mTypeMap[days[i].mWeathType];
}
// 缩放图标以适应标签大小,保持宽高比并使用平滑缩放
pixmap = pixmap.scaled(mIconList[i]->size(),Qt::KeepAspectRatio,Qt::SmoothTransformation);
// 设置天气图标标签的最大高度为40
mIconList[i]->setMaximumHeight(40);
// 设置天气图标标签的最大宽度为父窗口宽度的1/6.5
mIconList[i]->setMaximumWidth(ui->widget02->width()/6.5);
// 将缩放后的图标设置到天气图标标签上
mIconList[i]->setPixmap(pixmap);
// 设置天气类型标签的最大宽度为父窗口宽度的1/6.5
mWeaTypeList[i]->setMaximumWidth(ui->widget02->width()/6.5);
// 在天气类型标签上显示天气类型
mWeaTypeList[i]->setText(days[i].mWeathType);
// 获取空气质量等级
QString airQ = days[i].mAirq;
// 在空气质量标签上显示空气质量等级
mAirqList[i]->setText(airQ);
// 根据空气质量等级设置不同的背景颜色和样式
if( airQ == "优"){
// 空气质量为"优"时,设置背景色为浅绿色,圆角7px,文字色为浅灰色
mAirqList[i]->setStyleSheet("background-color: rgb(150, 213, 32); border-radius: 7px; color: rgb(230, 230, 230)");
}
if( airQ == "良"){
// 空气质量为"良"时,设置背景色为橙色
mAirqList[i]->setStyleSheet("background-color: rgb(208, 107, 39); border-radius: 7px; color: rgb(230, 230, 230)");
}
if( airQ == "轻度"){
// 空气质量为"轻度"污染时,设置背景色为浅红色
mAirqList[i]->setStyleSheet("background-color: rgb(255, 199, 199); border-radius: 7px; color: rgb(230, 230, 230)");
}
if( airQ == "中度"){
// 空气质量为"中度"污染时,设置背景色为红色
mAirqList[i]->setStyleSheet("background-color: rgb(255, 17, 17); border-radius: 7px; color: rgb(230, 230, 230)");
}
if( airQ == "重度"){
// 空气质量为"重度"污染时,设置背景色为深红色
mAirqList[i]->setStyleSheet("background-color: rgb(153, 0, 0); border-radius: 7px; color: rgb(230, 230, 230)");
}
// 处理风向显示,若为"无持续风向"则简化为"无"
if(days[i].mFx == "无持续风向")
{
mFxList[i]->setText("无");
}
else
{
mFxList[i]->setText(days[i].mFx);
}
// 查找风力信息中是否包含"转"字(如"3级转4级")
index = days[i].mFl.indexOf("转");
if(index != -1)
{
// 若包含"转",则取"转"之前的风力信息(如"3级")
mFlList[i]->setText(days[i].mFl.left(index));
}
else
{
// 若不包含"转",则直接显示原始风力信息
mFlList[i]->setText(days[i].mFl);
}
}
// 特殊处理前三天的星期显示,分别改为"今天"、"明天"、"后天"
mDayList[0]->setText("今天");
mDayList[1]->setText("明天");
mDayList[2]->setText("后天");
// 触发窗口重绘,确保所有UI更新生效
update();
}
我来总结一下我们是怎么解决上述三个问题的。
- 首先,针对城市名显示,我们要知道只有在搜索城市时才会切换城市,这时需要更新城市名,而刚开始默认的城市为默认值,因此我们可以设置一个枚举类型,当搜索城市时,会更新枚举变量(下一节详细介绍),我们只需要检查枚举变量的类型,来确定对应的城市后缀即可。
- 针对天气图标的显示,我们使用QMap数据结构,即每个天气类型映射一个资源文件路径,我们在构造函数中加入这些映射关系,如下图,在设置图标时,可以通过解析到的天气类型来找到对应的Icon资源文件路径。

- 针对空气质量显示,根据解析的空气质量,设置不同的Label背景样式。
4. 城市搜索功能
在搜索栏中输入其他城市名时,能够重新请求对应的HTTP数据,为此我们需要组包请求URL,我们先来分析一下我们默认请求的URL链接。
http://gfeljm.tianqiapi.com/api?unescape=1&version=v9&appid=48435331&appsecret=A24ayNLj&cityid=101110206
注意到URL末尾的cityid=101110206,这个字段用于表示不同的城市,我们要请求不同的城市只需修改这部分城市码即可,那么怎么根据城市名获取到这些城市码呢?
为此我们有一个JSON文件,其中包含了三千多个城市名和城市码,这样可以通过解析这个JSON数据,构造我们的城市和城市码的Map数据结构即可。
如下图为对应的JSON资源文件,我们可以看到它是一个数组类型,其中每个元素又是Object类型,分别表示每个城市及其信息。
为此,我们引入了一个类CityCodeUtils用于专门根据城市名提取城市码,以下是该类的定义:
class CityCodeUtils : public QObject
{
Q_OBJECT // 必须添加Q_OBJECT宏,用于元对象编译
public:
enum LastWord {
Shi,
Qu,
Xian,
ShiOrXian
};//定义枚举类型,用来标记当前搜索的是县,市还是区
int cityChangeFlag;//用于保存枚举类型
CityCodeUtils(QObject *parent = nullptr);//构造函数
QMap<QString,QString> cityMap;//定义的Map数据结构,用于保存城市名和城市码的映射关系
QString getCityCodeFromName(QString cityName);//根据QString类型的城市名得到城市码
void initCityMap();//初始化Map数据结构,即解析JSON资源文件
};
// CityCodeUtils类的构造函数,初始化父对象
CityCodeUtils::CityCodeUtils(QObject *parent) : QObject(parent)
{
// 初始化城市变更标志为"县"(默认值),因为默认解析的URL为武功县的天气情况
cityChangeFlag = CityCodeUtils::Xian;
}
// 根据城市名称获取对应的城市代码
// 参数: cityName - 城市名称字符串
// 返回值: 对应的城市代码字符串,若未找到则返回空字符串
QString CityCodeUtils::getCityCodeFromName(QString cityName)
{
// 检查城市映射表是否为空,若为空则初始化映射表
if(cityMap.isEmpty())
{
initCityMap();
}
// 声明QMap迭代器,用于遍历城市映射表
QMap<QString,QString>::iterator it;
// 处理包含"县"或"区"的城市名称(若用户输入城市全称的情况,如:武功县而不是武功,海淀区,而不是海淀)
if(cityName.contains("县") || cityName.contains("区"))
{
// 在映射表中查找完整城市名称(包含"县"或"区")
it = cityMap.find(cityName);
// 若未找到该城市
if(it == cityMap.end())
{
return ""; // 返回空字符串
}
else{
// 找到城市,设置标志为"市或县"
cityChangeFlag = CityCodeUtils::ShiOrXian;
return it.value(); // 返回找到的城市代码
}
}
// 若城市名称不包含"县"或"区",直接在映射表中查找
it = cityMap.find(cityName);
// 若未找到该城市
if(it == cityMap.end()){
// 尝试在城市名称后添加"市"再查找(如"北京"→"北京市")
it = cityMap.find(cityName+"市");
// 若仍未找到
if(it == cityMap.end()){
// 尝试在城市名称后添加"县"再查找(如"密云"→"密云县")
it = cityMap.find(cityName+"县");
// 若找到对应县城
if(it != cityMap.end())
{
// 设置标志为"县"
cityChangeFlag = CityCodeUtils::Xian;
return it.value(); // 返回找到的城市代码
}
else{
// 尝试在城市名称后添加"区"再查找(如"海淀"→"海淀区")
it = cityMap.find(cityName+"区");
// 若找到对应区
if(it != cityMap.end())
{
// 设置标志为"区"
cityChangeFlag = CityCodeUtils::Qu;
return it.value(); // 返回找到的城市代码
}
else{
// 所有尝试均未找到,返回空字符串
return "";
}
}
}
// 找到以"市"结尾的城市
cityChangeFlag = CityCodeUtils::Shi;
return it.value(); // 返回找到的城市代码
}
else
{
// 直接找到城市(如,输入北京直接找到)
cityChangeFlag = CityCodeUtils::Shi;
return it.value(); // 返回找到的城市代码
}
}
// 初始化城市映射表,从JSON文件中读取城市名称和对应代码
void CityCodeUtils::initCityMap()
{
// 创建QFile对象,关联城市代码JSON文件(资源文件路径)
QFile file(":/citycode.json");
// 尝试以只读方式打开文件
if(!file.open(QIODevice::ReadOnly))
{
// 打开失败时输出调试信息
qDebug()<<"打开城市表失败";
}
// 读取文件全部内容到字节数组
QByteArray rawData = file.readAll();
// 关闭文件
file.close();
// 将字节数组解析为QJsonDocument对象
QJsonDocument jsonDoc = QJsonDocument::fromJson(rawData);
// 检查解析后的JSON文档是否为数组类型
if(jsonDoc.isArray())
{
// 将JSON文档转换为QJsonArray对象(存储城市列表)
QJsonArray citys = jsonDoc.array();
// 遍历数组中的每个元素(每个元素代表一个城市)
for(QJsonValue value : citys)
{
// 检查当前元素是否为JSON对象
if(value.isObject())
{
// 将当前元素显式转换为QJsonObject(城市信息对象)
QJsonObject cityObj = value.toObject();
// 从城市对象中获取"city_name"字段的值(城市名称)
QString cityName = cityObj["city_name"].toString();
// 从城市对象中获取"city_code"字段的值(城市代码)
QString cityCode = cityObj["city_code"].toString();
// 将城市名称和代码插入到映射表中
cityMap.insert(cityName,cityCode);
}
}
}
}
在实现了以上类后,我们在搜索按钮的槽函数中直接调用,然后组包URL并重新发送HTTP请求即可,如下所示:
// 搜索按钮(btnSearch)的点击事件处理函数
// 用户点击搜索按钮后,触发该函数执行城市天气查询的核心逻辑
void Widget::on_btnSearch_clicked()
{
// 1. 获取用户输入的城市名称
// 从UI界面的城市输入框(lineEditCity)中读取用户输入的文本,作为目标城市名
QString cityNameFromUsr = ui->lineEditCity->text();
// 2. 根据用户输入的城市名,获取对应的城市代码
// 调用CityCodeUtils工具类的getCityCodeFromName方法,将城市名转换为接口所需的城市编码
// 城市代码用于天气API精准定位城市
QString cityCode = cityCodeUtils.getCityCodeFromName(cityNameFromUsr);
// 3. 判断城市代码是否有效(非空)
// 若城市代码不为空,说明用户输入的城市名有效,可发起天气请求
if(cityCode!=NULL)
{
// 3.1 拼接完整的天气API请求URL
// 在基础URL(strUrl)后追加城市代码参数(&cityid=xxx),生成针对目标城市的请求地址
strUrl += "&cityid=" + cityCode;
// 3.2 发起网络请求获取天气数据
// 使用QNetworkAccessManager(manager)的get方法发送GET请求
// QNetworkRequest封装请求URL,manager会异步处理该请求(后续通过信号槽接收响应)
manager->get(QNetworkRequest(QUrl(strUrl)));
}
// 4. 若城市代码为空,说明用户输入的城市名无效(未找到对应城市)
else
{
// 4.1 创建错误提示对话框
// QMessageBox用于显示弹窗提示,此处为错误类型提示
QMessageBox msg;
// 4.2 设置对话框标题为"错误"
msg.setWindowTitle("错误");
// 4.3 设置对话框图标为关键错误图标(红色叉号),增强视觉提示
msg.setIcon(QMessageBox::Critical);
// 4.4 设置对话框提示文本,指导用户输入正确格式的城市名
msg.setText("请输入正确的城市名(例如:北京)!");
// 4.5 设置对话框的标准按钮为"确定"(仅一个按钮,点击后关闭对话框)
msg.setStandardButtons(QMessageBox::Ok);
// 4.6 显示对话框并等待用户操作(模态对话框,阻塞当前线程直到用户点击按钮)
msg.exec();
}
}
5. 近期温度变化曲线绘制
在绘制温度变化曲线时,一般我们重新绘图事件paintEvent函数,当发生对应的事件时执行绘图事件函数,但是需要注意的是在paintEvent函数中绘制的图像是在整个widget中的,可能会存在遮挡,导致绘制的图像显示不出来。因此,这里我们使用事件过滤器,这样有一个好处就是我们可以在我们想要绘图的控件上安装事件过滤器,当检测到此控件上发生绘图事件时再执行我们的绘图操作,这样可以保证我们绘制的图像是在该控件上的,而不是整个widget。具体实现如下:
-
首先在构造函数中分别给显示曲线的区域(widget0404、widget0405)安装事件过滤器:
ui->widget0404->installEventFilter(this); ui->widget0405->installEventFilter(this); -
接着重写事件过滤器,在事件过滤器中检测事件发生的控件和事件类型,并进行绘图。
bool Widget::eventFilter(QObject *watched, QEvent *event)
{
if(watched ==ui->widget0404 && event->type() == QEvent::Paint)//当widget0404 控件收到绘图事件时
{
drawTempLineHigh();//绘制近六天高温曲线图
return true;//返回true表示当前事件处理完毕,不再进行传播
}
if(watched ==ui->widget0405 && event->type() == QEvent::Paint)//当widget0405 控件收到绘图事件时
{
drawTempLineLow();//绘制近六天低温曲线图
return true;//返回true表示当前事件处理完毕,不再进行传播
}
return QWidget::eventFilter(watched,event);//若是其他控件或事件类型交给Widget的父类的处理(不影响其他控件的事件处理)
}
- 绘图函数详解
// 在指定UI组件(widget0404)上绘制未来6天的最高温度折线图
void Widget::drawTempLineHigh()
{
// 1. 创建QPainter对象,指定绘图目标为UI上的widget0404组件
// QPainter是Qt绘图核心类,所有绘图操作通过该对象执行
QPainter painter(ui->widget0404);
// 2. 设置绘图渲染提示:启用抗锯齿
// 抗锯齿可让绘制的线条、图形边缘更平滑,避免锯齿状模糊
painter.setRenderHint(QPainter::Antialiasing, true);
// 3. 设置画笔和画刷颜色为黄色
// 画笔(Pen)用于绘制线条、轮廓;画刷(Brush)用于填充图形内部
// 此处统一设为黄色,对应最高温度折线的视觉标识
painter.setBrush(QBrush(Qt::yellow));
painter.setPen(QPen(Qt::yellow));
// 4. 声明变量:用于计算温度平均值、总和、偏移量及绘图基准线
int avg; // 6天最高温度的平均值(用于确定折线图的基准)
int sum = 0; // 6天最高温度的总和(用于计算平均值)
int offset = 0; // 单个温度与平均值的偏移量(用于确定Y轴坐标)
// 计算绘图区域的Y轴基准线:取widget0404高度的70%处(避免折线超出可视区域)
int middle = ui->widget0404->height() * 0.7;
// 5. 计算6天最高温度的总和
for (int i = 0; i < 6; i++) {
// 将days数组中第i天的最高温度(字符串)转为整数,累加到sum
sum += days[i].mTempHigh.toInt();
}
// 6. 计算6天最高温度的平均值
avg = sum / 6;
// 7. 声明QPoint数组,存储6天最高温度对应的绘图坐标点
QPoint points[6];
// 8. 遍历6天数据,计算每个温度对应的坐标点并绘制基础元素
for (int i = 0; i < 6; i++) {
// 8.1 计算X轴坐标:与空气质量标签(mAirqList[i])水平居中对齐
// mAirqList[i]->x():获取空气质量标签的X坐标
// mAirqList[i]->width()/2:获取标签宽度的一半,确保温度点在标签正中间
points[i].setX(mAirqList[i]->x() + mAirqList[i]->width() / 2);
// 8.2 计算Y轴坐标:基于平均值的偏移量调整
// (当天最高温度 - 平均值)*2:放大偏移量,让温度变化更明显
// middle - offset:以基准线为中心,温度高于平均值则向上偏移,低于则向下偏移
offset = (days[i].mTempHigh.toInt() - avg) * 2;
points[i].setY(middle - offset);
// 8.3 绘制温度点:在计算出的坐标上画一个半径为3的黄色圆形
// QPoint(points[i]):坐标点,3,3分别为椭圆的宽和高(相等时为圆形)
painter.drawEllipse(QPoint(points[i]), 3, 3);
// 8.4 绘制温度文本:在温度点左上方(X-10,Y-10)显示温度值(带"°"符号)
// 偏移10像素是为了避免文本与圆形重叠,提升可读性
painter.drawText(points[i].x() - 10, points[i].y() - 10, days[i].mTempHigh + "°");
}
// 9. 连接相邻坐标点,绘制最高温度折线
for (int i = 0; i < 5; i++) {
// 从第i个点绘制直线到第i+1个点,形成连续的温度趋势线
painter.drawLine(points[i], points[i + 1]);
}
}
// 在指定UI组件(widget0405)上绘制未来6天的最低温度折线图
// 逻辑与最高温度折线图一致,仅视觉颜色和数据来源(最低温度)不同
void Widget::drawTempLineLow()
{
// 1. 创建QPainter对象,指定绘图目标为UI上的widget0405组件
QPainter painter(ui->widget0405);
// 2. 启用抗锯齿,使线条边缘平滑
painter.setRenderHint(QPainter::Antialiasing, true);
// 3. 设置画笔和画刷颜色为青色(RGB:70, 192, 203)
// 青色作为最低温度折线的视觉标识,与最高温度的黄色区分
painter.setBrush(QColor(70, 192, 203));
painter.setPen(QColor(70, 192, 203));
// 4. 声明变量:功能与最高温度折线图一致
int avg; // 6天最低温度的平均值
int sum = 0; // 6天最低温度的总和
int offset = 0; // 单个最低温度与平均值的偏移量
// 绘图基准线:取widget0405高度的70%处
int middle = ui->widget0405->height() * 0.7;
// 5. 计算6天最低温度的总和
for (int i = 0; i < 6; i++) {
// 将days数组中第i天的最低温度(字符串)转为整数,累加到sum
sum += days[i].mTempLow.toInt();
}
// 6. 计算6天最低温度的平均值
avg = sum / 6;
// 7. 声明QPoint数组,存储6天最低温度对应的绘图坐标点
QPoint points[6];
// 8. 遍历6天数据,计算每个最低温度对应的坐标点并绘制基础元素
for (int i = 0; i < 6; i++) {
// 8.1 计算X轴坐标:与空气质量标签(mAirqList[i])水平居中对齐
points[i].setX(mAirqList[i]->x() + mAirqList[i]->width() / 2);
// 8.2 计算Y轴坐标:基于最低温度平均值的偏移量调整
offset = (days[i].mTempLow.toInt() - avg) * 2;
points[i].setY(middle - offset);
// 8.3 绘制最低温度点:半径为3的青色圆形
painter.drawEllipse(QPoint(points[i]), 3, 3);
// 8.4 绘制最低温度文本:在温度点左上方显示温度值(带"°"符号)
painter.drawText(points[i].x() - 10, points[i].y() - 10, days[i].mTempLow + "°");
}
// 9. 连接相邻坐标点,绘制最低温度折线
for (int i = 0; i < 5; i++) {
painter.drawLine(points[i], points[i + 1]);
}
}
6. 其他功能
(1)页面退出功能
为了使UI整体风格统一,我们在构造函数中调用了this->setWindowFlags(Qt::FramelessWindowHint);这个函数用于设置窗口的样式,Qt::FramelessWindowHint枚举代表要移除窗口默认的边框和标题栏为了大家能够清除的看到它的作用,我们注释这行,再来看看UI效果:
当我们加上这行之后,窗口就无法通过X关闭,以及鼠标拖动窗口移动,因此需要我们自行实现,本节我们先看推出功能实现,在构造函数中加入以下代码:
// 1. 创建QMenu菜单对象,指定当前窗口(this)为其父对象
// QMenu是Qt中的菜单容器,用于承载菜单项(QAction);设置父对象后,菜单会随父窗口生命周期自动释放,避免内存泄漏
menuQuit = new QMenu(this);
// 2. 为菜单设置样式表,修改菜单项的文本颜色
// "QMenu::item" 是样式表选择器,特指菜单中的所有菜单项;"color:white" 表示将菜单项的文本颜色设为白色
menuQuit->setStyleSheet("QMenu::item {color:white}");
// 3. 创建QAction菜单项对象,配置图标、文本和父对象
// QAction是菜单中的可交互项(如“退出”按钮),参数依次为:
// - QIcon(":/res/close.png"):为菜单项设置图标,图标路径来自项目资源文件(:/ 是Qt资源文件的根路径)
// - tr("退出"):设置菜单项显示文本,tr() 用于支持多语言国际化
// - this:指定当前窗口为父对象,确保动作随父窗口自动释放
QAction *closeAct = new QAction(QIcon(":/res/close.png"), tr("退出"), this);
// 4. 将创建的菜单项(closeAct)添加到菜单(menuQuit)中
// 只有添加到菜单的QAction,才会在菜单展开时显示给用户
menuQuit->addAction(closeAct);
// 5. 连接菜单项的“触发信号”到lambda表达式,实现点击菜单项的功能
// - closeAct->triggered:QAction的信号,当用户点击该菜单项时触发
// - [=](){}:lambda表达式(匿名函数),作为信号的响应函数,[=]表示捕获当前作用域的变量(按值捕获)
// - this->close():lambda的核心逻辑,调用当前窗口的close()方法,关闭整个窗口
connect(closeAct, &QAction::triggered, [=](){
this->close();
});
此外,还要能够显示菜单,我们这里的方法是当鼠标右键点击界面任何位置后显示:
void Widget::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::RightButton)//当是鼠标右击事件
{
menuQuit->exec(QCursor::pos());//显示菜单
}
if(event->button() == Qt::LeftButton)
{
moffset = event->globalPos() - this->pos();
}
}
(2)鼠标拖拽页面
鼠标拖拽页面即能够移动界面位置,在有窗口默认标题的项目中,我们可以通过左击边框区域来移动窗口位置,在该项目中我们删除了边框,因此需要我们自行实现。如上一节相似,我们首先在鼠标点击事件中得到鼠标左击事件,然后再进行处理:
void Widget::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::RightButton)
{
menuQuit->exec(QCursor::pos());
}
if(event->button() == Qt::LeftButton)//当是鼠标左键事件
{
//鼠标当前位置 event->globaPos();(相对于整个屏幕的位置)
//this->pos();窗口当前位置(窗口右上角在屏幕的位置)
moffset = event->globalPos() - this->pos();//计算偏移
}
}
要理解上述代码不同的位置,我们以一个图来解释:

如上图,显示了我们要移动一个窗口到新位置,鼠标点击位置我们可以通过event->globalPos()函数求得,核心是需要得到新的窗口位置,然后掉用this->move(Point)即可。由上图可知窗口位置(this->pos())=鼠标位置(event->globalPos())-偏移(moffset)。因此,我先在鼠标点击事件中计算出偏移,然后随着鼠标的移动,不断计算新的窗口位置,并调用move即可,如下代码所示:
void Widget::mouseMoveEvent(QMouseEvent *event)
{
this->move(event->globalPos() - moffset);
}
(3)回车城市搜索
回城搜索涉及到QLineEdit的returnPressed信号,如下图所示:
我在只需在槽函数中调用搜索按钮的槽函数即可:
void Widget::on_lineEditCity_returnPressed()
{
on_btnSearch_clicked();
}
(4)GIF动图美化UI
// 1. 创建QMovie对象,用于加载和控制GIF动图
// QMovie是Qt提供的动图处理类,支持GIF等格式的动图加载、播放、暂停等操作
// 参数":/res/taikongren.gif"是动图文件的资源路径:
// - ":/" 表示Qt项目的资源文件根目录(需提前在.qrc资源文件中配置该GIF路径)
// - "res/taikongren.gif" 是动图在资源文件中的相对路径
// 此处未显式指定父对象(第二个参数默认 nullptr),需注意后续手动管理内存避免泄漏
QMovie *movie = new QMovie(":/res/taikongren.gif");
// 2. 将创建的QMovie对象与UI中的标签控件(labelDonghua)绑定
// ui->labelDonghua 是Qt Designer中设计的QLabel控件(命名为"labelDonghua",用于显示动图)
// setMovie() 是QLabel的成员方法,作用是将QMovie的动图输出关联到当前标签,
// 使标签成为动图的显示载体(后续动图播放时,画面会实时渲染到该标签上)
ui->labelDonghua->setMovie(movie);
// 3. 启动动图播放
// start() 是QMovie的成员方法,调用后动图会开始播放(默认循环播放,直到调用stop()或对象销毁)
// 播放过程中,QMovie会自动更新帧,并通过setMovie()绑定的QLabel实时显示最新帧
movie->start();
五、可执行文件与源码获取
1.可执行文件

2. 源代码

3.百度网盘链接
通过网盘分享的文件:3. 天气预报
链接: https://pan.baidu.com/s/1l6hInaHZS4oB1Jo5n_NS7A 提取码: tm9d
六、总结
天气预报项目总结
本天气预报项目基于Qt框架开发,实现了完整的天气查询与展示功能,整合了网络请求、数据解析、UI交互及个性化优化等核心模块。
项目核心流程为:通过QNetworkAccessManager发起HTTP请求获取天气API数据,利用QJsonDocument、QJsonObject与QJsonArray解析JSON数据包,将7天天气数据存储到自定义Day类数组中,再通过updateUI函数更新至界面,实现城市名、温度、天气类型、空气质量等信息的展示。
为提升实用性,项目设计了城市搜索功能:通过CityCodeUtils类解析城市码JSON文件,构建城市名与代码的映射表,支持用户输入城市名匹配对应城市码,重新发起请求。同时,通过QPainter与事件过滤器绘制最高/最低温度折线图,避免绘图遮挡;还优化了用户体验,如无边框窗口设计、鼠标拖拽移动、右键退出菜单、回车搜索、GIF动图美化界面等,解决了默认边框缺失后的功能补全问题。
整体项目覆盖Qt多模块知识,实现了“请求-解析-显示-交互”的完整闭环,兼具功能性与用户体验,是Qt桌面应用开发的典型实践。
更多推荐

所有评论(0)