资讯详情

资讯详情

Python+PyQt5开发桌面天气应用全流程:API对接、界面设计与打包exe实践

桌面应用这个东西很多人一听就觉得“是不是过时了”。但我用下来反倒觉得天气查询这种高频但低时长的场景放在桌面端比网页和小程序都顺手不用开浏览器不用等启动页窗口往任务栏旁边一放下班瞄一眼就知道明天穿不穿外套。这篇就完整复盘一下我是怎么用 Python 和 PyQt5 从零写了一个桌面天气应用从需求拆解、API 对接、界面设计到打包成 exe 的整个流程踩过的坑也会一并写出来希望能给想动手做桌面工具的朋友一点可落地的参考。1. 为什么选择桌面应用项目设计与方案选型1.1 需求梳理一个天气工具的核心三件事做任何小工具之前先别急着写代码我把需求压到最简之后其实就三件事输入城市名、看到当前天气、展示关键数据。天气这个场景有个特点用户不会在窗口里待很久打开软件就是为了快速获取几个数值然后关掉继续干活。所以产品形态上桌面应用反而是最舒服的容器。网页或者小程序虽然也能做但打开成本高还容易被各种推送和广告干扰。命令行工具虽然也能查但天气数据带有大量文字信息命令行输出体验太糙谈不上实用。所以第一版的需求清单很简单一个城市输入框、一个搜索按钮、一块展示区域把温度、天气现象、体感温度、湿度、风向风力、降水情况这六类信息呈现出来。至于 7 天预报、折线图、自动定位这些属于“有更好”而不是“必须有”的功能我在设计结构时会预留接口但不会在第一版里全做完——先跑通最小可用版本才是做个人项目最省力的方式。1.2 方案对比为什么是 Python PyQt5 而不是其他组合技术选型这块我把自己接触过的几套方案摆在一起横评了一下。Tkinter 是 Python 自带的 GUI 库学习成本最低但控件样式非常复古做出来的窗口一眼看上去就是个“教学示例”想调成现代卡片风格得花不少力气所以我直接放弃了。PyQt5 或者 PySide6 的控件丰富度、样式定制能力都够用资料也多而且 Python 生态里做 GUI 的经典组合基本就是它。Electron 的方案我也认真考虑过前端技术栈做界面确实漂亮但它动不动占用两三百兆内存启动还要先跑一个 Chromium 内核为一个天气工具付出这个代价实在不划算。Flutter Desktop 界面现代化但初学门槛相对高而且不适合在 Python 项目里混用。最终我选择的是 Python 3 PyQt5 requests 这个组合。原因很实际第一数据获取走 HTTP 请求Python 的 requests 库三行就能完成一次调用第二JSON 解析在 Python 里几乎是零成本第三PyQt5 的 QSS 样式机制类似 CSS我可以用一套暗色样式把桌面界面做得很干净完全不需要额外引入前端资源。2. 搭建工程骨架环境准备与项目初始化2.1 虚拟环境与依赖安装我建议所有这类小工具都从虚拟环境开始。天气应用依赖的库不多但 PyQt5 的安装包体积不小直接装在系统 Python 里以后管理起来容易脏。Windows 下的操作流程是python -m venv venv venv\Scripts\activate pip install PyQt5 requests这里有个细节PyQt5 官方包在现代 Python 版本上一般能直接装好但如果你用的是 3.12 以上版本个别依赖可能出现 wheel 缺失的情况。遇到这种问题优先尝试使用 Python 3.10 或 3.11 建一个虚拟环境稳定很多。依赖装完后顺手导出一份 requirements.txt方便以后在其他机器上还原环境pip freeze requirements.txt2.2 项目目录与配置文件很多人写小工具喜欢所有代码塞进一个 main.py一开始确实方便但这个项目涉及 API 逻辑、界面逻辑、入口文件三个关注点混在一起后期维护会很痛苦。我用的目录结构如下weather_app/ ├── main.py ├── core/ │ ├── __init__.py │ └── api.py ├── style.qss ├── config.ini ├── app.ico └── requirements.txtmain.py 只负责创建窗口、装好样式表、启动事件循环core/api.py 封装所有和天气服务商的交互style.qss 单独放界面样式跟代码解耦config.ini 用来存放 API Key 这类配置信息。这个结构麻雀虽小五脏俱全后面想扩展历史曲线、多城市管理都是往对应模块里加东西不会动到其他部分。3. 对接天气API数据请求、解析与兜底3.1 数据源选择免费天气API的取舍天气数据的来源很多但选型要实际跑一下才知道靠不靠谱。我主要对比过三类和风天气的免费开发版、OpenWeatherMap、以及一些公开的天气网接口。OpenWeatherMap 的国际城市覆盖最广但国内访问速度经常不稳定而且免费版返回的天气代码需要自己映射成中文文案冷热数据转换也要额外处理。某些公共服务接口虽然免注册但可用性和稳定性没有保障做出来的工具万一明天接口失效就白干了。我最终选的是和风天气免费开发版注册后创建项目就能拿到 API Key国内访问速度快返回字段也足够日常使用。数据源是否需要注册国内访问稳定性字段丰富度个人练手推荐和风天气开发版是稳定高五星OpenWeatherMap是一般高三星公共天气接口否不稳定低二星需要提醒一句免费版的 API 有 QPS 限制个人使用完全够但别拿免费 Key 做商用或者高频轮询。真要做生产级应用买付费套餐是绕不开的。3.2 请求封装城市搜索实时天气一次搞定和风天气的 API 分成城市搜索和实时天气两部分。城市搜索接口的作用是把“北京”这种汉字转换成城市 ID因为实时天气接口只认 ID 不认地名。请求的封装我放在了 core/api.py 里核心代码大致是这样import requests QWEATHER_KEY 你的_KEY LOOKUP_URL https://devapi.qweather.com/v7/geo/lookup NOW_URL https://devapi.qweather.com/v7/weather/now class WeatherAPI: def __init__(self, key: str QWEATHER_KEY): self.key key self.session requests.Session() self.session.headers.update({User-Agent: WeatherApp/1.0}) def search_city(self, name: str): params {location: name, key: self.key} resp self.session.get(LOOKUP_URL, paramsparams, timeout5) data resp.json() if data.get(code) ! 200: raise RuntimeError(f城市搜索失败: code{data.get(code)}) locations data.get(location, []) if not locations: raise ValueError(查不到这个城市试试输入更完整的名称) return { id: locations[0][id], name: locations[0][name], } def get_now(self, location_id: str): params {location: location_id, key: self.key} resp self.session.get(NOW_URL, paramsparams, timeout5) data resp.json() if data.get(code) ! 200: raise RuntimeError(f天气查询失败: code{data.get(code)}) now data.get(now, {}) return { temp: now.get(temp), feels_like: now.get(feelsLike), text: now.get(text), wind_dir: now.get(windDir), wind_scale: now.get(windScale), humidity: now.get(humidity), precip: now.get(precip), updated: data.get(updateTime, ), }这里有几个小坑需要留意。第一和风天气返回的 code 字段是字符串 “200”不是整数 200判断时必须用字符串比较。第二timeout 一定要设置否则网络异常时请求会卡在那里界面跟着无响应。第三代码里的 Key 我建议放到 config.ini 里用 configparser 读取而不是硬编码在源文件里不然以后把代码传到 GitHub 很容易泄露。3.3 数据规整把天气代码变成人话天气接口返回的 text 字段在大多数情况下已经是中文但不同服务商的数据格式差异很大。比如有些服务商返回的是 weather_code需要一张映射表才能转成“晴”“多云”“小雨”。即使使用和风天气我也习惯把所有字段统一整理成一个结构化的 WeatherData 对象而不是在界面层直接操作字典。我比较推荐使用 dataclass 来定义数据模型字段清晰后面扩展也好维护from dataclasses import dataclass dataclass class WeatherData: city: str text: str temp: str feels_like: str humidity: str wind_dir: str wind_scale: str precip: str updated: str这样 API 返回的数据经过 get_now 组装后统一转成 WeatherData 实例传给界面层界面层只负责展示不关心数据是从哪个服务商来的。以后哪怕整个天气源换掉界面代码一行都不用动。4. 设计桌面UI布局、交互与多线程防卡顿4.1 界面布局用卡片把信息层级做清楚天气应用的信息层级其实很清晰温度是最重要的其次是天气现象和体感最次要的是湿度、风力这些细节。所以界面设计上我采用了卡片式布局核心信息做大字号居中显示次要信息放在下方一行小字里。主窗口由三个区域组成顶部是输入框加查询按钮中间是主天气卡片底部是细节信息条。PyQt5 里用嵌套布局很容易实现top QHBoxLayout() self.city_input QLineEdit() self.city_input.setPlaceholderText(输入城市名例如北京) self.search_btn QPushButton(查询) top.addWidget(self.city_input) top.addWidget(self.search_btn)主卡片我用 QFrame 加 objectName然后在 QSS 里统一设置圆角和背景色。这种写法和 Web 前端把样式抽成 CSS 文件是一个思路代码看起来干净调整视觉效果也方便。4.2 交互状态管理输入、查询、显示的状态闭环桌面应用的交互闭环看起来简单但细节不少。输入框为空时点击查询应该把焦点拉回输入框而不是发请求查询过程中要防止用户重复点击导致线程堆积查询失败时界面要给出提示而不是白屏。我采用的思路是点击按钮或者按回车都触发 on_search 方法方法里先做输入校验然后创建新的工作线程去请求天气。按钮点击和回车绑定在同一个槽函数减少了代码重复self.search_btn.clicked.connect(self.on_search) self.city_input.returnPressed.connect(self.on_search) def on_search(self): city_name self.city_input.text().strip() if not city_name: self.city_input.setFocus() return self.worker FetchWorker(city_name) self.worker.result_ready.connect(self.update_weather) self.worker.error_occurred.connect(self.show_error) self.worker.start()这里每点击一次查询就新建一个 QThread严格来说不是最优做法更好的方案是用 QThreadPool 管理线程池。但作为个人工具这样写逻辑最直白理解成本低代价是极端高频点击时可能产生多个线程。实际使用中加一个“如果 worker 还在运行先忽略新请求”的判断就足够。4.3 网络请求放到子线程避免窗口假死这是整个项目最容易踩的深坑。PyQt5 的事件循环运行在主线程里如果在主线程直接调用 requests.get网络请求会阻塞事件循环表现就是窗口拖不动、按钮点了没反应严重时系统会提示“程序未响应”。解决办法就是把网络请求放到 QThread 子线程里执行执行完成后通过信号把数据传回主线程。PyQt5 的一个关键规则是子线程不能直接操作控件只能发信号由主线程的槽函数更新界面。我的 FetchWorker 是这样写的from PyQt5.QtCore import QThread, pyqtSignal class FetchWorker(QThread): result_ready pyqtSignal(dict) error_occurred pyqtSignal(str) def __init__(self, city_name, parentNone): super().__init__(parent) self.city_name city_name def run(self): try: api WeatherAPI() city api.search_city(self.city_name) now api.get_now(city[id]) payload {city: city[name], **now} self.result_ready.emit(payload) except Exception as e: self.error_occurred.emit(str(e))信号 result_ready 携带的字典里既有城市名又有天气字段主线程收到后直接更新各控件。这样做的好处是界面永远不会卡哪怕网络超时也只是在 5 秒后弹出错误提示窗口全程可交互。5. 完整代码实现跑通最小可用版本5.1 数据层代码阅读整个工程最核心的三块内容前面已经拆得差不多现在把它们拼成一个能跑的最小版本。数据层我建议直接使用下面这个精简版接口支持城市搜索和实时天气两个动作返回统一字典结构。使用时只需要把自己的 KEY 填进 WeatherAPI 的初始化参数。5.2 界面与主窗口代码阅读主窗口部分我简化了样式细节保留完整的布局和交互逻辑。窗口宽度 360 像素高度 420 像素足够在桌面上平铺显示而不占用太多空间。void 没别的高深东西就是按结构把控件堆进去再用信号槽把控件和业务逻辑绑起来。这里有一个细节值得说我用了QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)处理高分屏的字体模糊问题这行代码必须放在 QApplication 创建之前写错位置不会报错但效果完全丢失。5.3 运行起来验证最小闭环代码写完后直接在项目根目录运行python main.py如果一切正常窗口会弹出来输入“上海”点查询大约一秒内卡片上就会显示上海当前的气温。第一次跑通这个闭环的意义很大因为之后的优化都是在验证“已经有了的东西做得更好”而不是在调一个永远跑不起来的半成品。6. 打包成exe从Python脚本到桌面程序6.1 PyInstaller 打包的基本操作开发完的 Python 程序不可能每次都让朋友装 Python 环境所以打包是桌面应用必须跨过的一关。我用的工具是 PyInstaller安装和打包命令都很简单pip install pyinstaller pyinstaller -F -w --name WeatherApp --icon app.ico main.py几个参数的含义分别是-F 表示打包成单个 exe 文件-w 表示运行时不弹出控制台窗口--icon 指定程序图标--name 指定输出文件名。打包完成后exe 文件在 dist 目录下直接复制给任何一台 Windows 电脑就能跑。第一次打包建议先用不带 -w 的参数打包在命令行里直接运行 exe如果程序报错控制台会打印完整堆栈信息排查问题比黑盒模式容易得多。确认没问题之后再打正式的无控制台版本。6.2 打包资源文件与路径处理如果程序里引用了外部文件比如 QSS 样式表、图标、配置文件直接打包会遇到一个很典型的问题exe 运行时的工作目录跟代码里写的相对路径对不上导致样式表加载失败程序白屏或者字体出现异常。解决方法是借助sys._MEIPASS这个 PyInstaller 运行时变量它会指向资源解压后的临时目录。通用的资源路径处理函数是这样的import sys import os def resource_path(relative_path): base_path getattr(sys, _MEIPASS, os.path.abspath(.)) return os.path.join(base_path, relative_path)代码里凡是读取样式表、图标的路径都统一通过 resource_path 拼出来同时打包时用 --add-data 把这些文件带进去。Windows 系统的分隔符注意用分号pyinstaller -F -w --add-data style.qss;. --add-data app.ico;. main.py我一开始没处理资源路径打包后的 exe 双击就有画面但样式全部丢失排查半天才发现问题出在路径上这一点建议所有做 PyInstaller 打包的朋友提前处理。7. 实测问题记录高频报错与排查思路7.1 高频问题速查表做这个小工具的过程中我在不同阶段遇到了一些比较典型的问题整理成一张表方便大家对照排查。现象可能原因解决方法城市搜索没结果API Key 错误或没有匹配到城市检查 Key 是否正确尝试输入“北京朝阳”这类更完整名称界面点击查询后无响应没有做输入校验空字符串直接触发搜索先 strip 再判空空字符串时把焦点还给输入框温度、湿度显示为 None解析字段名写错比如把 feelsLike 写成了 feels_like先打印原始 JSON对照实际字段逐一核对窗口拖动时卡死、假死网络请求放在了主线程把请求逻辑迁移到 QThread通过信号回传数据打包后 exe 打开白屏引用了外部文件但文件不在解压目录使用 sys._MEIPASS 处理资源路径并在打包命令中加入 --add-dataWindows 下 exe 被误报病毒PyInstaller 单文件模式特征明显使用 UPX 压缩或换一个正式图标一般能降低误报率查询过程中连续点击会崩溃上一个线程还在跑又创建了新线程判断 worker 是否在运行运行中则忽略新请求7.2 调试三步法天气应用这类带网络请求的 GUI 程序出问题的时候最忌讳乱猜。我总结了一套自己的排查顺序先控制台再原始 JSON最后看线程。控制台能告诉我们程序执行到哪一步崩的打印原始 JSON 能帮我们确认是数据格式变化还是解析逻辑错误排查线程问题看的是界面是否无响应、卡在哪个操作上。我在开发时给 api.py 加了一个 verbose 开关请求前打印完整 URL响应后打印前 200 个字符的 JSON这个习惯帮我省了很多时间。8. 继续往上加这类小工具的进阶方向最小可用版本跑通之后这个项目其实才到了一半因为天气工具的上限空间还挺大。我自己已经列了几个下一步的方向。第一个是常驻体验。把查询逻辑从“手动点击”改成 QTimer 定时刷新比如每 30 分钟自动拉一次数据加上系统托盘图标右键可以快速切换城市或退出这样工具才算真正融入了桌面环境。第二个是数据表达增强。当前版本只显示了实时天气下一步可以接逐小时预报和 7 天预报接口在底部加一条简单的温度变化曲线。PyQt5 的 QChart 模块做这个非常顺手配上 QSS 的暗色主题视觉效果会立刻提升一个档次。第三个是自动定位。Windows 下可以通过检查系统网络信息或者调用地理定位接口自动把用户所在城市带上免去第一次手动输入。真做完这三步这个桌面天气应用的完成度就完全能拿去展示和日常使用了。做完这个项目我最大的体会是桌面应用并没有死反而特别适合这种“小而独立”的工具场景。它需要你同时处理好界面交互、网络请求、数据建模、打包分发这几件事练一轮几乎等于把 Python 桌面开发的完整链路走了一遍。最后分享一个我自己的调试习惯每次改动完功能先回到代码里打印一次接口返回的原始 JSON确认数据没问题再碰界面代码这样能省掉大量“这里修一下那里动一下”的无意义操作。如果你也在写自己的桌面小工具我建议也先从完整跑通最小闭环开始别一上来就铺功能。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →