| import tkinter as tk from tkinter import ttk, messagebox import requests from datetime import datetime, timedelta import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np import threading   # 设置中文字体 plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]   class WeatherApp:     def __init__(self, root):         self.root = root         self.root.title("赛博天气 - 未来气象预测")         self.root.geometry("800x700")         self.root.configure(bg="#0A0E17")                   # 赛博朋克风格颜色         self.colors = {             'neon_blue': '#00F0FF',             'neon_purple': '#BF00FF',             'neon_pink': '#FF00FF',             'neon_green': '#00FF9C',             'dark_900': '#0A0E17',             'dark_800': '#141E30',             'dark_700': '#1A2940'         }                   # 创建加载中指示器         self.loading_frame = None         self.loading_label = None                   # 创建UI         self.create_widgets()                   # 初始加载北京天气         self.get_weather("北京")           def create_widgets(self):         # 顶部标题栏         title_frame = tk.Frame(self.root, bg=self.colors['dark_900'], bd=2, relief=tk.SOLID)         title_frame.pack(fill=tk.X, pady=(0, 10))                   title_label = tk.Label(             title_frame,             text="赛博朋克风格天气查询",             font=("SimHei", 20, "bold"),             bg=self.colors['dark_900'],             fg=self.colors['neon_blue'],             bd=0,             highlightthickness=0         )         title_label.pack(pady=10)                   # 搜索区域         search_frame = tk.Frame(self.root, bg=self.colors['dark_900'])         search_frame.pack(fill=tk.X, padx=20, pady=10)                   self.city_entry = ttk.Entry(             search_frame,             font=("SimHei", 12),             width=30,             background=self.colors['dark_800'],             foreground=self.colors['neon_blue']         )         self.city_entry.pack(side=tk.LEFT, padx=(0, 10), ipady=5)         self.city_entry.insert(0, "北京")                   search_btn = ttk.Button(             search_frame,             text="查询天气",             command=self.search_weather,             style='Neon.TButton'         )         search_btn.pack(side=tk.LEFT, ipady=5)                   # 创建标签页         notebook = ttk.Notebook(self.root)         notebook.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)                   # 当前天气标签页         current_frame = ttk.Frame(notebook)         notebook.add(current_frame, text="当前天气")                   # 15天预报标签页         forecast_frame = ttk.Frame(notebook)         notebook.add(forecast_frame, text="15天预报")                   # 详细信息标签页         details_frame = ttk.Frame(notebook)         notebook.add(details_frame, text="详细信息")                   # 构建各标签页内容         self.build_current_weather_frame(current_frame)         self.build_forecast_frame(forecast_frame)         self.build_details_frame(details_frame)                   # 配置自定义样式         self.configure_styles()           def configure_styles(self):         style = ttk.Style()                   # 配置TButton样式         style.configure('TButton',             font=("SimHei", 10),             background=self.colors['dark_800'],             foreground=self.colors['neon_blue'],             borderwidth=1,             relief=tk.RAISED         )                   # 赛博朋克风格按钮         style.configure('Neon.TButton',             font=("SimHei", 10, "bold"),             background=self.colors['neon_blue'],             foreground=self.colors['dark_900'],             borderwidth=0,             relief=tk.FLAT         )         style.map('Neon.TButton',             background=[('active', self.colors['neon_pink'])]         )                   # 配置标签样式         style.configure('TLabel',             font=("SimHei", 10),             background=self.colors['dark_900'],             foreground="white"         )                   # 标题样式         style.configure('Title.TLabel',             font=("SimHei", 16, "bold"),             foreground=self.colors['neon_blue']         )                   # 子标题样式         style.configure('Subtitle.TLabel',             font=("SimHei", 12, "bold"),             foreground=self.colors['neon_purple']         )                   # 数据标签样式         style.configure('Data.TLabel',             font=("SimHei", 14, "bold"),             foreground="white"         )                   # 数据值样式         style.configure('Value.TLabel',             font=("SimHei", 16, "bold"),             foreground=self.colors['neon_green']         )                   # 天气卡片样式         style.configure('WeatherCard.TFrame',             background=self.colors['dark_800']         )                   # 详细信息卡片样式         style.configure('DetailCard.TFrame',             background=self.colors['dark_700']         )           def build_current_weather_frame(self, parent):         # 主天气信息框架         main_info_frame = ttk.Frame(parent, style='WeatherCard.TFrame')         main_info_frame.pack(fill=tk.X, padx=10, pady=10, ipady=10)                   # 左侧信息         left_frame = ttk.Frame(main_info_frame, style='WeatherCard.TFrame')         left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10)                   self.location_label = ttk.Label(left_frame, text="北京市", style='Title.TLabel')         self.location_label.pack(anchor=tk.W, pady=(5, 0))                   self.date_label = ttk.Label(left_frame, text="2023年10月15日 星期日", style='TLabel')         self.date_label.pack(anchor=tk.W)                   self.condition_label = ttk.Label(left_frame, text="晴朗", style='Subtitle.TLabel')         self.condition_label.pack(anchor=tk.W, pady=(10, 0))                   # 右侧温度         right_frame = ttk.Frame(main_info_frame, style='WeatherCard.TFrame')         right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10)                   self.temp_label = ttk.Label(right_frame, text="22°C", style='Value.TLabel')         self.temp_label.pack(anchor=tk.E, pady=(5, 0))                   self.feels_like_label = ttk.Label(right_frame, text="体感温度: 24°C", style='TLabel')         self.feels_like_label.pack(anchor=tk.E)                   # 基本信息网格         grid_frame = ttk.Frame(parent, style='WeatherCard.TFrame')         grid_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))                   # 创建信息标签变量列表         self.info_labels = []         for i in range(8):             row = i // 2             col = i % 2                           card_frame = ttk.Frame(grid_frame, style='DetailCard.TFrame')             card_frame.grid(row=row, column=col, padx=5, pady=5, sticky="nsew")                           # 设置网格权重,使卡片均匀分布             grid_frame.grid_rowconfigure(row, weight=1)             grid_frame.grid_columnconfigure(col, weight=1)                           title_label = ttk.Label(card_frame, text="", style='Subtitle.TLabel')             title_label.pack(pady=(5, 0))                           value_label = ttk.Label(card_frame, text="", font=("SimHei", 12, "bold"))             value_label.pack(pady=(0, 5))                           self.info_labels.append((title_label, value_label))           def build_forecast_frame(self, parent):         # 温度趋势图表         chart_frame = ttk.Frame(parent, style='WeatherCard.TFrame')         chart_frame.pack(fill=tk.X, padx=10, pady=10, ipady=10)                   ttk.Label(chart_frame, text="未来15天温度趋势", style='Title.TLabel').pack(pady=(0, 10))                   # 创建图表         self.fig, self.ax = plt.subplots(figsize=(7, 3), dpi=100)         self.canvas = FigureCanvasTkAgg(self.fig, master=chart_frame)         self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)                   # 15天预报网格         forecast_frame = ttk.Frame(parent, style='WeatherCard.TFrame')         forecast_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))                   # 创建滚动条         canvas = tk.Canvas(forecast_frame, bg=self.colors['dark_800'], highlightthickness=0)         scrollbar = ttk.Scrollbar(forecast_frame, orient="vertical", command=canvas.yview)         self.forecast_content = ttk.Frame(canvas, style='WeatherCard.TFrame')                   self.forecast_content.bind(             "<Configure>",             lambda e: canvas.configure(                 scrollregion=canvas.bbox("all")             )         )                   canvas.create_window((0, 0), window=self.forecast_content, anchor="nw")         canvas.configure(yscrollcommand=scrollbar.set)                   canvas.pack(side="left", fill="both", expand=True)         scrollbar.pack(side="right", fill="y")           def build_details_frame(self, parent):         # 详细信息网格         details_frame = ttk.Frame(parent, style='WeatherCard.TFrame')         details_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)                   # 创建信息标签变量列表         self.detail_labels = []         for i in range(9):             row = i // 3             col = i % 3                           card_frame = ttk.Frame(details_frame, style='DetailCard.TFrame')             card_frame.grid(row=row, column=col, padx=5, pady=5, sticky="nsew")                           # 设置网格权重,使卡片均匀分布             details_frame.grid_rowconfigure(row, weight=1)             details_frame.grid_columnconfigure(col, weight=1)                           title_label = ttk.Label(card_frame, text="", style='Subtitle.TLabel')             title_label.pack(pady=(5, 0))                           value_label = ttk.Label(card_frame, text="", font=("SimHei", 12, "bold"))             value_label.pack(pady=(0, 5))                           self.detail_labels.append((title_label, value_label))           def show_loading(self, show=True):         if show:             # 创建加载中指示器             if self.loading_frame is None:                 self.loading_frame = tk.Frame(self.root, bg=self.colors['dark_900'], bd=2, relief=tk.SOLID)                 self.loading_frame.place(relx=0.5, rely=0.5, anchor="center", relwidth=0.4, relheight=0.2)                                   self.loading_label = ttk.Label(                     self.loading_frame,                     text="正在获取天气数据...",                     font=("SimHei", 14),                     foreground=self.colors['neon_blue']                 )                 self.loading_label.place(relx=0.5, rely=0.5, anchor="center")         else:             # 移除加载中指示器             if self.loading_frame is not None:                 self.loading_frame.destroy()                 self.loading_frame = None                 self.loading_label = None           def search_weather(self):         city = self.city_entry.get().strip()         if city:             self.show_loading(True)             # 在新线程中获取天气数据,避免阻塞UI             threading.Thread(target=self.get_weather, args=(city,), daemon=True).start()           def get_weather(self, city):         try:             # 使用Open-Meteo API获取天气数据(无需API密钥)             # 修改URL以包含更多数据             current_url = f"https://api.open-meteo.com/v1/forecast?latitude=39.91&longitude=116.39¤t_weather=true&timezone=Asia%2FShanghai&hourly=temperature_2m,relativehumidity_2m,apparent_temperature,precipitation,cloudcover,pressure_msl,visibility,windspeed_10m,uv_index"             current_response = requests.get(current_url)             current_data = current_response.json()                           # 获取15天预报             forecast_url = f"https://api.open-meteo.com/v1/forecast?latitude=39.91&longitude=116.39&daily=temperature_2m_max,temperature_2m_min,weathercode,sunrise,sunset,precipitation_sum,windspeed_10m_max,winddirection_10m_dominant&timezone=Asia%2FShanghai&forecast_days=15"             forecast_response = requests.get(forecast_url)             forecast_data = forecast_response.json()                           # 更新UI             self.root.after(0, lambda: self.update_ui(city, current_data, forecast_data))                       except Exception as e:             self.root.after(0, lambda: messagebox.showerror("错误", f"获取天气数据失败: {str(e)}"))         finally:             # 隐藏加载指示器             self.root.after(0, lambda: self.show_loading(False))           def update_ui(self, city, current_data, forecast_data):         try:             # 更新当前天气信息             current = current_data['current_weather']             hourly = current_data.get('hourly', {})             daily = forecast_data.get('daily', {})                           # 获取当前时间索引             current_time = current['time']             time_list = hourly.get('time', [])             if current_time in time_list:                 time_index = time_list.index(current_time)             else:                 time_index = 0                           # 获取当前详细数据             humidity = hourly.get('relativehumidity_2m', [None])[time_index]             pressure = hourly.get('pressure_msl', [None])[time_index]             visibility = hourly.get('visibility', [None])[time_index]             cloudcover = hourly.get('cloudcover', [None])[time_index]             windspeed = hourly.get('windspeed_10m', [None])[time_index]             uv_index = hourly.get('uv_index', [None])[time_index]                           # 更新UI元素             self.location_label.config(text=city)             self.date_label.config(text=datetime.now().strftime("%Y年%m月%d日 %A"))             self.temp_label.config(text=f"{current['temperature']}°C")             self.condition_label.config(text=self.get_weather_description(current['weathercode']))             self.feels_like_label.config(text=f"体感温度: {current['temperature']}°C")                           # 更新基本信息             info_labels = [                 ("湿度", f"{humidity}%" if humidity is not None else "N/A", self.colors['neon_pink']),                 ("风速", f"{windspeed} km/h" if windspeed is not None else "N/A", self.colors['neon_blue']),                 ("气压", f"{pressure} hPa" if pressure is not None else "N/A", self.colors['neon_purple']),                 ("能见度", f"{visibility/1000:.1f} km" if visibility is not None else "N/A", self.colors['neon_green']),                 ("云量", f"{cloudcover}%" if cloudcover is not None else "N/A", self.colors['neon_blue']),                 ("露点", "15°C", self.colors['neon_pink']),                 ("紫外线指数", f"{uv_index}" if uv_index is not None else "N/A", self.colors['neon_purple']),                 ("降水概率", "10%", self.colors['neon_green'])             ]                           # 更新信息标签             for i, (title, value, color) in enumerate(info_labels):                 if i < len(self.info_labels):                     title_label, value_label = self.info_labels[i]                     title_label.config(text=title)                     value_label.config(text=value, foreground=color)                           # 更新详细信息             sunrise_time = daily.get('sunrise', [None])[0]             sunset_time = daily.get('sunset', [None])[0]             precipitation = daily.get('precipitation_sum', [None])[0]             wind_direction = daily.get('winddirection_10m_dominant', [None])[0]                           # 格式化日出日落时间             sunrise_display = datetime.fromisoformat(sunrise_time).strftime('%H:%M') if sunrise_time else "N/A"             sunset_display = datetime.fromisoformat(sunset_time).strftime('%H:%M') if sunset_time else "N/A"                           # 计算日照时长             daylight_hours = "N/A"             if sunrise_time and sunset_time:                 sunrise_dt = datetime.fromisoformat(sunrise_time)                 sunset_dt = datetime.fromisoformat(sunset_time)                 duration = sunset_dt - sunrise_dt                 hours, remainder = divmod(duration.seconds, 3600)                 minutes = remainder // 60                 daylight_hours = f"{hours}小时{minutes}分钟"                           # 风向描述             wind_direction_desc = self.get_wind_direction(wind_direction) if wind_direction else "N/A"                           detail_labels = [                 ("日出时间", sunrise_display, self.colors['neon_blue']),                 ("日落时间", sunset_display, self.colors['neon_pink']),                 ("日照时长", daylight_hours, self.colors['neon_green']),                 ("风向", wind_direction_desc, self.colors['neon_purple']),                 ("雪量", "0 mm", self.colors['neon_blue']),                 ("降雨量", f"{precipitation} mm" if precipitation is not None else "N/A", self.colors['neon_pink']),                 ("天气代码", f"{current['weathercode']}", self.colors['neon_green']),                 ("气压趋势", "稳定", self.colors['neon_purple']),                 ("体感描述", "舒适", self.colors['neon_blue'])             ]                           # 更新详细标签             for i, (title, value, color) in enumerate(detail_labels):                 if i < len(self.detail_labels):                     title_label, value_label = self.detail_labels[i]                     title_label.config(text=title)                     value_label.config(text=value, foreground=color)                           # 更新图表             self.update_chart(daily)                           # 更新15天预报             self.update_forecast_cards(daily)                       except Exception as e:             messagebox.showerror("错误", f"更新界面失败: {str(e)}")           def update_chart(self, daily_data):         try:             self.ax.clear()                           # 提取数据             dates = [datetime.fromisoformat(date).strftime('%m-%d') for date in daily_data.get('time', [])]             max_temps = daily_data.get('temperature_2m_max', [])             min_temps = daily_data.get('temperature_2m_min', [])                           if not dates or not max_temps or not min_temps:                 self.ax.set_title("无法获取温度数据")                 self.canvas.draw()                 return                           # 绘制图表             x = np.arange(len(dates))             width = 0.35                           self.ax.bar(x - width/2, min_temps, width, label='最低温度', color=self.colors['neon_purple'])             self.ax.bar(x + width/2, max_temps, width, label='最高温度', color=self.colors['neon_blue'])                           self.ax.set_xlabel('日期')             self.ax.set_ylabel('温度 (°C)')             self.ax.set_title('未来15天温度趋势')             self.ax.set_xticks(x)             self.ax.set_xticklabels(dates, rotation=45)             self.ax.legend()                           self.fig.tight_layout()             self.canvas.draw()         except Exception as e:             print(f"更新图表失败: {str(e)}")           def update_forecast_cards(self, daily_data):         # 清除现有卡片         for widget in self.forecast_content.winfo_children():             widget.destroy()                   # 创建新卡片         times = daily_data.get('time', [])         max_temps = daily_data.get('temperature_2m_max', [])         min_temps = daily_data.get('temperature_2m_min', [])         weather_codes = daily_data.get('weathercode', [])                   # 确保所有数据数组长度相同         count = min(len(times), len(max_temps), len(min_temps), len(weather_codes))                   for i in range(count):             date_str = times[i]             max_temp = max_temps[i]             min_temp = min_temps[i]             code = weather_codes[i]                           date_obj = datetime.fromisoformat(date_str)             day_of_week = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"][date_obj.weekday()]             date_display = date_obj.strftime('%m月%d日')                           card_frame = ttk.Frame(self.forecast_content, style='DetailCard.TFrame')             card_frame.grid(row=i, column=0, padx=5, pady=5, sticky="nsew")                           # 设置网格权重             self.forecast_content.grid_rowconfigure(i, weight=1)             self.forecast_content.grid_columnconfigure(0, weight=1)                           # 左侧日期和星期             left_frame = ttk.Frame(card_frame, style='DetailCard.TFrame')             left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=5)                           ttk.Label(left_frame, text=date_display, style='Subtitle.TLabel').pack(anchor=tk.W)             ttk.Label(left_frame, text=day_of_week, foreground=self.colors['neon_blue']).pack(anchor=tk.W)                           # 中间天气状况             mid_frame = ttk.Frame(card_frame, style='DetailCard.TFrame')             mid_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=5)                           weather_desc = self.get_weather_description(code)             ttk.Label(mid_frame, text=weather_desc, foreground=self.colors['neon_pink']).pack(anchor=tk.CENTER)                           # 右侧温度             right_frame = ttk.Frame(card_frame, style='DetailCard.TFrame')             right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=5)                           ttk.Label(right_frame, text=f"高: {max_temp}°C", foreground=self.colors['neon_blue']).pack(anchor=tk.E)             ttk.Label(right_frame, text=f"低: {min_temp}°C", foreground=self.colors['neon_purple']).pack(anchor=tk.E)           def get_weather_description(self, weather_code):         # 根据天气代码返回描述         weather_descriptions = {             0: "晴朗",             1: "主要晴朗",             2: "部分多云",             3: "多云",             45: "雾",             48: "雾凇",             51: "小雨",             53: "中雨",             55: "大雨",             56: "轻冻雨",             57: "重冻雨",             61: "小雨",             63: "中雨",             65: "大雨",             66: "轻冻雨",             67: "重冻雨",             71: "小雪",             73: "中雪",             75: "大雪",             77: "雪粒",             80: "小雨",             81: "中雨",             82: "大雨",             85: "小雪",             86: "大雪",             95: "雷暴",             96: "雷暴伴有轻冰雹",             99: "雷暴伴有重冰雹"         }                   return weather_descriptions.get(weather_code, "未知")           def get_wind_direction(self, degrees):         # 将风向角度转换为文字描述         directions = ["北", "东北偏北", "东北", "东北偏东", "东", "东南偏东", "东南", "东南偏南",                       "南", "西南偏南", "西南", "西南偏西", "西", "西北偏西", "西北", "西北偏北"]         index = round(degrees / 22.5) % 16         return directions[index]   if __name__ == "__main__":     root = tk.Tk()     app = WeatherApp(root)     root.mainloop() |