python+tkinter编写一个桌面天气小工具

为什么编写

因为我工作电脑是win7,只能看看日历,没有天气

使用什么库

复制代码
tkinter桌面开发库 +PyInstaller 打包成exe

思路

1、让用户输入城市

2、用拿到的城市去拼接拿到天气的接口

3、处理响应值,让响应值和自己的ui做融合

4、要支持拖动、隐藏窗口、自动更新数据(半小时)

5、ui不要太丑就行

效果

实现代码

import os
import tkinter as tk
import json
import re
from tkinter import ttk

import pystray
from pystray import MenuItem as item
import requests
from tkinter import messagebox
from ttkthemes import ThemedStyle
from PIL import Image,ImageTk
from datetime import datetime

class DraggableWindow:
    def __init__(self, root):
        self.root = root
        self.setup_window()
        self.weather_info_all = {}
        self.w = 30
        self.h = 30
    # 创建窗口
    def setup_window(self):
        self.root.title("天气预报窗口")
        self.root.geometry("160x420") #窗口多大
        self.root.geometry("+1500+500") #在屏幕什么地方显示窗口


        # 设置窗口始终置顶 将窗口置顶。将 1 更改为 0 可以取消置顶效果。
        self.root.wm_attributes("-topmost", 1)
        # 隐藏窗口边框和标题栏
        self.root.overrideredirect(True)
        # 绑定鼠标事件,拖动页面
        self.root.bind("<ButtonPress-1>", self.on_drag_start)
        self.root.bind("<B1-Motion>", self.on_drag_motion)

        # 创建 ThemedStyle 对象并设置主题
        style = ThemedStyle(self.root)
        style.set_theme("arc")  # 设置主题为 "arc"

        # 一个顶部文案
        self.title_label = tk.Label(self.root, text='今日天气查询', padx=5, pady=5)
        self.title_label.pack()
        # 添加城市输入框
        self.city_entry = ttk.Entry(self.root,width=15)
        self.city_entry.pack(pady=5)
        # 把输入框显示出来如果将 pady 设置为正数,例如 pady=10,则表示在控件的上方和下方各添加 10 个像素的空白间隔。

        # 添加获取天气按钮
        self.get_weather_button = ttk.Button(self.root, text="获取天气信息", command=self.weather_write)
        self.get_weather_button.pack(pady=5)

        # 创建查询数据报错的文字,查不到再赋值错误信息,先不定义
        self.error_label = tk.Label(self.root, text="",foreground="red")
        self.error_label.pack(pady=0)


        # 是用来获取根窗口背景色
        self.bg_color = self.root.cget('bg')

        # 想要显示一张图片在初始页面

        # 获取文件所在目录
        self.base_path = os.path.dirname(os.path.abspath(__file__))
        # 拼接图片路径
        nice_day_image_path = os.path.join(self.base_path, "images","niceday.png")
        # 打开图片并调整大小
        day_img = Image.open(nice_day_image_path)
        day_img.thumbnail((120, 280), Image.ANTIALIAS)  # 按比例调整大小,不超过200和500
        self.day_photo = ImageTk.PhotoImage(day_img)  # 保持对图像对象的引用

        # 在Label中显示图片
        self.label_nice_day = ttk.Label(self.root, image= self.day_photo)
        self.label_nice_day.pack(padx=10, pady=0)

        s = ttk.Style()
        s.configure('hide.TButton', font=('Helvetica', 8))
        # 创建隐藏窗口的按钮
        self.hide_button = ttk.Button(self.root, text="隐藏窗口", command=self.hide_window,style='hide.TButton')
        self.hide_button.pack(pady=5)

        # 创建系统托盘图标
        self.create_tray_icon()

    # 我们定义一个函数on_drag_start来处理鼠标按下事件。在这个函数中,我们将更新x和y的值为当前鼠标的坐标。
    def on_drag_start(self, event):
        self.x = event.x
        self.y = event.y
    #接下来,我们定义一个函数on_drag来处理鼠标移动事件。在这个函数中,我们将计算鼠标移动的偏移量,并将窗口的位置移动相应的距离。
    def on_drag_motion(self, event):
        deltax = event.x - self.x
        deltay = event.y - self.y
        x = self.root.winfo_x() + deltax
        y = self.root.winfo_y() + deltay
        self.root.geometry(f"+{x}+{y}")
    # 获取天气数据
    def get_weather(self):
        print("执行定时获取天气信息任务...")
        self.city = self.city_entry.get() + "天气"
        print(self.city)
        base_url = f'https://xxxxx/xxxxx?query={self.city}'
        # print(base_url)
        try:
            response = requests.get(base_url)
            response_text = response.text  # 获取响应的文本数据.空格一定要排除掉,括号里面的是想要的
            match = re.search(r'<script>window.tplData = (.*?);<\/script>', response_text, re.DOTALL)
            if match:
                # 找到符合条件的第一个
                weather_content = match.group(1)
                # 转为json,返回的数据是str
                json_weather_data = json.loads(weather_content)
                # print("转为json的数据:")
                # print(json_weather_data)
                # 日期
                base_time = json_weather_data['base']['date']
                # 温度
                temperature = json_weather_data['weather']['temperature']
                # 天气类型
                weather_type = json_weather_data['weather']['weather']
                # 风的级别
                wind_power = json_weather_data['weather']['wind_power']
                # 体感温度
                body_temp_info = json_weather_data['weather']['bodytemp_info']
                pm = json_weather_data['ps_pm25']['level']
                pm_number = json_weather_data['ps_pm25']['ps_pm25']
                weather_dict = {}

                current_time = datetime.now()
                # 格式化时间为字符串
                formatted_time = current_time.strftime("%Y-%m-%d %H:%M:%S")
                weather_dict["now_time"] = formatted_time
                weather_dict['base_time']= base_time
                weather_dict['temperature']= temperature
                weather_dict['weather_type']= weather_type
                weather_dict['wind_power']= wind_power
                weather_dict['body_temp_info']= body_temp_info
                weather_dict['pm_number']= pm_number
                # 拿到的值赋值给内部天气变量
                self.weather_info_all = weather_dict
                # 搞了个定时任务,每30分钟刷新一次数据,掉个方法,方法里面隐藏原有的组件,重新展示现在的
                self.root.after(30 * 60 * 1000, self.forget_all_once_again)
        except Exception as e:
            messagebox.showerror("Error", f"Error fetching data: {e}")

    # 接受获取天气数据接口,然后显示frame
    def weather_write(self):
        try:
            # 拿到接口响应值
            self.get_weather()
            # 判断dic是不是为空
            if self.weather_info_all:
                # 拿到接口返回值之后隐藏输入框和按钮
                print('开始隐藏数据')
                self.hide_button_entry()

                # # 添加内容
                self.title_label2 = tk.Label(self.root, text=f"{self.weather_info_all['now_time']}", padx=20, pady=5)
                self.title_label2.pack()
                self.title_label3 = tk.Label(self.root, text=f"今日{self.city}", padx=20, pady=5)
                self.title_label3.pack()
                # 创建一个主框架,主框架下面几个子框架
                self.main_frame = tk.Frame(self.root)
                self.main_frame.pack(pady=0, padx=0, fill='both', expand=True)

                # 第一个子框架获取天气状态
                # 调取一个方法传入天气字段,传入主框架main_frame,传入这个方法显示天气类型:阴 晴天 啥的数据返回一个frame
                self.frame_weather = self.weather_type_frame(self.main_frame,self.weather_info_all['weather_type'])
                self.frame_weather.pack(pady=5, padx=0, fill='both', expand=True)

                # # 第二个子框架,显示温度数字
                self.frame_temperature = self.weather_number_frame(self.main_frame,self.weather_info_all['temperature'])
                self.frame_temperature.pack(pady=5, padx=0, fill='both', expand=True)

                # 第三个子框架,显示风力级别
                self.frame_wind = self.weather_wind_frame(self.main_frame, self.weather_info_all['wind_power'])
                self.frame_wind.pack(pady=5, padx=0, fill='both', expand=True)

                # 第四个子框架 显示体感舒适度
                self.frame_feel = self.weather_feel_frame(self.main_frame, self.weather_info_all['body_temp_info'])
                self.frame_feel.pack(pady=5, padx=0, fill='both', expand=True)
            else:
                # 一个报错文案 没拿到数据
                self.error_label.config(text="查不到该地区的天气")

        except Exception as c:
            messagebox.showerror("Error", f"Error fetching data: {c}")

    # 隐藏数据后再请求一次,定时任务控制
    def forget_all_once_again(self):
        self.title_label2.pack_forget()
        self.title_label3.pack_forget()
        self.main_frame.pack_forget()
        self.frame_weather.pack_forget()
        self.frame_temperature.pack_forget()
        self.frame_wind.pack_forget()
        self.frame_feel.pack_forget()
        # 全部隐藏后再次请求一次显示数据
        self.weather_write()


    # 隐藏获取天气数据的按钮、隐藏输入框、报错框
    def hide_button_entry(self):
        self.get_weather_button.pack_forget()  # 隐藏按钮
        self.city_entry.pack_forget()# 隐藏
        self.title_label.pack_forget()# 隐藏
        self.label_nice_day.pack_forget()# 隐藏

        if self.error_label and self.error_label.winfo_exists():  # 检查是否已经创建且存在,存在就隐藏
            print('元素存在')
            self.error_label.pack_forget()
        else:
            print("Label does not exist.")


    # 拿到天气文案,显示内容,返回一个frame
    def weather_type_frame(self,main_frame,weather_info):
        # 拿到传入的值,中文天气
        weather_info_get = weather_info
        # 创建一个子框架,显示天气图标和天气文案,子框架不要边框
        frame1 = tk.Frame(main_frame,relief=tk.FLAT, borderwidth=2)

        # 判断天气文案是什么,显示什么图片,调取一个方法,传入文字和想要显示到哪个frame
        if "晴" in weather_info_get :
            self.re_weather_type_phote("qing.png",frame1)
        elif "阴" in weather_info_get :
            self.re_weather_type_phote("yintian.png",frame1)
        elif "雨" in weather_info_get:
            self.re_weather_type_phote("xiayu.png", frame1)
        elif "风" in weather_info_get:
            self.re_weather_type_phote("feng.png", frame1)
        elif "雷" in weather_info_get:
            self.re_weather_type_phote("lei.png", frame1)
        else:
            self.re_weather_type_phote("anyone.png", frame1)
        # 创建一个天气标签并居中对齐 放到frame1里面
        self.re_weather_text(weather_info_get,frame1)
        # 调用2个方法后,返回一个frame1
        return frame1

    #temperature的iframe显示
    def weather_number_frame(self,main_frame,weather_info):
        # 拿到传入的值
        weather_info_get = weather_info
        # 创建一个子框架,显示天气图标和天气文案,子框架不要边框
        frame2 = tk.Frame(main_frame,relief=tk.FLAT, borderwidth=2)

        self.re_weather_type_phote("temperature.png",frame2)
        # 创建一个温度标签并居中对齐 放到frame里面
        self.re_weather_text(weather_info_get,frame2)
        # 调用2个方法后,返回一个frame1
        return frame2

    #风力wind_power的iframe显示
    def weather_wind_frame(self,main_frame,weather_info):
        # 拿到传入的值
        weather_info_get = weather_info
        # 创建一个子框架,显示天气图标和天气文案,子框架不要边框
        frame3 = tk.Frame(main_frame,relief=tk.FLAT, borderwidth=2)

        self.re_weather_type_phote("wind.png",frame3)
        # 创建一个风力标签并居中对齐 放到frame里面
        self.re_weather_text(weather_info_get,frame3)
        # 调用2个方法后,返回一个frame1
        return frame3

    #体感feel的iframe显示
    def weather_feel_frame(self,main_frame,weather_info):
        # 拿到传入的值
        weather_info_get = weather_info
        if int(self.weather_info_all['temperature']) > 25:
            # 创建一个子框架,显示天气图标和天气文案,子框架不要边框
            frame4 = tk.Frame(main_frame,relief=tk.FLAT, borderwidth=2)

            self.re_weather_type_phote("duanxiu.png",frame4)
        elif int(self.weather_info_all['temperature']) <= 28:
            # 创建一个子框架,显示天气图标和天气文案,子框架不要边框
            frame4 = tk.Frame(main_frame,relief=tk.FLAT, borderwidth=2)
            self.re_weather_type_phote("fengyi.png",frame4)
        else:
            frame4 = tk.Frame(main_frame, relief=tk.FLAT, borderwidth=2)
            self.re_weather_type_phote("yifu.png", frame4)
        # 创建一个温度标签并居中对齐 放到frame里面
        self.re_weather_text(weather_info_get,frame4)
        # 调用2个方法后,返回一个frame1
        return frame4


    # 显示图片的方法
    def re_weather_type_phote(self,photo_name,frame_name):

        # 获取文件所在目录
        base_path = os.path.dirname(os.path.abspath(__file__))
        # 拼接图片路径
        image_path = os.path.join(base_path, "images", photo_name)
        weather_type_img = Image.open(image_path).resize((self.w, self.h))
        weather_type_photo = ImageTk.PhotoImage(weather_type_img)
        frame_label1 = ttk.Label(frame_name, image=weather_type_photo, background=self.bg_color)
        frame_label1.image = weather_type_photo  # 保持图片对象的引用,防止被垃圾回收器回收
        frame_label1.pack(anchor='center', pady=0)

    # frame中显示文字的方法
    def re_weather_text(self,text,frame_name):
        frame_label2 = tk.Label(frame_name, text=text, font=('Microsoft YaHei', 10))
        frame_label2.pack(anchor='center', pady=5)

    # 获取输入框输入的城市
    def get_city_name(self):
        # 直接获取输入框中的值
        print(self.city_entry.get())
        return self.city_entry.get()

    # 状态栏图标方法
    def create_tray_icon(self):
        # 加载自定义图标
        icon_image_path = os.path.join(self.base_path, "images", "icon.png")
        icon_image = Image.open(icon_image_path)

        # 创建托盘菜单项
        menu = pystray.Menu(
            item('Show', self.show_window),
            item('Exit', self.exit_app)
        )

        # 创建托盘图标
        self.tray_icon = pystray.Icon("WeatherApp", icon_image, "Weather App", menu)

        # 启动托盘图标
        self.tray_icon.run_detached()

    def hide_window(self):
        self.root.withdraw()

    def show_window(self, icon, item):
        self.root.deiconify()

    def exit_app(self, icon, item):
        self.tray_icon.stop()
        self.root.quit()
