用Python和Tkinter打造桌面天气应用:从API到打包全攻略

发布时间:2026/9/7 23:39:03
用Python和Tkinter打造桌面天气应用:从API到打包全攻略 做桌面天气应用这件事最早是我自己犯懒。每天上班打开电脑第一件事就是开浏览器、访问天气网站偏偏那阵子浏览器一开就是十几个标签页天气页总是被挤到很后面。更别提有时候网速一般等页面渲染完还要手动输入城市名、点查询真正看到结果已经是几十秒之后了。我就想为什么没有一个东西开机就在桌面上打开就是天气不用等不用找一眼扫过去就知道今天要不要带伞。于是就有了这个桌面版天气预报应用。用 Python 写界面部分是 Tkinter数据走后端天气 API支持按城市搜索、未来天气预报展示、定时自动刷新还能记住你上次查的城市。整个项目从零到能用大概花了一个周末之后又陆续加了温度折线图、多城市切换、开机自启这些功能。如果你有一点 Python 基础跟着这篇文章走完全能复刻出属于你自己的版本。1. 为什么我放着网页不用非要做一个桌面天气应用做任何工具之前先想清楚一个问题这个东西放在桌面上比网页到底强在哪里如果答案不成立那这个项目就没有意义。天气信息的使用场景有点特殊。它需要高频扫一眼但不需要长时间停留需要尽快给出结论而不是展示一堆眼花缭乱的数据。网页版最大的问题是它被“埋”在浏览器里你得先打开浏览器、找到标签页或重新输入网址这中间多出来的每一步都会降低你查看的意愿。我自己的体感是一旦某个信息需要经过两步以上才能看到我就会开始拖延最后的结果是明明手机里装着天气软件早晨出门前还是懒得看。桌面应用的优势在于“常驻”两个字。它可以开机自启启动后就是一个独立的小窗口放在屏幕角落不遮挡工作区域。想看天气抬一下眼就够了不需要任何额外的操作步骤。这一点在写代码的时候感触更明显——当你把窗口置顶到桌面右上角设置好每半小时自动刷新一次之后它就像一个安静的小公告板存在感很低但用起来很顺手。桌面应用的第二个优势是可控性。网页上你能做的操作被限制在对方的设计里显示什么、按什么排序、能不能改字号你说了不算。桌面应用是你自己的代码UI 想怎么摆就怎么摆。比如我觉得传统天气应用把“湿度”“风速”放在次要位置是个错误因为我骑车通勤最关心的是风力。于是我就把风力显示放在了第二行温度之后紧接着就是风。这种按自己使用习惯定制的体验是任何现成产品都给不了的。第三个优势也最实际——环境相对可控。你写一个网页天气应用要考虑跨浏览器兼容、移动端适配、后端部署但如果只是自己电脑上用直接把 Python 脚本打包成 exe 或 App完事了。开发成本低一个量级而且只要你的本机环境不变它能用很久。我家里一台老笔记本装了早期版本到现在还在跑从没出过问题。这个项目适合谁做我建议三类人动手试试一是刚学完 Python 语法、想找个完整项目练手的人这个项目麻雀虽小五脏俱全涉及网络请求、JSON 解析、GUI 布局、配置读写、打包发布几乎覆盖了日常开发会遇到的基础知识点二是比较在意效率、喜欢“让工具迁就人”的桌面工具爱好者三是想送长辈一个实用小软件的——打包成一个 exe 发给家人双击就能用比任何复杂软件都友好。2. 技术选型对比Python/Tkinter、PyQt 与 Electron 之间的取舍这个项目最先决定的不是界面长什么样而是用什么技术栈。我当时认真比对了三套方案各有各的优劣这里把真实对比结果列出来省得你在选型上纠结。技术方案缺陷与优势最终定位Python Tkinter标准库自带无需额外安装重量级依赖开发快缺点是不够现代控件审美比较朴素最终选择Python PyQt/PySide控件丰富界面漂亮支持样式表定制缺点是打包体积大动辄上百 MB学习曲线陡一些备选方案Electron Vue/ReactWeb 技术栈视觉效果上限最高跨平台一致性好缺点是内存占用高启动偏慢打包体积更大不推荐先说我排除了 Electron 的原因。Electron 本质上是把一个浏览器内核打包进应用里所以它的开发体验确实好HTML/CSS 想怎么写怎么写界面出来很现代。但代价也很明显一个简单的天气应用打包出来超过 200MB启动内存占用轻松到 300MB 以上。你说它是天气应用吧它本身就能当半个浏览器用这纯属用大象拉小车。如果你有改造成企业级软件的计划用 Electron 没毛病但自用工具这么搞没必要。PyQt 是我纠结最久的方案。它的 QSS 机制可以写出非常漂亮的界面控件也比 Tkinter 完善太多表格、图表、动画都有现成组件。后来放弃的主要原因不是功能而是项目体量不匹配。我要写的界面就三块搜索框、当前天气卡片、未来几天的列表。这些控件 Tkinter 全部能实现无非是排版上多花点心思。PyQt 的安装、理解和打包都比 Tkinter 重一个档位对一个周末想看到成品的小项目来说直接上 PyQt 容易在环境配置上消磨掉最开始那股热情。最终选的 Tkinter是因为它有两个别的方案替代不了的优势第一装好 Python 就有不需要额外安装第三方 GUI 库环境几乎零配置第二它足够轻程序跑起来内存占用不到 30MB打包出来的 exe 也就 15MB 左右双击秒开。它的控件确实称不上漂亮但通过 ttk 主题、字体搭配、网格布局的合理设计做出一个干净清爽的界面完全没问题。更关键的是Tkinter 的学习资料多遇到问题随便搜就有答案对新手极其友好。Tkinter 唯一让我不满意的点是它自带的字体渲染在高分屏上有时会显得发虚。解决办法是把 DPI 感知打开再加一句字体统一设置。这部分后面会专门讲到。接口请求方面我用了 Python 的 requests 库这是事实上的标准没有悬念。JSON 解析用内置的 json。整个项目第三方的依赖就只有 requests 和 PyInstaller打包用干净利落这也是我很满意的一个点。3. 天气数据从哪里来API 选型与 JSON 数据解析界面是骨架数据才是灵魂。天气应用的核心就是数据一个可靠的数据源比好看的界面重要得多。3.1 天气数据服务商怎么选市面上的天气 API 服务商不少我筛选的标准就三条一是国内可直接访问接口响应速度要快二是免费额度够个人用三是返回的数据结构要清晰文档要友好。我首推心知天气不是因为它的功能最全而是它的 API 调用最简单。注册账号后在控制台创建一个天气应用就能拿到一个 API Key调用 URL 直接拼参数就能返回 JSON不需要额外配置 Host 头。免费版一天几万次请求额度个人应用挂一百年都用不完。和风天气也很好数据维度更丰富包括逐小时预报、空气质量、生活指数等。它的接口需要设置额外的 Host 头稍微多一步但对有一定基础的开发者完全不是问题。我目前用的是和风天气因为后面想加重污染提醒和紫外线指数它给得更全。顺便提一句也有部分公开接口完全不需要 Key但这类接口不稳定指不定哪天调整域名或返回格式就变了作为生产用途不太推荐。个人玩一玩可以做成正规应用还是选注册制的服务商至少出问题能找到客服。3.2 请求结构详解以心知天气的“当前天气”接口为例一个完整的请求大致是这样的import requests API_URL https://api.seniverse.com/v3/weather API_KEY 你的私钥 def fetch_now(location): params { key: API_KEY, location: location, language: zh-Hans, unit: c, } resp requests.get(f{API_URL}/now.json, paramsparams, timeout5) resp.raise_for_status() data resp.json() result data[results][0] return { city: result[location][name], temp: result[now][temperature], code: result[now][code], text: result[now][text], humidity: result[now][humidity], wind_direction: result[now][wind_direction], wind_scale: result[now][wind_scale], last_update: result[last_update], }这里有几个需要解释的地方。location参数支持城市拼音、城市 ID 和经纬度。拼音最简单直接传beijing就行但同名城市会有歧义比如changchun可能是长春也可能是某个区。用城市 ID 最准确在心知天气的文档里有城市列表可以查。我实际用的是经纬度因为我默认展示的是本机所在城市通过一个 IP 定位接口拿到大致经纬度再传给天气接口。这样就算你人在外地打开应用看到的也是当地天气体验好很多。unitc表示温度用摄氏度不传的话有可能是华氏度这个参数容易被忽略但很关键。timeout5设了 5 秒超时。天气应用是高频查看的工具如果接口卡了不能让整个界面等着不动。超时的异常处理后面章节会专门讲。3.3 JSON 结构的解析技巧心知天气的标准返回结构长这样{ results: [ { location: { name: 北京, id: WX4FBXXFKE4F }, now: { text: 晴, code: 0, temperature: 6, humidity: 23, wind_direction: 东北, wind_scale: 2 }, last_update: 2025-01-20T09:15:0008:00 } ] }解析其实很简单核心就是一层层取字典。我写代码的习惯是接口返回后不要直接拿里面的字段用而是先把它映射成一个结构清晰的 Python 字典或 dataclass这样界面层的代码就完全不需要关心 JSON 的嵌套结构了。上面代码里的fetch_now返回的就是已经整理好的数据界面层拿到data[text]、data[temp]直接用可读性好很多。这里还要注意一个坑心知天气的温度字段在返回里是字符串不是数字。如果你要做温度高低比较或者画折线图需要先int()转换。我第一次就是因为忘了转换画折线图时全是字符串拼接报错报得一头雾水。4. 界面搭建单窗口三层布局把天气信息讲清楚天气应用的信息层级很明确城市搜索是入口当前天气是重点未来预报是延伸。界面设计我按这个逻辑分了三层从上到下依次是搜索栏、当前天气卡片、预报列表。4.1 窗口基础配置窗口大小我用的是 420×640。这个尺寸在 1080p 屏幕上既不占地方又放得下三层内容。再小就会显得拥挤预报列表要滚动才能看全再大就失去桌面小组件那种“放在角落不碍事”的意义了。import tkinter as tk from tkinter import ttk class WeatherApp: def __init__(self): self.root tk.Tk() self.root.title(桌面天气) self.root.geometry(420x640) self.root.resizable(False, False) self.root.attributes(-topmost, False)关于置顶这里特意说下。有人喜欢把天气窗口设在最顶层任何时候都能看到。我的建议是不要默认置顶那样一开会挡住代码编辑器。我给应用加了“窗口置顶”的开关默认关闭用户想让它停在最上面就自己打开。桌面工具的礼貌是尽量不干扰而不是时刻强调存在感。高分屏用户注意需要在入口处设置 DPI 感知否则字体和控件会明显发虚try: from ctypes import windll windll.shcore.SetProcessDpiAwareness(1) except Exception: pass这行代码只在 Windows 上有效所以用 try/except 包住其他系统直接跳过不影响运行。4.2 搜索栏与当前天气卡片搜索栏用了一个 Entry 加一个 Button回车也能触发查询。这块技术含量不高但有两个细节值得提一是绑定回车键很多人会顺手输入完按回车如果只做了按钮事件这个用户就流失了二是在查询前简单清理空格、判断空输入避免发起无意义的请求。当前天气卡片是整个界面的视觉中心。我把它设计成一个带浅色背景的 Frame里面分三行展示第一行是城市名和更新时间第二行是大号温度数字和天气描述第三行是湿度和风向风力。温度数字我用了大号字体Segoe UI32pt让它一眼就扫得到。天气描述和温度并排放晴朗、多云、小雨这类文本用 14pt 常规样式。湿度和风力放在底部一行用灰色小字信息在但不抢眼。self.city_label ttk.Label(self.weather_frame, text--, font(Segoe UI, 16, bold)) self.temp_label ttk.Label(self.weather_frame, text--°C, font(Segoe UI, 32, bold)) self.desc_label ttk.Label(self.weather_frame, text--, font(Segoe UI, 14)) self.detail_label ttk.Label(self.weather_frame, text湿度 --% | 风力 --级, font(Segoe UI, 10))整体用 grid 布局排列这个卡片的信息流是从上到下、从主到次人眼扫过去是一个 Z 字形符合阅读习惯。4.3 未来天气预报列表与图标映射预报列表我按日期逐行展开每天一行左边是星期几中间是天气描述右边是最高/最低温度。这样排列信息密度适中一眼能看完未来几天的情况。天气图标这块一开始想用 emoji后来发现 Tkinter 对 emoji 的渲染在 Windows 下有时会显示成黑白或方块不同系统差别很大。稳妥的做法是直接用文字描述不画图标。如果你实在想要图标有两个思路使用 Unicode 符号比如晴用 ☀多云用 ☁雨用 ☂。这些符号在 Windows 的宋体里都能渲染效果基本稳定。自己下载一套天气图标 PNG 素材用PIL.ImageTk.PhotoImage加载显示最可控但需要处理素材资源路径后面打包时是个坑。我最终在列表里用了文字保持最简。当前天气卡片上的大图标用的是 Unicode 符号。效果不错不会有版本兼容问题。预报数据来自心知天气的“每日预报”接口。返回结构里未来天数可以指定我拿了未来三到七天的数据。代码逻辑和当前天气类似只是results[0][daily]是一个数组每个元素对应一天的预报包含date、text_day、text_night、high、low等字段。这里也有个坑high和low同样默认是字符串画温度曲线图前记得转 int。按日期解析出“今天是周几”是另一个容易被卡住的地方。我的做法是基于当天日期推算from datetime import datetime, timedelta today datetime.now() weekdays [周一, 周二, 周三, 周四, 周五, 周六, 周日] labels [] for i in range(len(daily_data)): d today timedelta(daysi) labels.append(weekdays[d.weekday()])这样不用管数据里带不带 weekday 字段自己算简单可靠。5. 让应用像“原生”一样好用定时刷新、配置记忆与异常兜底一个天气应用如果只能打开时查一次那和网页版没区别。桌面化的核心竞争力在于“不用管它也在干活”这就涉及三个功能定时刷新、配置记忆、异常兜底。5.1 定时刷新与手动刷新Tkinter 里做定时任务很简单用根窗口的after方法def auto_refresh(self): self.load_weather() self.root.after(30 * 60 * 1000, self.auto_refresh) # 每30分钟注意after的第一个参数是毫秒单位别写错。这个方法的逻辑是先执行一次刷新然后再安排下一次。这样即使刷新过程抛异常了不影响下一次调度。手动刷新按钮用单独的图标或者直接写“刷新”两个字就行。我给按钮设置了灰色刷新时变色避免用户反复点击。关键点是如果上一次请求还没返回刷新按钮应该进入不可用状态。防抖逻辑也很重要。接口请求是异步的如果用户在上一轮还没回来时又点了一次查询会出现两个请求竞争返回界面数据可能错乱。我的处理方法是引入一个self.is_loading标志请求前检查请求中置为 True请求结束无论是成功还是失败都重置为 False。这个防御性编程的细节让应用在慢网络下也不会出现诡异行为。5.2 城市配置保存与启动加载关掉应用再打开如果又回到默认城市用户会很失望。所以我加了配置记忆功能把最近一次查询的城市保存到本地文件。import json, os CONFIG_PATH os.path.join(os.path.expanduser(~), .desktop_weather.json) def save_city(city): cfg {city: city, topmost: False} with open(CONFIG_PATH, w, encodingutf-8) as f: json.dump(cfg, f, ensure_asciiFalse) def load_city(): if os.path.exists(CONFIG_PATH): with open(CONFIG_PATH, r, encodingutf-8) as f: cfg json.load(f) return cfg.get(city, beijing) return beijing这个文件路径放在了用户目录下而不是应用目录。为什么要这样做因为打包成 exe 后应用很可能被安装在 Program Files 或别的位置如果往应用目录写文件Windows 的权限限制可能导致写入失败。放在用户目录则没有任何权限问题也更符合规范。5.3 网络异常与接口限流的兜底处理网络请求是桌面应用最不稳定的环节。我在代码里对三类异常做了专门处理超时requests.Timeout提示“连接超时请检查网络”连接错误requests.ConnectionError提示“无法连接服务器”接口限流服务商返回的 status 码不是正常值提示“请求过于频繁”。不能用 5 秒超时用户点击查询后有一瞬间的等待感这种体验还能接受。但超过 5 秒说明网络确实有问题直接中止请求并弹出一个 ttk 的气泡提示而不是让整个界面卡死。实现时我加了一个全局异常拦截try: self.data fetch_now(self.city_var.get()) except requests.Timeout: messagebox.showwarning(提示, 网络超时请稍后再试) return except Exception as e: messagebox.showwarning(提示, f查询失败{e})这里有一个我特别想强调的观点不要用messagebox来处理所有错误。天气应用是常驻桌面的工具弹窗会打断思路。我后来把弹窗改成了状态栏提示在窗口底部显示一行小字“上次更新xx 失败原因网络超时”。只有连续失败三次以上才考虑弹窗。这样既不打扰用户又能留下排查线索。6. 打包分发与开机自启把脚本变成真正的桌面软件写完脚本只是第一步要让它真正像桌面软件一样被使用还需要打包、分发、自启这几步。这里有不少坑我一个个说。6.1 PyInstaller 打包与资源路径打包工具用的是 PyInstaller一条命令就能出结果pip install pyinstaller pyinstaller -F -w -i weather.ico weather.py参数说明-F表示打包成单个 exe 文件方便分发-w表示不显示控制台窗口因为这是 GUI 应用-i指定图标文件。打包后的 exe 有几个我踩过的坑。如果你在程序里引用了外部文件比如天气图标素材千万不要用相对路径。PyInstaller 打包后相对路径会失效。解法是在代码里动态获取运行时路径import sys, os def resource_path(relative_path): base_path getattr(sys, _MEIPASS, os.path.dirname(os.path.abspath(__file__))) return os.path.join(base_path, relative_path)PyInstaller 会把资源文件解压到一个临时目录sys._MEIPASS就是这个临时目录的路径。开发环境跑代码时没有这个属性就回退到正常的脚本路径。这个函数是我所有 PyInstaller 打包项目的固定开场白。第二个坑是杀毒软件误报。PyInstaller 打包出来的单文件 exe 偶尔会被某些杀软标记为可疑程序这不是代码有问题是加壳和单文件机制导致特征相似。解决办法一是尽量用-w而不是--console二是代码签名。如果只是自己用直接忽略即可或者用 PyInstaller 的--key参数稍作混淆。还有一件事打包前用upx压缩可以减小体积但可能导致杀毒软件误报率更高我后来放弃了 UPX体积也就多了几 MB换来的是更省心。6.2 开机自启的三种实现想让应用开机自动打开取决于你用哪个系统。我主要用的是 Windows 和 macOSLinux 指导几句也能搞定。Windows 上最简单的自启方式是在启动文件夹里放一个快捷方式。启动文件夹路径shell:startup在运行框里输入即可打开。把 weather.exe 的快捷方式丢进去重启后就自动运行了。用代码实现的效果一样直接把一个 vbs 脚本或快捷方式写进启动目录import os, shutil startup_dir os.path.join(os.environ[APPDATA], rMicrosoft\Windows\Start Menu\Programs\Startup) # 将 weather.exe 的快捷方式复制到该目录即可macOS 则是用 LaunchAgent 的 plist 文件。my.weather.plist 放到了~/Library/LaunchAgents下设定RunAtLoad为 true登录后自动启动。Linux 桌面环境一般遵循 XDG 规范在~/.config/autostart目录下放一个.desktop文件就行。我实现自启的方式是在应用里加了一个“开机自启”的复选框用 Python 判断当前系统执行对应操作。这样不用用户手动去翻系统设置对不太熟悉电脑的长辈尤其友好。还要注意一个小细节如果做开机自启建议应用默认启动时最小化到系统托盘而不是直接呼出一个大窗口。加载托盘图标我用了 pystray 库但因为 Tkinter 的事件循环和 pystray 的循环需要同时跑踩了不少坑。后来换成只做“启动时最小化到任务栏”不搞系统托盘省心很多。自启时窗口正常出现在桌面角落配合退出按钮足够了。7. 温度趋势图、多城市管理与几个实用优化到这里一个基础版天气应用已经可以正常工作了。接下来这些是我后来陆续加进去的增强功能每一个都是自己在使用中觉得“缺了点什么”才动手补的。7.1 用 Canvas 画未来几天的温度曲线温度曲线是我强烈建议加的一个功能。单纯看最高/最低温度的数字用户很难快速把握这几天的温度走势但一条曲线就直观多了。Tkinter 没有现成的图表控件但可以用 Canvas 自己画。逻辑很简单把未来七天的最高温、最低温分别作为两组点映射到 Canvas 坐标系然后用create_line连线。def draw_temp_trend(self, canvas, daily_data, width, height): canvas.delete(all) highs [int(d[high]) for d in daily_data] lows [int(d[low]) for d in daily_data] max_temp max(max(highs), max(lows)) 2 min_temp min(min(highs), min(lows)) - 2 # 横坐标按天数均匀分布纵坐标按温度线性映射 points_high [] points_low [] for i, (high, low) in enumerate(zip(highs, lows)): x 30 i * (width - 60) / max(1, len(daily_data) - 1) y_high 20 (max_temp - high) / (max_temp - min_temp) * (height - 40) y_low 20 (max_temp - low) / (max_temp - min_temp) * (height - 40) points_high.append((x, y_high)) points_low.append((x, y_low)) # 绘制折线 for idx in range(len(points_high) - 1): canvas.create_line(points_high[idx], points_high[idx 1], fill#d97706, width2) canvas.create_line(points_low[idx], points_low[idx 1], fill#2563eb, width2)这里最需要注意的是 Canvas 坐标系 Y 轴是向下的温度高对应 Y 值小所以计算时要取反。我第一次画出来曲线是反的折腾了十分钟才反应过来。加这个功能后天气预报就从“告诉你明天几度”升级成了“看一眼就知道未来几天是升温还是降温”。早上不用犹豫要不要多带件外套看曲线比看列表快得多。7.2 多城市管理与快速切换多城市管理是后来加的。因为我周末可能会去周边城市每次敲拼音查询太麻烦。我设计了一个列表应用可以保存你关注的多座城市点击城市名就能快速切换。实现思路就是在配置文件的 JSON 里多存一个字段记录城市列表{ city: beijing, cities: [beijing, shanghai, guangzhou, shenzhen], topmost: false }界面上用ttk.Combobox下拉框展示这些城市选中即查询比纯手动输入城市名友好很多。这个功能对整个项目来说改动不大但对日常使用体验的提升很明显。7.3 实测中的几个体会与优化方向项目跑了两三个月我积累了三个值得分享的心得。第一个体会是数据刷新频率不要太高。有人喜欢每 10 分钟就刷新一次但天气数据其实半小时内基本不会变化频繁刷新除了增加接口请求外没有任何意义。我设了 30 分钟自动刷新手动刷新按钮随时可用这样既及时又不浪费。第二个体会是字体选择对观感的影响比想象中大。默认字体在高分屏上发虚我换成了 “Microsoft YaHei UI” 配 “Segoe UI”整体观感立刻提升了一个档次。中英文混排时中文用雅黑、数字用 Segoe UI 的组合是最稳的不会出现比例失调的问题。第三个建议是给应用加一个“最后更新时间”的显示。这个信息一开始觉得是废话后来发现特别有用——用户可以判断当前数据是不是够新鲜不会因为显示的是昨天的天气而产生困惑。一眼看到“更新时间09:15”心里就有底了。关于后续优化方向我能想到的比较有价值的方向有三个一是接入空气质量指数和紫外线指数对计划户外活动的帮助比单纯温度更大二是把应用做成真正的系统托盘驻留程序点击托盘图标才弹出窗口完全淡出任务栏三是做一个简单的启动器能热切换开自启、置顶等设置而不是靠改配置文件。就我目前的日常使用来说这个应用已经取代了浏览器的天气标签页也取代了我手机上装的天气 App。写脚本、改代码的间隙偶尔抬头看一眼桌面右上角那几行字今天出门穿什么、要不要带伞心里就有数了。这种感觉很微妙明明只是个不起眼的小窗口但它是我自己写出来的每一行代码背后的取舍都清清楚楚。做工具这件事最迷人的大概就是这种“按自己的想法解决问题”的过程。你有没有什么一直反复做、觉得特别烦琐的小事如果有不妨试试像我一样挑一个最简单的场景写一个属于自己的桌面应用。