关注

从零打造桌面天气预报工具:PyQt5 实战开发全解析(附完整项目代码)

每天打开电脑第一件事查天气?翻手机 APP 总觉得不够方便?其实用 PyQt5 写一个桌面版天气预报工具并不难 —— 不需要复杂的框架,几行代码就能实现界面交互,结合爬虫技术还能实时获取天气数据。

哇哇 今天就带大家手把手拆解一个实用的桌面天气预报程序,从界面设计到数据爬取,从线程优化到异常处理,让你看懂每一行代码的作用,轻松入门 PyQt5 开发

🔥🔥🔥🔥哇哇 提醒:文章结尾附项目完整代码,安装好对应需求库,复制代码即可运行🔥🔥🔥🔥


一、先看效果:这个程序能做什么?

运行程序后,你会看到一个简洁的窗口:顶部是输入框和查询按钮,中间是天气数据表格,底部有状态提示。输入支持的城市名(比如 “北京”“成都”),点击 “查询天气”,表格就会显示未来 7 天的日期、最高温度、最低温度和天气状况,而且会根据天气类型自动变色 —— 雨天是浅蓝色,晴天是浅黄色,多云是浅灰色,直观又好看。

最关键的是,查询过程中界面不会卡顿,网络超时或输入错误时会弹出提示,完全像一个 “正经” 的桌面软件。这个看似简单的工具,其实包含了 PyQt5 界面设计、多线程处理、网络爬虫、数据解析等多个知识点,非常适合作为 PyQt5 入门实战案例。


二、准备工作:3 个核心库搞定开发

开发前先准备好 “工具箱”,这个程序需要三个关键库:

  • PyQt5:用于搭建桌面界面,提供窗口、按钮、表格等各种控件,是整个程序的 “骨架”。
  • requests:作为网络爬虫的 “跑腿员”,负责从天气网站下载数据。
  • BeautifulSoup:解析网页内容的 “过滤器”,能从杂乱的 HTML 中精准提取温度、日期等信息。

如果你的电脑还没安装这些库,打开终端输入三行命令即可:

pip install pyqt5 -i https://mirrors.aliyun.com/pypi/simple/
pip install requests -i https://mirrors.aliyun.com/pypi/simple/
pip install beautifulsoup4 -i https://mirrors.aliyun.com/pypi/simple/

!!!!滴滴:不知道怎么安装第三方库或者想换用国内镜像源加速下载,可以点击链接跳转推荐学习文章↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

Python国内镜像源全攻略:加速你的第三方库下载

极速配置!Anaconda换用国内pip镜像源,一劳永逸解决下载龟速!

安装完成后,就可以开始写代码了。


三、核心代码拆解:从界面到功能的实现逻辑

1. 城市代码字典:给城市编个 “身份证号”

程序开头有一个CITY_CODES字典,里面存着城市名和对应的数字编码,比如'北京': '101010100'。这串数字可不是随便编的 ——

中国天气网的每个城市天气页面都有固定 URL:http://www.weather.com.cn/weather/城市编码.shtml,比如北京的天气页就是http://www.weather.com.cn/weather/101010100.shtml

这个字典的作用就像 “城市通讯录”,当用户输入 “北京” 时,程序能快速找到对应的编码,拼出正确的网页地址,为后续爬取数据做准备。

2. WeatherWorker:用线程解决界面卡顿问题

如果你直接在界面按钮点击事件里写网络请求,会发现一个尴尬的问题:查询时窗口会卡住,按钮点不动、窗口拖不动,像 “死机” 一样。这是因为 PyQt5 的界面渲染和用户操作在同一个主线程里,网络请求这种耗时操作会阻塞主线程。

解决办法是用QThread创建后台线程,让网络请求在 “后台” 运行,不影响界面响应。这就是WeatherWorker类的作用:

class WeatherWorker(QThread):
    finished = pyqtSignal(dict, str)  # 成功信号:天气数据和城市名
    error = pyqtSignal(str)  # 错误信号:错误信息

    def run(self):
        # 网络请求和数据解析代码
        # ...
  • 信号(Signal):线程和主线程的 “信使”。查询成功时,finished信号会把天气数据传给主线程;出错时,error信号会发送错误信息。
  • run 方法:线程启动后自动执行的函数,里面封装了从网络请求到数据解析的全过程。这样耗时操作被放在后台,界面就能保持流畅。

3. 网络爬取与数据解析:从网页中 “挖” 出天气信息

run方法里藏着获取天气数据的核心逻辑,主要分三步:

第一步:发送网络请求

requests.get访问天气网页,为了避免被网站拒绝,需要设置headers模拟浏览器:

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/86.0.4240.198...'
}
url = f'http://www.weather.com.cn/weather/{citycode}.shtml'
res = requests.get(url, headers=headers, timeout=20)

timeout=20表示如果 20 秒内没拿到数据,就触发超时错误,避免程序无限等待。

第二步:解析 HTML 内容

拿到网页内容后,用BeautifulSoup提取需要的信息。打开天气网页面源码会发现,温度、日期、天气状况分别藏在特定标签里:

  • 温度在p class="tem"标签中
  • 日期在ul class="t clearfix"下的h1标签中
  • 天气状况在p class="wea"标签中

所以代码里用find_all精准定位这些标签

soup = BeautifulSoup(res.text, 'html.parser')
tem_list = soup.find_all('p', class_='tem')  # 温度列表
day_list = soup.find('ul', class_='t clearfix').find_all('h1')  # 日期列表
wealist = soup.find_all('p', class_='wea')  # 天气状况列表
第三步:处理特殊情况

有时候网页中某一天的最高温度会缺失(比如当天只显示最低温),这时候直接提取会报错。代码用try-except处理这种情况:

try:
    temHigh = tem_list[i].span.string  # 尝试获取最高温
except AttributeError:
    # 当天没有最高温时,用第二天的代替(最后一天则显示N/A)
    temHigh = tem_list[i + 1].span.string if i < 6 else "N/A"

这种 “容错处理” 能让程序更稳定,不会因为数据格式小变化就崩溃。

4. WeatherApp:搭建美观又好用的界面

界面是程序的 “脸面”,WeatherApp类负责把各种控件(输入框、按钮、表格)组合成一个完整窗口,主要做了三件事:

第一步:设计布局

用 PyQt5 的布局管理器(QVBoxLayout纵向布局、QHBoxLayout横向布局)排列控件,避免手动调整位置的麻烦:

  • 顶部放标题标签
  • 中间用横向布局放输入框和查询按钮
  • 下面是状态提示标签
  • 核心区域是表格控件
  • 底部放数据来源说明

这样无论窗口怎么拉伸,控件都会自动调整位置,保持美观。

第二步:美化界面

通过setStyleSheet设置 CSS 样式,让界面摆脱默认的 “原生丑陋”:

self.setStyleSheet("""
    QPushButton {
        background-color: #4a86e8;
        color: white;
        border-radius: 5px;
    }
    QPushButton:hover {
        background-color: #3a76d8;  /* 鼠标悬停时变色 */
    }
""")

按钮的圆角、悬停效果,表格的网格线颜色,甚至不同天气的背景色,都是通过样式表实现的,让程序看起来更专业。

第三步:绑定事件与信号

界面控件需要和功能逻辑关联,比如点击 “查询按钮” 要触发查询操作,线程返回数据后要更新表格。这就需要用到 PyQt5 的 “信号与槽” 机制:

# 按钮点击时,调用get_weather方法
self.search_btn.clicked.connect(self.get_weather)

# 线程查询成功时,调用display_weather显示数据
self.worker.finished.connect(self.display_weather)

# 线程出错时,调用show_error显示错误
self.worker.error.connect(self.show_error)

这种 “松耦合” 的设计让界面和逻辑分离,便于后续修改和扩展。

5. 数据展示:让天气信息清晰呈现

display_weather方法负责把解析好的天气数据填充到表格中:

  • 先设置表格行数为 7(对应 7 天数据)
  • 循环将日期、最高温、最低温、天气状况分别放入表格单元格
  • 根据天气类型设置单元格背景色(雨天蓝色、晴天黄色等)

表格设置为 “不可编辑”(setEditTriggers(QTableWidget.NoEditTriggers)),避免用户误操作;列宽设置为 “自适应”(QHeaderView.Stretch),确保在不同屏幕上都能完整显示。


四、细节设计:让程序更 “抗造” 的小技巧

一个好用的程序不仅要能正常工作,还要能应对各种意外情况,这个程序里有几个值得学习的细节:

  • 防止重复点击:查询时禁用按钮(self.search_btn.setEnabled(False)),避免用户连续点击导致多个线程同时运行,查询完成后再恢复可用。
  • 详细的异常处理:网络超时、城市不存在、网页结构变化等情况都有对应的错误提示,用户知道哪里出了问题,而不是一脸懵。
  • 状态反馈:查询中显示 “正在获取数据”,成功后显示城市名,出错时显示错误信息,让用户清楚当前状态。
  • 输入校验:用户没输入城市或输入不支持的城市时,用QMessageBox弹窗提示,避免无效请求。

五、扩展方向:让你的程序更强大

学会这个基础版本后,你可以试着给程序加功能,比如:

  • 添加更多城市:在CITY_CODES字典中补充更多城市的编码(可以从中国天气网查询)。
  • 增加实时天气:爬取当天的实时温度、风力等信息,在表格上方显示。
  • 历史数据对比:将每天的天气数据保存到本地文件,生成温度变化曲线图。
  • 设置提醒:当预报有雨时,自动弹出 “带伞提醒”。

六、总结:PyQt5 开发不难,动手做才是关键

这个天气预报程序虽然简单,但涵盖了 PyQt5 开发的核心知识点:界面布局、控件使用、信号与槽、多线程、样式美化,还结合了网络爬虫和数据解析的技巧。其实 PyQt5 入门并不难,关键是多动手写代码,遇到问题时逐行调试(比如打印res.text看网页内容,打印soup看解析结果)。

试着自己运行一下代码,输入你所在的城市,看看能不能正确显示天气?如果遇到报错,仔细看看错误信息指向哪一行,慢慢排查 —— 编程能力就是在解决问题的过程中一点点提升的。

当你能用代码做出一个每天都能用到的工具时,那种成就感远比背知识点来得强烈。现在就动手试试,让你的电脑桌面多一个自己写的实用工具吧!


附:本项目完整代码

已调试好,安装好依赖库后可以直接运行

当前版本仅支持全国所有省份省会城市以及特定城市的天气查询,若要增添城市,可在

CITY_CODES 定义的字典里按模板添加城市与其对应的代码

import sys  # 用于Python解释器相关操作,如命令行参数、退出等
import requests  # 用于发送HTTP网络请求
from bs4 import BeautifulSoup  # 用于解析和提取HTML页面内容
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
                             QLineEdit, QPushButton, QTableWidget, QTableWidgetItem,
                             QHeaderView, QLabel, QMessageBox)  # PyQt5界面相关控件
from PyQt5.QtCore import Qt, QThread, pyqtSignal  # Qt常用枚举、线程类、信号机制
from PyQt5.QtGui import QFont, QColor, QPalette  # 字体、颜色、调色板相关

# 城市代码字典,城市名称 -> weather.com.cn的城市编码
CITY_CODES = {
    # 直辖市
    '北京': '101010100',
    '上海': '101020100',
    '天津': '101030100',
    '重庆': '101040100',

    # 省会
    '石家庄': '101090101',
    '太原': '101100101',
    '沈阳': '101070101',
    '长春': '101060101',
    '哈尔滨': '101050101',
    '南京': '101190101',
    '杭州': '101210101',
    '合肥': '101220101',
    '福州': '101230101',
    '南昌': '101240101',
    '济南': '101120101',
    '郑州': '101180101',
    '武汉': '101200101',
    '长沙': '101250101',
    '广州': '101280101',
    '海口': '101310101',
    '成都': '101270101',
    '贵阳': '101260101',
    '昆明': '101290101',
    '西安': '101110101',
    '兰州': '101160101',
    '西宁': '101150101',
    '台北': '101340101',

    # 自治区首府
    '呼和浩特': '101080101',
    '南宁': '101300101',
    '拉萨': '101140101',
    '银川': '101170101',
    '乌鲁木齐': '101130101',

    # 特别行政区
    '香港': '101320101',
    '澳门': '101330101',

    # 其他重要城市
    '桂林': '101300501',
    '柳州': '101300301',
}

class WeatherWorker(QThread):
    """后台线程处理天气数据获取,防止阻塞界面主线程"""
    finished = pyqtSignal(dict, str)  # 查询完成信号,参数:天气数据和城市名
    error = pyqtSignal(str)  # 查询出错信号,参数:错误信息

    def __init__(self, city_name):
        super().__init__()  # 初始化父类QThread
        self.city_name = city_name  # 需要查询天气的城市名

    def run(self):
        """线程开始运行后执行的函数"""
        try:
            citycode = CITY_CODES.get(self.city_name)  # 获取城市编码
            if not citycode:
                self.error.emit(f"未找到城市: {self.city_name}")  # 没找到城市
                return

            headers = {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                              'AppleWebKit/537.36 (KHTML, like Gecko) '
                              'Chrome/86.0.4240.198 Safari/537.36'
            }
            url = f'http://www.weather.com.cn/weather/{citycode}.shtml'  # 拼接网页URL

            # 发送HTTP GET请求
            res = requests.get(url, headers=headers, timeout=20)
            res.encoding = 'utf-8'  # 指定编码

            # 检查HTTP响应状态码
            if res.status_code != 200:
                self.error.emit(f"网络错误: HTTP {res.status_code}")
                return

            # 用BeautifulSoup解析HTML文档
            soup = BeautifulSoup(res.text, 'html.parser')

            # 找到包含温度的所有<p class="tem">标签
            tem_list = soup.find_all('p', class_='tem')
            # 找到包含日期的<ul class="t clearfix">下面的所有<h1>标签
            day = soup.find('ul', class_='t clearfix')
            day_list = day.find_all('h1')
            # 找到包含天气描述的所有<p class="wea">标签
            wealist = soup.find_all('p', class_='wea')

            # 用于存储7天的天气数据
            weather_data = {}
            for i in range(7):  # 解析7天数据
                try:
                    # 最高温度可能不存在(如当天只有最低温),做特殊处理
                    try:
                        temHigh = tem_list[i].span.string  # 最高温
                    except AttributeError:
                        temHigh = tem_list[i + 1].span.string if i < 6 else "N/A"  # 兼容特殊情况

                    temLow = tem_list[i].i.string  # 最低温
                    wea = wealist[i].string  # 天气描述

                    # 存入字典,日期为key
                    weather_data[day_list[i].string] = {
                        '最高温度': temHigh,
                        '最低温度': temLow,
                        '天气': wea
                    }
                except Exception as e:
                    self.error.emit(f"解析错误: {str(e)}")  # 解析失败
                    return

            # 查询成功,发出信号
            self.finished.emit(weather_data, self.city_name)

        except requests.exceptions.Timeout:
            self.error.emit("请求超时,请重试")  # 超时错误
        except requests.exceptions.RequestException as e:
            self.error.emit(f"网络请求失败: {str(e)}")  # 其他请求错误
        except Exception as e:
            self.error.emit(f"未知错误: {str(e)}")  # 其他未知错误


