diff --git a/src/ui/weather_floating_widget.py b/src/ui/weather_floating_widget.py new file mode 100644 index 0000000..d520962 --- /dev/null +++ b/src/ui/weather_floating_widget.py @@ -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"