天气悬浮窗口

pull/109/head
Maziang 3 months ago
parent cd22077341
commit 7a85f36544

@ -0,0 +1,609 @@
# -*- coding: utf-8 -*-
"""
天气悬浮窗口模块
提供一个可拖拽的天气悬浮窗口显示当前天气信息
"""
import sys
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QFrame, QTextEdit, QDialog, QComboBox
)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QPoint, QThread
from PyQt5.QtGui import QFont, QPalette, QColor
# 导入主题管理器
from .theme_manager import theme_manager
class WeatherFloatingWidget(QDialog):
"""天气悬浮窗口类"""
# 自定义信号
closed = pyqtSignal()
refresh_requested = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self.drag_position = None
self.is_dragging = False
self.weather_data = None
self.setup_ui()
self.setup_connections()
self.setup_theme()
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
self.setAttribute(Qt.WA_TranslucentBackground)
def setup_ui(self):
"""设置UI界面"""
# 设置窗口属性
self.setWindowTitle("天气")
self.setFixedSize(400, 320) # 增加窗口尺寸
# 创建主框架,用于实现圆角和阴影效果
self.main_frame = QFrame()
self.main_frame.setObjectName("mainFrame")
# 主布局
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(self.main_frame)
# 内容布局
content_layout = QVBoxLayout(self.main_frame)
content_layout.setContentsMargins(15, 15, 15, 15) # 减小内边距
content_layout.setSpacing(10) # 减小间距
# 设置最小尺寸策略
self.main_frame.setMinimumSize(380, 300)
# 标题栏
title_layout = QHBoxLayout()
self.title_label = QLabel("天气信息")
self.title_label.setFont(QFont("Arial", 12, QFont.Bold))
title_layout.addWidget(self.title_label)
title_layout.addStretch()
# 关闭按钮
self.close_btn = QPushButton("×")
self.close_btn.setFixedSize(20, 20)
self.close_btn.setObjectName("closeButton")
title_layout.addWidget(self.close_btn)
content_layout.addLayout(title_layout)
# 分隔线
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setObjectName("separator")
content_layout.addWidget(separator)
# 天气图标和温度显示区域
weather_display_layout = QHBoxLayout()
weather_display_layout.setSpacing(8) # 适当间距
weather_display_layout.setContentsMargins(2, 2, 2, 2) # 减小内边距
self.weather_icon_label = QLabel("🌞")
self.weather_icon_label.setFont(QFont("Arial", 28))
self.weather_icon_label.setAlignment(Qt.AlignCenter)
self.weather_icon_label.setFixedSize(60, 60)
weather_display_layout.addWidget(self.weather_icon_label)
# 温度和城市信息
temp_city_layout = QVBoxLayout()
temp_city_layout.setSpacing(8) # 增加间距
temp_city_layout.setContentsMargins(0, 0, 0, 0)
self.temperature_label = QLabel("25°C")
self.temperature_label.setFont(QFont("Arial", 20, QFont.Bold))
self.temperature_label.setObjectName("temperatureLabel")
temp_city_layout.addWidget(self.temperature_label)
self.city_label = QLabel("北京")
self.city_label.setFont(QFont("Arial", 12))
self.city_label.setObjectName("cityLabel")
temp_city_layout.addWidget(self.city_label)
weather_display_layout.addLayout(temp_city_layout)
weather_display_layout.addStretch()
content_layout.addLayout(weather_display_layout)
# 天气描述
self.weather_desc_label = QLabel("晴天")
self.weather_desc_label.setFont(QFont("Arial", 12))
self.weather_desc_label.setObjectName("weatherDescLabel")
self.weather_desc_label.setAlignment(Qt.AlignCenter)
content_layout.addWidget(self.weather_desc_label)
# 详细信息(湿度、风速)
details_layout = QHBoxLayout()
details_layout.setSpacing(10) # 适当间距
details_layout.setContentsMargins(2, 2, 2, 2) # 减小内边距
self.humidity_label = QLabel("湿度: 45%")
self.humidity_label.setFont(QFont("Arial", 11))
self.humidity_label.setObjectName("detailLabel")
details_layout.addWidget(self.humidity_label)
self.wind_label = QLabel("风速: 2级")
self.wind_label.setFont(QFont("Arial", 11))
self.wind_label.setObjectName("detailLabel")
details_layout.addWidget(self.wind_label)
content_layout.addLayout(details_layout)
# 城市选择区域
city_layout = QHBoxLayout()
city_layout.setSpacing(10)
city_layout.setContentsMargins(0, 0, 0, 0)
self.city_combo = QComboBox()
self.city_combo.setObjectName("cityCombo")
# 添加所有省会城市,与主窗口保持一致
self.city_combo.addItems([
'自动定位',
'北京', '上海', '广州', '深圳', '杭州', '南京', '武汉', '成都', '西安', # 一线城市
'天津', '重庆', '苏州', '青岛', '大连', '宁波', '厦门', '无锡', '佛山', # 新一线城市
'石家庄', '太原', '呼和浩特', '沈阳', '长春', '哈尔滨', # 东北华北
'合肥', '福州', '南昌', '济南', '郑州', '长沙', '南宁', '海口', # 华东华中华南
'贵阳', '昆明', '拉萨', '兰州', '西宁', '银川', '乌鲁木齐' # 西南西北
])
self.city_combo.setFixedWidth(120) # 增加城市选择框宽度,与主窗口保持一致
city_layout.addWidget(self.city_combo)
city_layout.addStretch()
content_layout.addLayout(city_layout)
# 按钮区域
button_layout = QHBoxLayout()
button_layout.setSpacing(10)
button_layout.setContentsMargins(0, 0, 0, 0)
self.refresh_btn = QPushButton("刷新")
self.refresh_btn.setObjectName("refreshButton")
button_layout.addWidget(self.refresh_btn)
button_layout.addStretch()
self.detail_btn = QPushButton("详情")
self.detail_btn.setObjectName("detailButton")
button_layout.addWidget(self.detail_btn)
content_layout.addLayout(button_layout)
# 添加弹性空间
content_layout.addStretch()
def setup_connections(self):
"""设置信号连接"""
self.close_btn.clicked.connect(self.close_window)
self.refresh_btn.clicked.connect(self.on_refresh_clicked)
self.detail_btn.clicked.connect(self.show_detailed_weather)
self.city_combo.currentTextChanged.connect(self.on_city_changed)
def setup_theme(self):
"""设置主题"""
# 连接主题切换信号
theme_manager.theme_changed.connect(self.on_theme_changed)
# 应用当前主题
self.apply_theme()
def apply_theme(self):
"""应用主题样式"""
is_dark = theme_manager.is_dark_theme()
colors = theme_manager.get_current_theme_colors()
if is_dark:
# 深色主题样式
self.main_frame.setStyleSheet(f"""
QFrame#mainFrame {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 8px;
}}
QLabel {{
color: {colors['text']};
background-color: transparent;
padding: 4px 6px;
margin: 2px;
}}
QLabel#temperatureLabel {{
color: {colors['accent']};
font-size: 20px;
font-weight: bold;
padding: 6px 8px;
margin: 3px;
}}
QLabel#cityLabel {{
color: {colors['text_secondary']};
font-size: 12px;
padding: 3px 5px;
margin: 2px;
}}
QLabel#weatherDescLabel {{
color: {colors['text']};
font-size: 12px;
font-weight: 500;
padding: 4px 6px;
margin: 2px;
}}
QLabel#detailLabel {{
color: {colors['text_secondary']};
font-size: 11px;
padding: 3px 5px;
margin: 2px;
}}
QFrame#separator {{
background-color: {colors['border']};
}}
QPushButton#closeButton {{
background-color: transparent;
border: none;
color: {colors['text']};
font-weight: bold;
}}
QPushButton#closeButton:hover {{
background-color: #e81123;
color: white;
border-radius: 3px;
}}
QPushButton#refreshButton, QPushButton#detailButton {{
background-color: {colors['accent']};
color: white;
border: none;
border-radius: 6px;
padding: 6px 16px;
font-size: 11px;
font-weight: 500;
}}
QPushButton#refreshButton:hover, QPushButton#detailButton:hover {{
background-color: {colors['accent_hover']};
}}
QComboBox#cityCombo {{
background-color: {colors['surface']};
color: {colors['text']};
border: 1px solid {colors['border']};
border-radius: 6px;
padding: 4px 8px;
font-size: 11px;
font-weight: 500;
min-height: 24px;
}}
QComboBox#cityCombo:hover {{
border-color: {colors['accent']};
}}
QComboBox#cityCombo::drop-down {{
border: none;
width: 15px;
}}
QComboBox#cityCombo::down-arrow {{
image: none;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
border-top: 5px solid {colors['text']};
}}
""")
else:
# 浅色主题样式
self.main_frame.setStyleSheet(f"""
QFrame#mainFrame {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}}
QLabel {{
color: {colors['text']};
background-color: transparent;
padding: 4px 6px;
margin: 2px;
}}
QLabel#temperatureLabel {{
color: {colors['accent']};
font-size: 20px;
font-weight: bold;
padding: 6px 8px;
margin: 3px;
}}
QLabel#cityLabel {{
color: {colors['text_secondary']};
font-size: 12px;
padding: 3px 5px;
margin: 2px;
}}
QLabel#weatherDescLabel {{
color: {colors['text']};
font-size: 12px;
font-weight: 500;
padding: 4px 6px;
margin: 2px;
}}
QLabel#detailLabel {{
color: {colors['text_secondary']};
font-size: 11px;
padding: 3px 5px;
margin: 2px;
}}
QFrame#separator {{
background-color: {colors['border']};
}}
QPushButton#closeButton {{
background-color: transparent;
border: none;
color: {colors['text']};
font-weight: bold;
}}
QPushButton#closeButton:hover {{
background-color: #e81123;
color: white;
border-radius: 3px;
}}
QPushButton#refreshButton, QPushButton#detailButton {{
background-color: {colors['accent']};
color: white;
border: none;
border-radius: 6px;
padding: 6px 16px;
font-size: 11px;
font-weight: 500;
}}
QPushButton#refreshButton:hover, QPushButton#detailButton:hover {{
background-color: {colors['accent_hover']};
}}
QComboBox#cityCombo {{
background-color: {colors['surface']};
color: {colors['text']};
border: 1px solid {colors['border']};
border-radius: 6px;
padding: 4px 8px;
font-size: 11px;
font-weight: 500;
min-height: 24px;
}}
QComboBox#cityCombo:hover {{
border-color: {colors['accent']};
}}
QComboBox#cityCombo::drop-down {{
border: none;
width: 15px;
}}
QComboBox#cityCombo::down-arrow {{
image: none;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
border-top: 5px solid {colors['text']};
}}
""")
def on_theme_changed(self, is_dark):
"""主题切换槽函数"""
self.apply_theme()
def mousePressEvent(self, event):
"""鼠标按下事件,用于拖拽"""
if event.button() == Qt.LeftButton:
# 检查是否点击在标题栏区域
if event.pos().y() <= 40: # 假设标题栏高度为40像素
self.is_dragging = True
self.drag_position = event.globalPos() - self.frameGeometry().topLeft()
event.accept()
def mouseMoveEvent(self, event):
"""鼠标移动事件,用于拖拽"""
if self.is_dragging and event.buttons() == Qt.LeftButton:
self.move(event.globalPos() - self.drag_position)
event.accept()
def mouseReleaseEvent(self, event):
"""鼠标释放事件"""
self.is_dragging = False
def update_weather(self, weather_data):
"""更新天气信息"""
self.weather_data = weather_data
if weather_data and 'error' not in weather_data:
# 获取天气数据
city = weather_data.get('city', '未知城市')
current_data = weather_data.get('current', {})
temp = current_data.get('temp', 'N/A')
desc = current_data.get('weather', 'N/A')
humidity = current_data.get('humidity', 'N/A')
wind_scale = current_data.get('wind_scale', 'N/A')
# 更新显示
self.city_label.setText(city)
self.temperature_label.setText(f"{temp}°C")
self.weather_desc_label.setText(desc)
self.humidity_label.setText(f"湿度: {humidity}%")
self.wind_label.setText(f"风速: {wind_scale}")
# 更新天气图标
emoji = self.get_weather_emoji(desc)
self.weather_icon_label.setText(emoji)
else:
# 显示错误信息
self.city_label.setText("获取失败")
self.temperature_label.setText("--°C")
self.weather_desc_label.setText("无法获取天气数据")
self.humidity_label.setText("湿度: --%")
self.wind_label.setText("风速: --级")
self.weather_icon_label.setText("")
def get_weather_emoji(self, weather_desc):
"""根据天气描述返回对应的emoji"""
if not weather_desc:
return "🌞"
weather_desc_lower = weather_desc.lower()
# 天气图标映射
weather_emoji_map = {
'': '🌞',
'多云': '',
'': '☁️',
'': '🌧️',
'小雨': '🌦️',
'中雨': '🌧️',
'大雨': '⛈️',
'暴雨': '🌩️',
'': '❄️',
'小雪': '🌨️',
'中雪': '❄️',
'大雪': '☃️',
'': '🌫️',
'': '😷',
'': '💨',
'大风': '🌪️',
'': '⛈️',
'雷阵雨': '⛈️',
'冰雹': '🌨️',
'沙尘': '🌪️'
}
for key, emoji in weather_emoji_map.items():
if key in weather_desc_lower:
return emoji
# 默认返回晴天图标
return '🌞'
def on_refresh_clicked(self):
"""刷新按钮点击事件"""
self.refresh_requested.emit()
def on_city_changed(self, city_name):
"""城市选择变化事件"""
# 发射城市变化信号,通知主窗口更新天气
if hasattr(self.parent(), 'on_city_changed'):
self.parent().on_city_changed(city_name)
def set_current_city(self, city_name):
"""设置当前城市"""
# 阻止信号发射,避免循环调用
self.city_combo.blockSignals(True)
index = self.city_combo.findText(city_name)
if index >= 0:
self.city_combo.setCurrentIndex(index)
self.city_combo.blockSignals(False)
def close_window(self):
"""关闭窗口 - 只是隐藏而不是销毁"""
try:
self.closed.emit()
self.hide() # 隐藏窗口而不是销毁
except Exception as e:
print(f"Error in close_window: {e}")
def show_detailed_weather(self):
"""显示详细天气信息对话框"""
# 检查是否有天气数据
if not self.weather_data or 'error' in self.weather_data:
from PyQt5.QtWidgets import QMessageBox
QMessageBox.information(self, "天气信息", "暂无天气数据,请先刷新天气信息")
return
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit
weather_data = self.weather_data
# 创建对话框
dialog = QDialog(self)
dialog.setWindowTitle("详细天气")
dialog.setMinimumWidth(400)
layout = QVBoxLayout()
# 城市信息
city_label = QLabel(f"<h2>{weather_data.get('city', '未知城市')}</h2>")
layout.addWidget(city_label)
# 当前天气信息
current_layout = QVBoxLayout()
current_layout.addWidget(QLabel("<b>当前天气:</b>"))
# 获取温度信息,支持嵌套结构
current_data = weather_data.get('current', {})
temp = current_data.get('temp', 'N/A')
if temp != 'N/A' and isinstance(temp, str):
temp = float(temp) if temp.replace('.', '').isdigit() else temp
# 从预报数据中获取最高和最低气温
temp_range = ""
temp_max = 'N/A'
temp_min = 'N/A'
if 'forecast' in weather_data and weather_data['forecast']:
forecast_data = weather_data['forecast'][0] # 今天的预报
if isinstance(forecast_data, dict):
temp_max = forecast_data.get('temp_max', 'N/A')
temp_min = forecast_data.get('temp_min', 'N/A')
if temp_max != 'N/A' and temp_min != 'N/A':
temp_range = f" ({temp_min}°C~{temp_max}°C)"
current_info = f"""
当前温度: {temp}°C{temp_range}
最高气温: {temp_max}°C
最低气温: {temp_min}°C
天气状况: {current_data.get('weather', 'N/A')}
"""
current_text = QTextEdit()
current_text.setPlainText(current_info.strip())
current_text.setReadOnly(True)
current_layout.addWidget(current_text)
layout.addLayout(current_layout)
# 生活提示信息(替换原来的天气预报)
life_tips = weather_data.get('life_tips', [])
if life_tips:
tips_layout = QVBoxLayout()
tips_layout.addWidget(QLabel("<b>生活提示:</b>"))
tips_text = QTextEdit()
tips_info = ""
for tip in life_tips:
tips_info += f"{tip}\n"
tips_text.setPlainText(tips_info.strip())
tips_text.setReadOnly(True)
tips_layout.addWidget(tips_text)
layout.addLayout(tips_layout)
# 按钮
button_layout = QHBoxLayout()
refresh_button = QPushButton("刷新")
refresh_button.clicked.connect(lambda: self.refresh_weather_and_close(dialog))
close_button = QPushButton("关闭")
close_button.clicked.connect(dialog.close)
button_layout.addWidget(refresh_button)
button_layout.addWidget(close_button)
layout.addLayout(button_layout)
dialog.setLayout(layout)
dialog.exec_()
def refresh_weather_and_close(self, dialog):
"""刷新天气并关闭对话框"""
self.on_refresh_clicked()
dialog.close()
def closeEvent(self, event):
"""窗口关闭事件 - 只是隐藏而不是销毁"""
self.closed.emit()
self.hide() # 隐藏窗口而不是销毁
event.ignore()
def show_at_position(self, x, y):
"""在指定位置显示窗口"""
self.move(x, y)
self.show()
def update_position(self, x, y):
"""更新窗口位置"""
self.move(x, y)

@ -698,8 +698,15 @@ class WordRibbon(QFrame):
self.refresh_weather_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }")
self.refresh_weather_btn.setToolTip("刷新天气")
# 悬浮窗口按钮
self.floating_weather_btn = QPushButton("🪟 悬浮")
self.floating_weather_btn.setFixedSize(60, 30)
self.floating_weather_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }")
self.floating_weather_btn.setToolTip("切换天气悬浮窗口")
control_layout.addWidget(self.city_combo)
control_layout.addWidget(self.refresh_weather_btn)
control_layout.addWidget(self.floating_weather_btn)
# 添加右侧弹性空间,确保内容居中
control_layout.addStretch()
@ -852,6 +859,10 @@ class WordRibbon(QFrame):
"""刷新天气按钮点击处理"""
pass
def on_floating_weather(self):
"""悬浮窗口按钮点击处理"""
pass
def on_city_changed(self, city):
"""城市选择变化处理"""
pass

@ -19,6 +19,7 @@ from ui.word_style_ui import WeatherAPI
from file_parser import FileParser
from input_handler.input_processor import InputProcessor
from ui.calendar_widget import CalendarWidget
from ui.weather_floating_widget import WeatherFloatingWidget
# 导入主题管理器
from ui.theme_manager import theme_manager
@ -108,6 +109,12 @@ class WordStyleMainWindow(QMainWindow):
self.calendar_widget = CalendarWidget(self)
self.calendar_widget.hide() # 默认隐藏
# 初始化天气悬浮窗口
self.weather_floating_widget = WeatherFloatingWidget(self)
self.weather_floating_widget.hide() # 默认隐藏
self.weather_floating_widget.closed.connect(self.on_weather_floating_closed)
self.weather_floating_widget.refresh_requested.connect(self.refresh_weather)
# 设置窗口属性
self.setWindowTitle("文档1 - MagicWord")
self.setGeometry(100, 100, 1200, 800)
@ -440,8 +447,14 @@ class WordStyleMainWindow(QMainWindow):
print(f"获取到天气数据: {weather_data}")
# 直接传递原始数据update_weather_display会处理嵌套结构
self.update_weather_display(weather_data)
# 同步更新天气悬浮窗口
if hasattr(self, 'weather_floating_widget') and self.weather_floating_widget.isVisible():
self.weather_floating_widget.update_weather(weather_data)
else:
print(f"无法获取城市 {city} 的天气数据")
# 显示错误信息到天气悬浮窗口
if hasattr(self, 'weather_floating_widget') and self.weather_floating_widget.isVisible():
self.weather_floating_widget.update_weather({'error': '无法获取天气数据'})
def refresh_weather(self):
"""刷新天气"""
@ -692,6 +705,11 @@ class WordStyleMainWindow(QMainWindow):
show_weather_action = QAction('显示详细天气', self)
show_weather_action.triggered.connect(self.show_detailed_weather)
weather_menu.addAction(show_weather_action)
# 天气悬浮窗口
toggle_floating_weather_action = QAction('天气悬浮窗口', self)
toggle_floating_weather_action.triggered.connect(self.toggle_floating_weather)
weather_menu.addAction(toggle_floating_weather_action)
# 插入菜单
insert_menu = menubar.addMenu('插入(I)')
@ -929,6 +947,9 @@ class WordStyleMainWindow(QMainWindow):
if hasattr(self.ribbon, 'refresh_weather_btn'):
self.ribbon.refresh_weather_btn.clicked.connect(self.refresh_weather)
if hasattr(self.ribbon, 'floating_weather_btn'):
self.ribbon.floating_weather_btn.clicked.connect(self.toggle_floating_weather)
# 日历组件信号
if hasattr(self, 'calendar_widget'):
self.calendar_widget.date_selected.connect(self.insert_date_to_editor)
@ -1623,9 +1644,15 @@ class WordStyleMainWindow(QMainWindow):
print(f"refresh_weather - 原始数据包含life_tips: {weather_data.get('life_tips', [])}")
print(f"refresh_weather - formatted_data包含life_tips: {formatted_data.get('life_tips', [])}")
self.update_weather_display(formatted_data)
# 同步更新天气悬浮窗口
if hasattr(self, 'weather_floating_widget') and self.weather_floating_widget.isVisible():
self.weather_floating_widget.update_weather(formatted_data)
self.status_bar.showMessage("天气数据已刷新", 2000)
else:
self.status_bar.showMessage("天气数据刷新失败请检查API密钥", 3000)
# 显示错误信息到天气悬浮窗口
if hasattr(self, 'weather_floating_widget') and self.weather_floating_widget.isVisible():
self.weather_floating_widget.update_weather({'error': '天气数据刷新失败'})
except Exception as e:
self.status_bar.showMessage(f"天气刷新失败: {str(e)}", 3000)
@ -1721,6 +1748,29 @@ class WordStyleMainWindow(QMainWindow):
self.refresh_weather()
dialog.close()
def toggle_floating_weather(self):
"""切换天气悬浮窗口显示/隐藏"""
if hasattr(self, 'weather_floating_widget'):
if self.weather_floating_widget.isVisible():
self.weather_floating_widget.hide()
self.status_bar.showMessage("天气悬浮窗口已隐藏", 2000)
else:
self.weather_floating_widget.show()
# 确保窗口在屏幕内
self.weather_floating_widget.move(100, 100)
self.status_bar.showMessage("天气悬浮窗口已显示", 2000)
# 同步当前城市选择
if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'city_combo'):
current_city = self.ribbon.city_combo.currentText()
self.weather_floating_widget.set_current_city(current_city)
# 如果有天气数据,更新显示
if hasattr(self, 'current_weather_data') and self.current_weather_data:
self.weather_floating_widget.update_weather(self.current_weather_data)
def on_weather_floating_closed(self):
"""天气悬浮窗口关闭时的处理"""
self.status_bar.showMessage("天气悬浮窗口已关闭", 2000)
def toggle_weather_tools(self, checked):
"""切换天气工具组显示"""
if checked:

Loading…
Cancel
Save