class WeatherApp(QMainWindow):
    """主应用程序窗口类,继承自QMainWindow"""

    def __init__(self):
        super().__init__()  # 初始化父类
        self.initUI()  # 初始化界面
        self.worker = None  # 后台线程初始化为None

    def initUI(self):
        """设置主窗口和界面布局"""
        self.setWindowTitle("天气预报查询")  # 窗口标题
        self.setGeometry(300, 300, 800, 600)  # 窗口位置和大小

        # 设置整体界面样式
        self.setStyleSheet("""
            QMainWindow {
                background-color: #f0f8ff;
            }
            QPushButton {
                background-color: #4a86e8;
                color: white;
                border-radius: 5px;
                padding: 8px 16px;
                font-weight: bold;
            }
            QPushButton:hover {
                background-color: #3a76d8;
            }
            QLineEdit {
                padding: 8px;
                border: 1px solid #ccc;
                border-radius: 4px;
                font-size: 14px;
            }
            QTableWidget {
                background-color: white;
                border: 1px solid #ddd;
                border-radius: 4px;
                gridline-color: #e0e0e0;
            }
            QHeaderView::section {
                background-color: #e3f2fd;
                padding: 8px;
                border: none;
                font-weight: bold;
            }
        """)

        # 创建主部件和布局
        main_widget = QWidget()  # 主体widget
        self.setCentralWidget(main_widget)  # 设置为中心部件
        main_layout = QVBoxLayout()  # 纵向布局
        main_widget.setLayout(main_layout)  # 应用布局

        # 标题标签
        title = QLabel("7天天气预报查询")
        title_font = QFont("Arial", 18, QFont.Bold)
        title.setFont(title_font)
        title.setAlignment(Qt.AlignCenter)
        title.setStyleSheet("color: #1a5276; margin: 20px 0;")
        main_layout.addWidget(title)

        # 输入区域布局
        input_layout = QHBoxLayout()
        self.city_input = QLineEdit()  # 城市输入框
        self.city_input.setPlaceholderText("输入城市名称 (如: 北京)")
        self.city_input.setMinimumHeight(40)
        input_layout.addWidget(self.city_input)

        self.search_btn = QPushButton("查询天气")  # 查询按钮
        self.search_btn.setMinimumHeight(40)
        self.search_btn.clicked.connect(self.get_weather)  # 按钮点击事件绑定
        input_layout.addWidget(self.search_btn)

        main_layout.addLayout(input_layout)  # 输入区加入主布局

        # 状态标签,用于显示提示信息
        self.status_label = QLabel("准备查询...")
        self.status_label.setAlignment(Qt.AlignCenter)
        self.status_label.setStyleSheet("color: #7f8c8d; font-style: italic; margin: 10px 0;")
        main_layout.addWidget(self.status_label)

        # 天气表格
        self.weather_table = QTableWidget()  # 表格控件
        self.weather_table.setColumnCount(4)  # 4列:日期、最高温、最低温、天气
        self.weather_table.setHorizontalHeaderLabels(["日期", "最高温度", "最低温度", "天气"])
        self.weather_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)  # 列宽自适应
        self.weather_table.setEditTriggers(QTableWidget.NoEditTriggers)  # 不可编辑
        self.weather_table.verticalHeader().setVisible(False)  # 行头不可见
        self.weather_table.setStyleSheet("""
            QTableWidget::item {
                padding: 12px;
            }
        """)
        main_layout.addWidget(self.weather_table)

        # 底部信息标签
        footer = QLabel("数据来源: 中国天气网")
        footer.setAlignment(Qt.AlignCenter)
        footer.setStyleSheet("color: #95a5a6; margin-top: 20px;")
        main_layout.addWidget(footer)

    def get_weather(self):
        """点击查询按钮时启动天气查询流程"""
        city_name = self.city_input.text().strip()  # 获取输入城市名

        if not city_name:
            QMessageBox.warning(self, "输入错误", "请输入城市名称")  # 未输入城市
            return

        if city_name not in CITY_CODES:
            QMessageBox.warning(self, "城市错误", "该城市不在支持列表中")  # 城市不支持
            return

        # 禁用按钮,防止重复点击
        self.search_btn.setEnabled(False)
        self.status_label.setText("正在查询天气数据...")
        self.status_label.setStyleSheet("color: #3498db;")

        # 清空表格历史数据
        self.weather_table.setRowCount(0)

        # 创建并启动后台线程
        self.worker = WeatherWorker(city_name)
        self.worker.finished.connect(self.display_weather)  # 成功信号连接
        self.worker.error.connect(self.show_error)  # 错误信号连接
        self.worker.start()  # 启动线程

    def display_weather(self, weather_data, city_name):
        """显示查询到的天气数据到表格"""
        self.search_btn.setEnabled(True)  # 恢复按钮可用
        self.status_label.setText(f"{city_name} 7天天气预报")
        self.status_label.setStyleSheet("color: #27ae60; font-weight: bold;")

        self.weather_table.setRowCount(len(weather_data))  # 设置表行数

        # 填充表格
        for row, (date, data) in enumerate(weather_data.items()):
            # 日期
            date_item = QTableWidgetItem(date)
            date_item.setTextAlignment(Qt.AlignCenter)
            self.weather_table.setItem(row, 0, date_item)

            # 最高温度
            high_temp_item = QTableWidgetItem(f"{data['最高温度']}℃")
            high_temp_item.setTextAlignment(Qt.AlignCenter)
            self.weather_table.setItem(row, 1, high_temp_item)

            # 最低温度
            low_temp_item = QTableWidgetItem(f"{data['最低温度']}℃")
            low_temp_item.setTextAlignment(Qt.AlignCenter)
            self.weather_table.setItem(row, 2, low_temp_item)

            # 天气描述
            weather_item = QTableWidgetItem(data['天气'])
            weather_item.setTextAlignment(Qt.AlignCenter)
            self.weather_table.setItem(row, 3, weather_item)

            # 根据天气类型设置单元格背景色
            if "雨" in data['天气']:
                weather_item.setBackground(QColor(220, 237, 255))  # 蓝色
            elif "晴" in data['天气']:
                weather_item.setBackground(QColor(255, 245, 220))  # 黄色
            elif "云" in data['天气']:
                weather_item.setBackground(QColor(240, 240, 240))  # 灰色

    def show_error(self, message):
        """显示错误信息到界面和弹窗"""
        self.search_btn.setEnabled(True)  # 恢复按钮
        self.status_label.setText(message)  # 状态提示
        self.status_label.setStyleSheet("color: #e74c3c;")
        QMessageBox.critical(self, "错误", message)  # 弹窗显示错误


if __name__ == "__main__":
    app = QApplication(sys.argv)  # 创建应用对象,传入命令行参数
    window = WeatherApp()  # 创建主窗口实例
    window.show()  # 显示主窗口
    sys.exit(app.exec_())  # 启动事件循环,主程序入口

从模仿到原创,再到潜能激发

每一个优秀的开发者都是从复制粘贴开始的

勤加练习,终有一天你也能写出改变世界的代码

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/PTYDJI/article/details/150055646

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--