if __name__ == "__main__":
    root = tk.Tk()  # 创建Tkinter主窗口
    app = DraggableWindow(root)  # 创建DraggableWindow实例
    root.mainloop()  # 启动Tkinter主事件循环

打包成exe

当前目录根目录下执行

python -m PyInstaller --name "SunRain" --onefile --noconsole --add-data "images;images" sun_rain.py

--name:打包后的exe名字

--noconsole:运行后不显示黑色窗口

--add-data:一起打包进入的图片目录

注意事项

1、我隐去了查询的接口,你们可以自己找公共开放的接口

2、打包成exe后发现特别大,这么简单的程序打包出来有10m,估计是为了在其他没有python环境的电脑上运行所以打包了python虚拟机和依赖包进去,由此证明python真的不适合大型项目开发

3、打包的image图片目录要和py文件在一个层级,打包出来后dist目录下的exe文件可以不依赖任何环境运行

4、如果想看到实时的天气,就需要修改,目前是30分钟

复制代码
self.root.after(30 * 60 * 1000, self.forget_all_once_again)
相关推荐
重生之我是数学王子10 分钟前
QT基础 编码问题 定时器 事件 绘图事件 keyPressEvent QT5.12.3环境 C++实现
开发语言·c++·qt
Ai 编码助手12 分钟前
使用php和Xunsearch提升音乐网站的歌曲搜索效果
开发语言·php
学习前端的小z16 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
神仙别闹23 分钟前
基于C#和Sql Server 2008实现的(WinForm)订单生成系统
开发语言·c#
XINGTECODE24 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
zwjapple41 分钟前
typescript里面正则的使用
开发语言·javascript·正则表达式
小五Five42 分钟前
TypeScript项目中Axios的封装
开发语言·前端·javascript
前端每日三省44 分钟前
面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?
开发语言·前端·javascript
好看资源平台1 小时前
网络爬虫——综合实战项目:多平台房源信息采集与分析系统
爬虫·python
凡人的AI工具箱1 小时前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang