
简介基于C与QT开发的天气预报系统源码是面向计算机相关专业学生课程设计和期末大作业的高分参考项目覆盖从天气API数据获取、界面布局到交互逻辑的完整实现。资源包共74个文件含6个头文件、5个C源文件、4个UI界面文件及1个项目配置文件另有大量PNG/JPG图片资源美化界面并附带README和“qt八股文”笔记压缩包整体约21.35MB。目前已有65人学习下载适合需要项目实战或备战课程设计的初学者进阶者。源码通过实例展示了面向对象设计、QT信号槽机制、网络请求解析及多线程数据刷新等关键技能同时提供JSON城市代码和可运行的EXE程序方便直接运行调试与对照研究是巩固C综合开发能力的优质素材。1. 一个从“能跑”到“能拿高分”的 C 天气预报大作业差在哪期末答辩现场最常见的一幕是每组都做了一个窗口一个输入框一个按钮点下去把网上拉回来的 JSON 字符串拆一拆拼成一行文字显示出来。老师翻了翻代码问一句“这个 QLabel 上的内容是在哪个类里更新的”答不上来分数就停在 75。同样是基于 C 和 QT 的天气预报系统高分项目和普通项目的分水岭不在功能多而在三点数据模型是不是单独设计的、网络层和界面层有没有解耦、异常情况是不是都有明确兜底。本文按“先定 C 数据类 → 再写网络请求和 JSON 解析 → 最后拼 QT 界面”的顺序把这个大作业拆成可以直接照做的五步每一步都给你类定义、信号槽写法和答辩时可讲的参数细节。2. 先定数据模型用 C 类把天气和预报从 JSON 里解耦出来2.1 为什么拿到需求先画类图而不是先拖控件很多同学打开 Qt Designer 就直接往界面上扔控件等后台逻辑写完了再回头接数据结果界面类里塞满了字符串裁剪、JSON 取值和格式化逻辑。老师一眼看过去MainWindow 一个类干了三件事不能算好的 C 面向对象设计。我的做法是反着来先定义“天气长什么样”再定义“数据从哪里来”最后才轮到“界面怎么画”。这样至少有三个实际好处第一天气数据结构是稳定的API 返回什么、界面显示什么都由它中转将来换 API 只改一个类第二答辩时老师问“这个系统分了哪些层”你可以直接指着头文件回答第三网络解析代码写起来清爽不会出现上千行的 MainWindow。2.2 WeatherData 和 DailyForecast一个能容纳当天和未来几天的 C 容器天气这个东西拆开看是两块数据当天的实时观测温度、湿度、风向、风力、天气现象以及未来若干天的预报每天的最高最低温和白天/夜间现象。C 里最自然的表达是两个类型一个描述“某一天的预报”一个描述“整个系统的天气数据集合”。// weatherdata.h #ifndef WEATHERDATA_H #define WEATHERDATA_H #include QString #include QVector // 一天的预报属于内部结构因此单独放一个 struct struct DailyForecast { QString date; // 预报日期格式 2026-05-20 QString condDay; // 白天天气现象如 晴、多云、小雨 int tempMax 0; // 最高温度摄氏度缺省给 0 int tempMin 0; // 最低温度摄氏度缺省给 0 }; // 对外暴露的天气数据模型 class WeatherData { public: // ---- 城市与时间 ---- QString cityName; // 城市显示名 QString updateTime; // 数据更新时间 // ---- 实时观测 ---- double temp 0.0; // 当前温度 int humidity 0; // 相对湿度百分比 QString windDir; // 风向如 东南风 int windScale 0; // 风力等级如 3 QString condition; // 当前天气现象 // ---- 多日预报 ---- QVectorDailyForecast daily; // 长度由 API 决定一般为 3~7 天 // 取预报天数界面循环时用 int dailyCount() const { return daily.size(); } }; #endif // WEATHERDATA_H这里要特别说明几处 C 的设计用意。DailyForecast用 struct因为它只是被动的数据载体没有行为WeatherData用 class 且字段默认是 public是因为它本质上是 DTO数据传输对象不封装业务逻辑面试和答辩时很少有人会追问“你为什么让成员公有”但如果你把它们写成 private 再堆 getter/setter反而显得为了封装而封装。成员初始化使用了 C 11 的默认成员初始化器 0而不是在构造函数里赋一遍初值这样即使某次解析漏了字段温度也不会是随机值这是个能讲两句的细节。2.3 WeatherAPI 类把网络逻辑隔离在界面之外数据模型定好之后要写一个专门负责“发请求、收响应、转成 WeatherData”的类。这个类在架构上属于 service 层它不应该知道 QLabel 是什么也不应该引用 MainWindow 的头文件只通过信号把结果抛出去。它的头文件长这样// weatherapi.h #ifndef WEATHERAPI_H #define WEATHERAPI_H #include QObject #include QNetworkAccessManager #include QNetworkReply #include weatherdata.h class WeatherAPI : public QObject { Q_OBJECT public: explicit WeatherAPI(const QString apiKey, QObject *parent nullptr); // 根据城市 ID 请求实时天气与预报 void requestWeather(const QString cityId); signals: void weatherReady(const WeatherData data); // 解析成功携带数据 void errorOccurred(const QString message); // 网络或解析失败 private slots: void onReplyFinished(QNetworkReply *reply); private: QNetworkAccessManager *manager_ nullptr; QString apiKey_; }; #endif // WEATHERAPI_H成员类型职责manager_QNetworkAccessManager*全局唯一的网络管理者负责发送请求apiKey_QString你在天气服务商控制台申请到的访问密钥weatherReadysignal数据解析完成后由 MainWindow 连接更新界面errorOccurredsignal网络错误、JSON 解析失败、业务码异常时统一走这里QNetworkAccessManager在整个程序生命周期里只应该创建一个实例重复 new 会浪费 socket 资源。所以我在构造函数里把它初始化一次parent传this由 WeatherAPI 负责它的生命周期这样也规避了“谁 delete 谁”的 C 内存管理陷阱。城市 ID 而不是城市名作为参数是因为中文城市名在 URL 里要编码而且同名城市很多先通过另一个接口查出 ID 再查天气是通用做法。3. 在 QT 里把界面拼起来布局、刷新按钮和信号槽3.1 用 QLineEdit、QComboBox 和 QLabel 搭出天气面板主干界面不需要复杂一个输入城市的关键字控件、一个查询按钮、一个显示大块天气信息的区域就够了。考虑到答辩时可能需要演示多个城市我会在输入框旁边再加一个历史城市下拉框用户体验更好还多一个控件知识点。// mainwindow.cpp 构造函数中的界面搭建片段 auto *central new QWidget(this); setCentralWidget(central); auto *mainLayout new QVBoxLayout(central); // 顶部下拉框历史城市 输入框 查询按钮 auto *topBar new QHBoxLayout; historyCombo_ new QComboBox(this); historyCombo_-setMinimumWidth(120); cityEdit_ new QLineEdit(this); cityEdit_-setPlaceholderText(QStringLiteral(输入城市名如 北京)); searchBtn_ new QPushButton(QStringLiteral(查询), this); topBar-addWidget(historyCombo_); topBar-addWidget(cityEdit_, 1); // 第二个参数让输入框占据剩余宽度 topBar-addWidget(searchBtn_); mainLayout-addLayout(topBar); // 中部天气信息标签区每个信息单独一个 QLabel infoLabel_ new QLabel(this); infoLabel_-setAlignment(Qt::AlignCenter); infoLabel_-setWordWrap(true); infoLabel_-setText(QStringLiteral(请输入城市后查询)); mainLayout-addWidget(infoLabel_, 1);cityEdit_后面参数1表示它在QHBoxLayout中占的拉伸比例这样窗口横向缩放时输入框跟着变宽按钮和下拉框保持固定宽度。infoLabel_勾选setWordWrap(true)是为了防止未来几天预报拼成一行时被截断这个属性在展示多行文本时很有用。整个窗口的默认大小可以设为resize(480, 420)正好是一个演示终端不会太大、贴到论文截图里又看得清的大小。3.2 用 QNetworkAccessManager 发请求并搞清楚回调在哪个线程查询按钮按下后要做的事很简单取城市 ID调用 WeatherAPI 的requestWeather然后把两个信号连到 MainWindow 的槽上。连接要放在构造函数里不能在按钮点击的 lambda 里反复 connect那会导致同一个响应被处理多次。// mainwindow.cpp 中信号槽连接与刷新入口 api_ new WeatherAPI(yourApiKey, this); connect(searchBtn_, QPushButton::clicked, this, [this]() { const QString keyword cityEdit_-text().trimmed(); if (keyword.isEmpty()) { QMessageBox::warning(this, QStringLiteral(提示), QStringLiteral(城市名不能为空)); return; } // 先用城市关键字查 ID拿到 ID 后再查天气见 3.3 与 4.2 searchCityId(keyword); }); connect(api_, WeatherAPI::weatherReady, this, MainWindow::updateWeatherUI); connect(api_, WeatherAPI::errorOccurred, this, MainWindow::showError);QNetworkAccessManager::get()返回后立刻执行下一行代码真正的网络收发在 Qt 的事件循环里异步完成所以不会卡住 UI。这也是它和QThread::sleep加同步 socket 方案的本质区别不需要手动开子线程。回调回来时默认连接方式是Qt::AutoConnection发射信号和接收槽在同一线程就直接调用在不同线程就转成队列调用对写大作业来说你只需要记住一点不要在回调里做耗时超过几十毫秒的图片加载或文件写入界面才会一直流畅。3.3 解析 JSON 的思路先取对象再取字段缺了什么都要兜底接口返回的数据是一个 JSON 字符串QT 里用QJsonDocument、QJsonObject和QJsonArray三层结构去解析。解析时最容易出的问题是想当然地“链式取值”比如doc[now][temp]一旦哪一层字段不存在拿到的就是QJsonValue::Undefined再转成字符串就变成空界面显示“温度℃”这种滑稽结果。所以每一层取值后都要判断一次类型。// weatherapi.cpp 中 JSON 解析的核心片段 void WeatherAPI::onReplyFinished(QNetworkReply *reply) { if (reply-error() ! QNetworkReply::NoError) { emit errorOccurred(QStringLiteral(网络请求失败%1) .arg(reply-errorString())); reply-deleteLater(); return; } const QByteArray raw reply-readAll(); // 一次性读完响应体 reply-deleteLater(); // 释放 replyC 内存管理要点 QJsonParseError parseError; const QJsonDocument doc QJsonDocument::fromJson(raw, parseError); if (parseError.error ! QJsonParseError::NoError || !doc.isObject()) { emit errorOccurred(QStringLiteral(JSON 解析失败%1) .arg(parseError.errorString())); return; } const QJsonObject root doc.object(); // 业务状态码非 200说明城市名不存在或 key 额度不足 if (root.value(code).toString() ! 200) { emit errorOccurred(QStringLiteral(接口返回错误code%1) .arg(root.value(code).toString())); return; } WeatherData wd; const QJsonObject now root.value(now).toObject(); wd.temp now.value(temp).toDouble(); // 温度用 double 保留小数 wd.condition now.value(text).toString(); wd.humidity now.value(humidity).toInt(); wd.windDir now.value(windDir).toString(); wd.windScale now.value(windScale).toInt(); wd.updateTime root.value(updateTime).toString(); // daily 是一个数组这里只取前 3 天展示 for (const QJsonValue v : root.value(daily).toArray()) { const QJsonObject d v.toObject(); DailyForecast df; df.date d.value(fxDate).toString(); df.condDay d.value(textDay).toString(); df.tempMax d.value(tempMax).toInt(); df.tempMin d.value(tempMin).toInt(); wd.daily.append(df); if (wd.daily.size() 3) break; // 控制预报天数界面不至于拥挤 } if (wd.cityName.isEmpty()) { wd.cityName cityName_; } emit weatherReady(wd); }这里的reply-deleteLater()是 QT 里释放对象的标准姿势它不会立刻 delete而是等当前事件循环安全点再回收避免在信号处理过程中对象被提前销毁。toInt()、toDouble()、toString()这些方法在字段缺失时都会返回默认值0 或空串所以即使接口临时改了字段名程序也只是显示默认值而不是崩溃。3.4 更新界面用 QString::arg 把多段数据拼成一行可读的文本拿到 WeatherData 之后MainWindow 的槽函数负责把它变成屏幕上能看的文字。我一般会把“拼格式”单独拆成一个成员函数这样老师问你“界面更新逻辑在哪”时你能准确指到formatWeatherText而不是满屏找。// mainwindow.cpp 更新界面与格式化输出 void MainWindow::updateWeatherUI(const WeatherData wd) { QString text; text QStringLiteral(城市%1\n).arg(wd.cityName); text QStringLiteral(当前%1 %2℃\n) .arg(wd.condition) .arg(wd.temp, 0, f, 1); // 保留一位小数 text QStringLiteral(湿度%1%% 风力%2%3\n) .arg(wd.humidity) .arg(wd.windDir) .arg(wd.windScale); text QStringLiteral(更新时间%1\n\n).arg(wd.updateTime); text QStringLiteral(未来三天\n); for (const DailyForecast df : wd.daily) { text QStringLiteral( %1 %2 %3~%4℃\n) .arg(df.date) .arg(df.condDay) .arg(df.tempMin) .arg(df.tempMax); } infoLabel_-setText(text); // 把查询过的城市加入历史下拉框避免重复输入 if (historyCombo_-findText(wd.cityName) -1) { historyCombo_-addItem(wd.cityName); } }QString::arg连续拼接有个容易踩的坑当字符串里同时存在%1和%字面量时会解析错乱。比如第 8 行要想显示“湿度45%”必须写%1%%第一个%1被替换成数字第二个%%转义成一个百分号。小于 10 的温度在arg(wd.temp, 0, f, 1)中会用空格补位实测效果是小数点对齐视觉上整齐很多。4. 从“能显示”到“能答辩”错误处理、城市切换和图标映射4.1 把错误分成三类并在界面上给出明确文案网络请求可能失败的原因实在太多了如果所有错误都弹一个笼统的“查询失败”答辩时老师一定会追问“哪些场景会走这个分支”。我习惯在代码里把错误分类处理并让提示文案可读失败场景判断方式提示文案断网 / 域名解析失败 / 超时reply-error() ! NoError网络请求失败请检查网络连接城市不存在或 key 额度耗尽根节点code不是 200接口返回错误codexxx返回内容不是合法 JSONQJsonParseError数据解析失败可能为接口升级三种错误统一走errorOccurred信号在 MainWindow 的一个槽里处理void MainWindow::showError(const QString message) { infoLabel_-setText(QStringLiteral(查询出错\n%1).arg(message)); searchBtn_-setEnabled(true); // 无论成败按钮都恢复可点 searchBtn_-setText(QStringLiteral(查询)); }有个细节值得在答辩时主动提超时处理。Qt 的QNetworkRequest有setTransferTimeout(8000)接口设置 8 秒没有响应就自动放弃请求避免用户在城市名输错或网络黑洞时无限等待。transferTimeout是毫秒数0表示不启用这个是很多同学不知道的参数放代码里是明显的加分项。4.2 城市输入到天气数据的完整链路关键字查 IDID 查天气天气服务商一般提供两个接口一个负责“城市关键字 → 城市 ID”另一个负责“城市 ID → 实时和预报数据”。第一次做的时候容易想直接用城市名去查天气结果发现服务商要求先查 ID中文城市名还要做 URL 编码处理起来很麻烦。// 城市关键字查 ID 的发起函数 void MainWindow::searchCityId(const QString keyword) { searchBtn_-setEnabled(false); // 防连点等本次请求完成 searchBtn_-setText(QStringLiteral(查询中…)); QUrl url(QStringLiteral(https://your-api-endpoint/city/lookup)); QUrlQuery query; query.addQueryItem(location, keyword); // 中文会被自动 URL 编码 // “和风天气”等平台一般还需要 location 支持拼音或英文如 beijing query.addQueryItem(key, api_-apiKey()); url.setQuery(query); QNetworkRequest request(url); // 8 秒无响应视为失败 request.setTransferTimeout(8000); // 用第二个 QNetworkAccessManager 管理 lookup 请求 lookupManager_-get(request); }lookupManager_和天气请求的manager_分开是刻意为之不同语义的请求用不同的管理器响应回调里不用靠 URL 判断“这是哪一次请求”代码可读性更好。QUrlQuery负责处理查询串编码“北京”这类中文转成百分号编码后传输不用自己手动QUrl::toPercentEncoding这个细节知道的人越多作业里手写编码出 bug 的就越少。4.3 天气现象到图标的映射用 QMap 表而不是满屏 if-else天气预报接口返回的text字段是中文文本比如“晴”“多云”“小雨”。直接显示文字当然可以但如果界面里有一排小图标视觉完成度会高一个档次。把网络图片下载到本地不现实请求太多且慢比较实用的方案是本地准备一组图标用 QMap 做“天气现象 → 图片路径”的映射// weathericon.cpp 中初始化映射表的片段 QMapQString, QString buildIconMap() { QMapQString, QString iconMap; iconMap.insert(QStringLiteral(晴), QStringLiteral(:/icons/sunny.png)); iconMap.insert(QStringLiteral(多云), QStringLiteral(:/icons/cloudy.png)); iconMap.insert(QStringLiteral(阴), QStringLiteral(:/icons/overcast.png)); iconMap.insert(QStringLiteral(小雨), QStringLiteral(:/icons/rain.png)); iconMap.insert(QStringLiteral(中雨), QStringLiteral(:/icons/rain.png)); iconMap.insert(QStringLiteral(大雨), QStringLiteral(:/icons/rain.png)); iconMap.insert(QStringLiteral(雷阵雨), QStringLiteral(:/icons/storm.png)); iconMap.insert(QStringLiteral(雪), QStringLiteral(:/icons/snow.png)); return iconMap; }映射表放在独立的buildIconMap函数里而不是构造函数内初始化这样其他 UI 组件也能复用。匹配的时候取前两个字符判断“小雨、大雨、暴雨”都映射到雨图标“中雨”这种不在精确键里的值可以用QMap::lowerBound做前缀查找或者干脆用if (text.contains(雨))兜底。图标文件放进 qrc 资源文件里以:/icons/xxx.png为前缀引用发布时会把图片打进二进制可执行文件不会出现“拷了 exe 却丢了图片文件夹”的经典问题。4.4 析构和资源释放C 内存管理在 QT 里的正确姿势WeatherAPI 的manager_、MainWindow 的api_和lookupManager_都传了parent父对象析构时子对象会被自动回收这是 QT 对象树的机制。但网络回复QNetworkReply不能依赖这个因为每个请求都会创建新的 reply父对象不一定能在它想析构的时候照顾到所有 reply。因此每个回调里必须调用reply-deleteLater()这个操作对大作业来说不仅是内存管理正确性的体现也是答辩时老师最爱问的一个点。WeatherAPI::~WeatherAPI() { // 若请求还在飞行中由 QNetworkAccessManager 析构时统一清理 // reply 的 deleteLater 已放在各自的回调里这里不需要手动遍历 }注意不要试图在WeatherAPI的析构函数里delete manager_设置parent后 Qt 会负责清理手动删除反而可能造成双重释放。这一点在 C 面试题里属于“RAII 与对象树如何协作”的范畴能讲清楚它比多做两个界面特效更得分。5. 交付前的自查清单和一个视觉加分技巧5.1 五步演示脚本确保答辩现场不翻车高分项目和技术实力不一定成正比但答辩演示的流畅程度一定会影响印象分。我总结一套固定脚本启动程序 → 在下拉框选一个历史城市 → 点查询 → 展示完整天气信息 → 故意输入一个不存在的城市名让评审看到错误提示。最后一步是很多同学忽略的恰恰是它最能体现项目的健壮性。演示前把网络断开一次再恢复确认错误提示和恢复后的再次查询都正常。5.2 用 QSS 给默认控件换肤不加任何依赖QT 的默认样式在演示时显得略呆板一个低成本的做法是用 QSS 样式表覆盖几个关键控件。效果立竿见影而且不用写一行 C 逻辑。以下样式我一般直接追加在 MainWindow 构造函数里setStyleSheet(QStringLiteral( QWidget { background-color: #f5f7fa; } QPushButton { background-color: #3090ff; color: white; border-radius: 6px; padding: 6px 18px; } QPushButton:hover { background-color: #1e80ff; } QPushButton:disabled { background-color: #aac8ff; } QLineEdit, QComboBox { background-color: white; border: 1px solid #dcdfe6; border-radius: 6px; padding: 4px 8px; } ));按钮在请求发出后会被setEnabled(false)置灰QSS 里配一个:disabled状态的颜色用户就能直觉地知道“程序正在工作”而不是以为卡死了。QSS的语法类似 CSS但选择器只支持 Qt 的控件类型和属性不需要引入任何第三方库这也是大作业项目里性价比最高的观感提升手段。5.3 最后一项自查在关闭窗口前处理未完成的网络请求如果用户在请求进行中直接关闭窗口理论上析构会先销毁 manager 再销毁 replyQt 会打印一条警告。严谨一点的做法是在 MainWindow 的关闭事件里主动中断请求重写closeEvent调用abort()取消未完成的请求。虽然大作业里不写也不会判错但这份主动管理异步资源的意识恰好是“能跑”和“高质量”之间的那层窗户纸。void MainWindow::closeEvent(QCloseEvent *event) { api_-abortAll(); // 遍历并 abort 所有未完成的 QNetworkReply event-accept(); // 接受关闭事件 }abortAll的实现很简单在 WeatherAPI 里维护一个QSetQNetworkReply*每次发起请求时把 reply 加入集合在onReplyFinished里移除abortAll遍历集合调用abort()。这个模式在真实项目中叫“请求生命周期管理”写进大作业的代码注释里老师一眼就能看出来你理解异步程序的资源控制比写十个“智能”刷新按钮都管用。本文还有配套的精品资源点击获取