From 7a85f3654485936202af6551ad31d7fb75da170e Mon Sep 17 00:00:00 2001
From: Maziang <929110464@qq.com>
Date: Thu, 20 Nov 2025 20:14:09 +0800
Subject: [PATCH 1/8] =?UTF-8?q?=E5=A4=A9=E6=B0=94=E6=82=AC=E6=B5=AE?=
=?UTF-8?q?=E7=AA=97=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/ui/weather_floating_widget.py | 609 ++++++++++++++++++++++++++++++
src/ui/word_style_ui.py | 11 +
src/word_main_window.py | 50 +++
3 files changed, 670 insertions(+)
create mode 100644 src/ui/weather_floating_widget.py
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"
{weather_data.get('city', '未知城市')}
")
+ layout.addWidget(city_label)
+
+ # 当前天气信息
+ current_layout = QVBoxLayout()
+ current_layout.addWidget(QLabel("当前天气:"))
+
+ # 获取温度信息,支持嵌套结构
+ 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("生活提示:"))
+
+ 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)
\ No newline at end of file
diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py
index 562b183..102b28c 100644
--- a/src/ui/word_style_ui.py
+++ b/src/ui/word_style_ui.py
@@ -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
diff --git a/src/word_main_window.py b/src/word_main_window.py
index 9a76eb6..eb7afbb 100644
--- a/src/word_main_window.py
+++ b/src/word_main_window.py
@@ -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:
From efaf1ae33cf438d6adbb1ad4aca8bde7e61cc74a Mon Sep 17 00:00:00 2001
From: Maziang <929110464@qq.com>
Date: Thu, 20 Nov 2025 20:24:04 +0800
Subject: [PATCH 2/8] =?UTF-8?q?=E5=A4=A9=E6=B0=94=E6=82=AC=E6=B5=AE?=
=?UTF-8?q?=E7=AA=97=E5=8F=A3fix?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/ui/weather_floating_widget.py | 130 ++++++++++++++++--------------
1 file changed, 70 insertions(+), 60 deletions(-)
diff --git a/src/ui/weather_floating_widget.py b/src/ui/weather_floating_widget.py
index d520962..047c619 100644
--- a/src/ui/weather_floating_widget.py
+++ b/src/ui/weather_floating_widget.py
@@ -39,7 +39,7 @@ class WeatherFloatingWidget(QDialog):
"""设置UI界面"""
# 设置窗口属性
self.setWindowTitle("天气")
- self.setFixedSize(400, 320) # 增加窗口尺寸
+ self.setFixedSize(360, 280) # 调整窗口尺寸使其更紧凑
# 创建主框架,用于实现圆角和阴影效果
self.main_frame = QFrame()
@@ -52,8 +52,8 @@ class WeatherFloatingWidget(QDialog):
# 内容布局
content_layout = QVBoxLayout(self.main_frame)
- content_layout.setContentsMargins(15, 15, 15, 15) # 减小内边距
- content_layout.setSpacing(10) # 减小间距
+ content_layout.setContentsMargins(10, 10, 10, 10) # 减小内边距使布局更紧凑
+ content_layout.setSpacing(6) # 减小间距使布局更紧凑
# 设置最小尺寸策略
self.main_frame.setMinimumSize(380, 300)
@@ -65,6 +65,8 @@ class WeatherFloatingWidget(QDialog):
self.title_label.setFont(QFont("Arial", 12, QFont.Bold))
title_layout.addWidget(self.title_label)
title_layout.addStretch()
+ # 添加一个小的固定空间,使关闭按钮向左移动
+ title_layout.addSpacing(25) # 向左移动25个单位
# 关闭按钮
self.close_btn = QPushButton("×")
@@ -82,27 +84,27 @@ class WeatherFloatingWidget(QDialog):
# 天气图标和温度显示区域
weather_display_layout = QHBoxLayout()
- weather_display_layout.setSpacing(8) # 适当间距
+ weather_display_layout.setSpacing(5) # 减小间距使布局更紧凑
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.setFont(QFont("Arial", 24)) # 稍微减小字体大小
self.weather_icon_label.setAlignment(Qt.AlignCenter)
- self.weather_icon_label.setFixedSize(60, 60)
+ self.weather_icon_label.setFixedSize(50, 50) # 减小尺寸
weather_display_layout.addWidget(self.weather_icon_label)
# 温度和城市信息
temp_city_layout = QVBoxLayout()
- temp_city_layout.setSpacing(8) # 增加间距
+ temp_city_layout.setSpacing(4) # 减小间距使布局更紧凑
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.setFont(QFont("Arial", 18, 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.setFont(QFont("Arial", 11)) # 稍微减小字体大小
self.city_label.setObjectName("cityLabel")
temp_city_layout.addWidget(self.city_label)
@@ -113,23 +115,23 @@ class WeatherFloatingWidget(QDialog):
# 天气描述
self.weather_desc_label = QLabel("晴天")
- self.weather_desc_label.setFont(QFont("Arial", 12))
+ self.weather_desc_label.setFont(QFont("Arial", 11)) # 稍微减小字体大小
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.setSpacing(6) # 减小间距使布局更紧凑
details_layout.setContentsMargins(2, 2, 2, 2) # 减小内边距
self.humidity_label = QLabel("湿度: 45%")
- self.humidity_label.setFont(QFont("Arial", 11))
+ self.humidity_label.setFont(QFont("Arial", 10)) # 稍微减小字体大小
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.setFont(QFont("Arial", 10)) # 稍微减小字体大小
self.wind_label.setObjectName("detailLabel")
details_layout.addWidget(self.wind_label)
@@ -137,7 +139,7 @@ class WeatherFloatingWidget(QDialog):
# 城市选择区域
city_layout = QHBoxLayout()
- city_layout.setSpacing(10)
+ city_layout.setSpacing(6) # 减小间距使布局更紧凑
city_layout.setContentsMargins(0, 0, 0, 0)
self.city_combo = QComboBox()
@@ -151,7 +153,7 @@ class WeatherFloatingWidget(QDialog):
'合肥', '福州', '南昌', '济南', '郑州', '长沙', '南宁', '海口', # 华东华中华南
'贵阳', '昆明', '拉萨', '兰州', '西宁', '银川', '乌鲁木齐' # 西南西北
])
- self.city_combo.setFixedWidth(120) # 增加城市选择框宽度,与主窗口保持一致
+ self.city_combo.setFixedWidth(100) # 减小城市选择框宽度使布局更紧凑
city_layout.addWidget(self.city_combo)
city_layout.addStretch()
@@ -160,17 +162,19 @@ class WeatherFloatingWidget(QDialog):
# 按钮区域
button_layout = QHBoxLayout()
- button_layout.setSpacing(10)
+ button_layout.setSpacing(6) # 减小间距使布局更紧凑
button_layout.setContentsMargins(0, 0, 0, 0)
self.refresh_btn = QPushButton("刷新")
self.refresh_btn.setObjectName("refreshButton")
+ self.refresh_btn.setFixedHeight(26) # 减小按钮高度
button_layout.addWidget(self.refresh_btn)
button_layout.addStretch()
self.detail_btn = QPushButton("详情")
self.detail_btn.setObjectName("detailButton")
+ self.detail_btn.setFixedHeight(26) # 减小按钮高度
button_layout.addWidget(self.detail_btn)
content_layout.addLayout(button_layout)
@@ -209,56 +213,59 @@ class WeatherFloatingWidget(QDialog):
QLabel {{
color: {colors['text']};
background-color: transparent;
- padding: 4px 6px;
- margin: 2px;
+ padding: 3px 5px; // 适度增加padding使布局更舒适
+ margin: 1px;
}}
QLabel#temperatureLabel {{
color: {colors['accent']};
- font-size: 20px;
+ font-size: 19px; // 适度增大字体大小
font-weight: bold;
- padding: 6px 8px;
- margin: 3px;
+ padding: 5px 7px; // 适度增加padding使布局更舒适
+ margin: 2px;
}}
QLabel#cityLabel {{
color: {colors['text_secondary']};
- font-size: 12px;
- padding: 3px 5px;
- margin: 2px;
+ font-size: 12px; // 适度增大字体大小
+ padding: 3px 5px; // 适度增加padding使布局更舒适
+ margin: 1px;
}}
QLabel#weatherDescLabel {{
color: {colors['text']};
- font-size: 12px;
+ font-size: 12px; // 适度增大字体大小
font-weight: 500;
- padding: 4px 6px;
- margin: 2px;
+ padding: 3px 5px; // 适度增加padding使布局更舒适
+ margin: 1px;
}}
QLabel#detailLabel {{
color: {colors['text_secondary']};
- font-size: 11px;
- padding: 3px 5px;
- margin: 2px;
+ font-size: 11px; // 适度增大字体大小
+ padding: 3px 5px; // 适度增加padding使布局更舒适
+ margin: 1px;
}}
QFrame#separator {{
background-color: {colors['border']};
}}
QPushButton#closeButton {{
- background-color: transparent;
+ background-color: rgba(255, 255, 255, 0.1);
border: none;
color: {colors['text']};
+ font-size: 18px;
font-weight: bold;
+ border-radius: 6px;
+ padding: 2px 4px;
}}
QPushButton#closeButton:hover {{
background-color: #e81123;
color: white;
- border-radius: 3px;
+ border-radius: 6px;
}}
QPushButton#refreshButton, QPushButton#detailButton {{
background-color: {colors['accent']};
color: white;
border: none;
- border-radius: 6px;
- padding: 6px 16px;
- font-size: 11px;
+ border-radius: 6px; // 适度增加圆角
+ padding: 5px 14px; // 适度增加padding使按钮更舒适
+ font-size: 11px; // 适度增大字体大小
font-weight: 500;
}}
QPushButton#refreshButton:hover, QPushButton#detailButton:hover {{
@@ -268,24 +275,24 @@ class WeatherFloatingWidget(QDialog):
background-color: {colors['surface']};
color: {colors['text']};
border: 1px solid {colors['border']};
- border-radius: 6px;
- padding: 4px 8px;
- font-size: 11px;
+ border-radius: 6px; // 适度增加圆角
+ padding: 4px 7px; // 适度增加padding使布局更舒适
+ font-size: 11px; // 适度增大字体大小
font-weight: 500;
- min-height: 24px;
+ min-height: 24px; // 适度增加最小高度使布局更舒适
}}
QComboBox#cityCombo:hover {{
border-color: {colors['accent']};
}}
QComboBox#cityCombo::drop-down {{
border: none;
- width: 15px;
+ width: 14px; // 适度增加宽度
}}
QComboBox#cityCombo::down-arrow {{
image: none;
- border-left: 3px solid transparent;
- border-right: 3px solid transparent;
- border-top: 5px solid {colors['text']};
+ border-left: 2px solid transparent;
+ border-right: 2px solid transparent;
+ border-top: 5px solid {colors['text']}; // 适度增加箭头大小
}}
""")
else:
@@ -294,8 +301,8 @@ class WeatherFloatingWidget(QDialog):
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);
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}}
QLabel {{
color: {colors['text']};
@@ -313,7 +320,7 @@ class WeatherFloatingWidget(QDialog):
QLabel#cityLabel {{
color: {colors['text_secondary']};
font-size: 12px;
- padding: 3px 5px;
+ padding: 4px 6px;
margin: 2px;
}}
QLabel#weatherDescLabel {{
@@ -326,30 +333,33 @@ class WeatherFloatingWidget(QDialog):
QLabel#detailLabel {{
color: {colors['text_secondary']};
font-size: 11px;
- padding: 3px 5px;
+ padding: 4px 6px;
margin: 2px;
}}
QFrame#separator {{
background-color: {colors['border']};
}}
QPushButton#closeButton {{
- background-color: transparent;
+ background-color: rgba(0, 0, 0, 0.05);
border: none;
color: {colors['text']};
+ font-size: 18px;
font-weight: bold;
+ border-radius: 6px;
+ padding: 2px 4px;
}}
QPushButton#closeButton:hover {{
background-color: #e81123;
color: white;
- border-radius: 3px;
+ border-radius: 6px;
}}
QPushButton#refreshButton, QPushButton#detailButton {{
background-color: {colors['accent']};
color: white;
border: none;
- border-radius: 6px;
- padding: 6px 16px;
- font-size: 11px;
+ border-radius: 6px; // 适度增加圆角
+ padding: 5px 14px; // 适度增加padding使按钮更舒适
+ font-size: 11px; // 适度增大字体大小
font-weight: 500;
}}
QPushButton#refreshButton:hover, QPushButton#detailButton:hover {{
@@ -359,24 +369,24 @@ class WeatherFloatingWidget(QDialog):
background-color: {colors['surface']};
color: {colors['text']};
border: 1px solid {colors['border']};
- border-radius: 6px;
- padding: 4px 8px;
- font-size: 11px;
+ border-radius: 6px; // 适度增加圆角
+ padding: 4px 7px; // 适度增加padding使布局更舒适
+ font-size: 11px; // 适度增大字体大小
font-weight: 500;
- min-height: 24px;
+ min-height: 24px; // 适度增加最小高度使布局更舒适
}}
QComboBox#cityCombo:hover {{
border-color: {colors['accent']};
}}
QComboBox#cityCombo::drop-down {{
border: none;
- width: 15px;
+ width: 14px; // 适度增加宽度
}}
QComboBox#cityCombo::down-arrow {{
image: none;
- border-left: 3px solid transparent;
- border-right: 3px solid transparent;
- border-top: 5px solid {colors['text']};
+ border-left: 2px solid transparent;
+ border-right: 2px solid transparent;
+ border-top: 5px solid {colors['text']}; // 适度增加箭头大小
}}
""")
From bfdb9d124c1a7de1ee102023e18986d2a34b9bf8 Mon Sep 17 00:00:00 2001
From: Maziang <929110464@qq.com>
Date: Thu, 20 Nov 2025 20:36:08 +0800
Subject: [PATCH 3/8] =?UTF-8?q?=E5=8F=A4=E8=AF=97=E5=8F=A5=E6=82=AC?=
=?UTF-8?q?=E6=B5=AE=E7=AA=97=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/ui/quote_floating_widget.py | 415 ++++++++++++++++++++++++++++++++
src/ui/word_style_ui.py | 8 +
src/word_main_window.py | 37 ++-
3 files changed, 459 insertions(+), 1 deletion(-)
create mode 100644 src/ui/quote_floating_widget.py
diff --git a/src/ui/quote_floating_widget.py b/src/ui/quote_floating_widget.py
new file mode 100644
index 0000000..ae740b7
--- /dev/null
+++ b/src/ui/quote_floating_widget.py
@@ -0,0 +1,415 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""
+每日谏言悬浮窗口
+"""
+
+import sys
+from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
+ QLabel, QPushButton, QFrame, QGraphicsDropShadowEffect)
+from PyQt5.QtCore import Qt, QPoint, pyqtSignal
+from PyQt5.QtGui import QFont, QColor
+
+
+class QuoteFloatingWidget(QWidget):
+ """每日谏言悬浮窗口"""
+
+ # 定义信号
+ closed = pyqtSignal() # 窗口关闭信号
+ refresh_requested = pyqtSignal() # 刷新请求信号
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
+ self.setAttribute(Qt.WA_TranslucentBackground)
+
+ # 初始化变量
+ self.is_dragging = False
+ self.drag_position = QPoint()
+
+ # 设置默认谏言数据
+ self.quote_data = {
+ "quote": "书山有路勤为径,学海无涯苦作舟。",
+ "author": "韩愈",
+ "source": "《古今贤文·劝学篇》"
+ }
+
+ # 初始化UI
+ self.init_ui()
+ self.setup_styles()
+ self.apply_theme(is_dark=True) # 默认使用深色主题
+
+ def init_ui(self):
+ """初始化UI"""
+ # 主框架
+ self.main_frame = QFrame(self)
+ self.main_frame.setObjectName("mainFrame")
+ self.main_frame.setFixedSize(360, 200) # 设置窗口大小
+
+ main_layout = QVBoxLayout(self.main_frame)
+ main_layout.setContentsMargins(12, 12, 12, 12)
+ main_layout.setSpacing(8)
+
+ # 标题栏
+ title_layout = QHBoxLayout()
+ title_layout.setContentsMargins(0, 0, 0, 0)
+ title_layout.setSpacing(0)
+
+ self.title_label = QLabel("每日谏言")
+ self.title_label.setFont(QFont("Arial", 12, QFont.Bold))
+ title_layout.addWidget(self.title_label)
+ title_layout.addStretch()
+ # 添加一个小的固定空间,使关闭按钮向左移动
+ title_layout.addSpacing(25) # 向左移动25个单位
+
+ # 关闭按钮
+ self.close_btn = QPushButton("×")
+ self.close_btn.setFixedSize(20, 20)
+ self.close_btn.setObjectName("closeButton")
+ self.close_btn.clicked.connect(self.close_window)
+ title_layout.addWidget(self.close_btn)
+
+ main_layout.addLayout(title_layout)
+
+ # 分隔线
+ separator = QFrame()
+ separator.setObjectName("separator")
+ separator.setFixedHeight(1)
+ main_layout.addWidget(separator)
+
+ # 谏言内容区域
+ content_layout = QVBoxLayout()
+ content_layout.setContentsMargins(0, 0, 0, 0)
+ content_layout.setSpacing(6)
+
+ # 谏言文本
+ self.quote_label = QLabel()
+ self.quote_label.setObjectName("quoteLabel")
+ self.quote_label.setWordWrap(True)
+ self.quote_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
+ content_layout.addWidget(self.quote_label)
+
+ # 作者信息
+ self.author_label = QLabel()
+ self.author_label.setObjectName("authorLabel")
+ self.author_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
+ content_layout.addWidget(self.author_label)
+
+ main_layout.addLayout(content_layout)
+ main_layout.addStretch()
+
+ # 底部按钮区域
+ bottom_layout = QHBoxLayout()
+ bottom_layout.setContentsMargins(0, 0, 0, 0)
+ bottom_layout.setSpacing(8)
+
+ # 刷新按钮
+ self.refresh_btn = QPushButton("换一句")
+ self.refresh_btn.setObjectName("refreshButton")
+ self.refresh_btn.clicked.connect(self.on_refresh_clicked)
+ bottom_layout.addWidget(self.refresh_btn)
+
+ bottom_layout.addStretch()
+ main_layout.addLayout(bottom_layout)
+
+ # 设置主布局
+ outer_layout = QVBoxLayout(self)
+ outer_layout.setContentsMargins(0, 0, 0, 0)
+ outer_layout.addWidget(self.main_frame)
+
+ # 更新显示
+ self.update_quote()
+
+ def setup_styles(self):
+ """设置样式"""
+ pass # 样式将在apply_theme中设置
+
+ def apply_theme(self, is_dark=True):
+ """应用主题"""
+ if is_dark:
+ # 深色主题配色
+ colors = {
+ 'surface': '#2d2d2d',
+ 'border': '#444444',
+ 'text': '#ffffff',
+ 'text_secondary': '#cccccc',
+ 'accent': '#4CAF50',
+ 'accent_hover': '#45a049',
+ 'button_hover': '#555555',
+ 'error': '#f44336'
+ }
+
+ self.main_frame.setStyleSheet(f"""
+ QFrame#mainFrame {{
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ }}
+ QLabel {{
+ color: {colors['text']};
+ background-color: transparent;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QLabel#quoteLabel {{
+ color: {colors['text']};
+ font-size: 14px;
+ font-weight: 500;
+ padding: 6px 8px;
+ margin: 3px;
+ }}
+ QLabel#authorLabel {{
+ color: {colors['text_secondary']};
+ font-size: 12px;
+ font-style: italic;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QFrame#separator {{
+ background-color: {colors['border']};
+ }}
+ QPushButton#closeButton {{
+ background-color: rgba(255, 255, 255, 0.1);
+ border: none;
+ color: {colors['text']};
+ font-size: 18px;
+ font-weight: bold;
+ border-radius: 6px;
+ padding: 2px 4px;
+ }}
+ QPushButton#closeButton:hover {{
+ background-color: #e81123;
+ color: white;
+ border-radius: 6px;
+ }}
+ QPushButton#refreshButton {{
+ background-color: {colors['accent']};
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 6px 16px;
+ font-size: 11px;
+ font-weight: 500;
+ }}
+ QPushButton#refreshButton:hover {{
+ background-color: {colors['accent_hover']};
+ }}
+ """)
+ else:
+ # 浅色主题配色
+ colors = {
+ 'surface': '#ffffff',
+ 'border': '#dddddd',
+ 'text': '#333333',
+ 'text_secondary': '#666666',
+ 'accent': '#4CAF50',
+ 'accent_hover': '#45a049',
+ 'button_hover': '#f0f0f0',
+ 'error': '#f44336'
+ }
+
+ self.main_frame.setStyleSheet(f"""
+ QFrame#mainFrame {{
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ }}
+ QLabel {{
+ color: {colors['text']};
+ background-color: transparent;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QLabel#quoteLabel {{
+ color: {colors['text']};
+ font-size: 14px;
+ font-weight: 500;
+ padding: 6px 8px;
+ margin: 3px;
+ }}
+ QLabel#authorLabel {{
+ color: {colors['text_secondary']};
+ font-size: 12px;
+ font-style: italic;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QFrame#separator {{
+ background-color: {colors['border']};
+ }}
+ QPushButton#closeButton {{
+ background-color: rgba(0, 0, 0, 0.05);
+ border: none;
+ color: {colors['text']};
+ font-size: 18px;
+ font-weight: bold;
+ border-radius: 6px;
+ padding: 2px 4px;
+ }}
+ QPushButton#closeButton:hover {{
+ background-color: #e81123;
+ color: white;
+ border-radius: 6px;
+ }}
+ QPushButton#refreshButton {{
+ background-color: {colors['accent']};
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 6px 16px;
+ font-size: 11px;
+ font-weight: 500;
+ }}
+ QPushButton#refreshButton:hover {{
+ background-color: {colors['accent_hover']};
+ }}
+ """)
+
+ def update_quote(self, quote_data=None):
+ """更新谏言显示"""
+ if quote_data:
+ self.quote_data = quote_data
+ else:
+ # 如果没有提供数据,使用默认数据
+ if not hasattr(self, 'quote_data'):
+ self.quote_data = {
+ "quote": "书山有路勤为径,学海无涯苦作舟。",
+ "author": "韩愈",
+ "source": "《古今贤文·劝学篇》"
+ }
+
+ # 更新显示
+ self.quote_label.setText(self.quote_data["quote"])
+ author_info = f"— {self.quote_data['author']}"
+ if self.quote_data.get("source"):
+ author_info += f" 《{self.quote_data['source']}》"
+ self.author_label.setText(author_info)
+
+ def on_refresh_clicked(self):
+ """刷新按钮点击事件"""
+ # 发送刷新请求信号
+ self.refresh_requested.emit()
+ # 同时直接获取新的内容并更新显示
+ self.fetch_and_update_quote()
+
+ def fetch_and_update_quote(self):
+ """获取新的谏言内容并更新显示"""
+ try:
+ # 尝试获取古诗词
+ import requests
+ import random
+
+ try:
+ # 使用古诗词·一言API
+ response = requests.get("https://v1.jinrishici.com/all.json", timeout=5, verify=False)
+ if response.status_code == 200:
+ data = response.json()
+ content = data.get('content', '')
+ author = data.get('author', '佚名')
+ origin = data.get('origin', '')
+
+ if content:
+ quote_data = {
+ "quote": content,
+ "author": author,
+ "source": origin
+ }
+ self.update_quote(quote_data)
+ return
+ except Exception as e:
+ print(f"获取古诗词失败: {e}")
+
+ # 如果古诗词获取失败,使用备用API
+ try:
+ # 使用每日一言API
+ response = requests.get("https://api.nxvav.cn/api/yiyan?json=true", timeout=5, verify=False)
+ if response.status_code == 200:
+ data = response.json()
+ yiyan = data.get('yiyan', '')
+ nick = data.get('nick', '佚名')
+
+ if yiyan:
+ quote_data = {
+ "quote": yiyan,
+ "author": nick,
+ "source": ""
+ }
+ self.update_quote(quote_data)
+ return
+ except Exception as e:
+ print(f"获取每日一言失败: {e}")
+
+ # 如果API都失败,使用预设内容
+ quotes = [
+ {"quote": "学而时习之,不亦说乎?", "author": "孔子", "source": "《论语》"},
+ {"quote": "千里之行,始于足下。", "author": "老子", "source": "《道德经》"},
+ {"quote": "天行健,君子以自强不息。", "author": "佚名", "source": "《周易》"},
+ {"quote": "书山有路勤为径,学海无涯苦作舟。", "author": "韩愈", "source": "《古今贤文·劝学篇》"},
+ {"quote": "山重水复疑无路,柳暗花明又一村。", "author": "陆游", "source": "《游山西村》"}
+ ]
+
+ # 随机选择一个名言
+ new_quote = random.choice(quotes)
+ self.update_quote(new_quote)
+
+ except Exception as e:
+ print(f"获取新谏言失败: {e}")
+ # 出错时显示默认内容
+ default_quote = {
+ "quote": "书山有路勤为径,学海无涯苦作舟。",
+ "author": "韩愈",
+ "source": "《古今贤文·劝学篇》"
+ }
+ self.update_quote(default_quote)
+
+ def close_window(self):
+ """关闭窗口 - 只是隐藏而不是销毁"""
+ try:
+ self.closed.emit()
+ self.hide() # 隐藏窗口而不是销毁
+ except Exception as e:
+ print(f"Error in close_window: {e}")
+
+ 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 main():
+ """测试函数"""
+ app = QApplication(sys.argv)
+
+ # 创建并显示窗口
+ widget = QuoteFloatingWidget()
+ widget.show()
+
+ # 移动到屏幕中心
+ screen_geometry = app.desktop().screenGeometry()
+ widget.move(
+ (screen_geometry.width() - widget.width()) // 2,
+ (screen_geometry.height() - widget.height()) // 2
+ )
+
+ sys.exit(app.exec_())
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py
index 102b28c..07ad4ff 100644
--- a/src/ui/word_style_ui.py
+++ b/src/ui/word_style_ui.py
@@ -34,6 +34,7 @@ class WordRibbon(QFrame):
self.weather_group = None # 天气组件组
self.quote_visible = False # 每日一言组件显示状态
self.quote_group = None # 每日一言组件组
+ self.current_quote_type = "普通箴言" # 每日一言类型
self.ribbon_layout = None # 功能区布局
self.setup_ui()
@@ -704,9 +705,16 @@ class WordRibbon(QFrame):
self.floating_weather_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }")
self.floating_weather_btn.setToolTip("切换天气悬浮窗口")
+ # 每日谏言悬浮窗口按钮
+ self.floating_quote_btn = QPushButton("📜 悬浮")
+ self.floating_quote_btn.setFixedSize(60, 30)
+ self.floating_quote_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }")
+ self.floating_quote_btn.setToolTip("切换每日谏言悬浮窗口")
+
control_layout.addWidget(self.city_combo)
control_layout.addWidget(self.refresh_weather_btn)
control_layout.addWidget(self.floating_weather_btn)
+ control_layout.addWidget(self.floating_quote_btn)
# 添加右侧弹性空间,确保内容居中
control_layout.addStretch()
diff --git a/src/word_main_window.py b/src/word_main_window.py
index eb7afbb..8d8929a 100644
--- a/src/word_main_window.py
+++ b/src/word_main_window.py
@@ -20,6 +20,7 @@ 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.quote_floating_widget import QuoteFloatingWidget
# 导入主题管理器
from ui.theme_manager import theme_manager
@@ -115,6 +116,12 @@ class WordStyleMainWindow(QMainWindow):
self.weather_floating_widget.closed.connect(self.on_weather_floating_closed)
self.weather_floating_widget.refresh_requested.connect(self.refresh_weather)
+ # 初始化每日谏言悬浮窗口
+ self.quote_floating_widget = QuoteFloatingWidget(self)
+ self.quote_floating_widget.hide() # 默认隐藏
+ self.quote_floating_widget.closed.connect(self.on_quote_floating_closed)
+ self.quote_floating_widget.refresh_requested.connect(self.refresh_daily_quote)
+
# 设置窗口属性
self.setWindowTitle("文档1 - MagicWord")
self.setGeometry(100, 100, 1200, 800)
@@ -710,6 +717,11 @@ class WordStyleMainWindow(QMainWindow):
toggle_floating_weather_action = QAction('天气悬浮窗口', self)
toggle_floating_weather_action.triggered.connect(self.toggle_floating_weather)
weather_menu.addAction(toggle_floating_weather_action)
+
+ # 每日谏言悬浮窗口切换动作
+ toggle_floating_quote_action = QAction('每日谏言悬浮窗口', self)
+ toggle_floating_quote_action.triggered.connect(self.toggle_floating_quote)
+ weather_menu.addAction(toggle_floating_quote_action)
# 插入菜单
insert_menu = menubar.addMenu('插入(I)')
@@ -946,9 +958,10 @@ class WordStyleMainWindow(QMainWindow):
self.ribbon.city_combo.currentTextChanged.connect(self.on_city_changed)
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.ribbon, 'floating_quote_btn'):
+ self.ribbon.floating_quote_btn.clicked.connect(self.toggle_floating_quote)
# 日历组件信号
if hasattr(self, 'calendar_widget'):
@@ -1771,6 +1784,22 @@ class WordStyleMainWindow(QMainWindow):
"""天气悬浮窗口关闭时的处理"""
self.status_bar.showMessage("天气悬浮窗口已关闭", 2000)
+ def toggle_floating_quote(self):
+ """切换每日谏言悬浮窗口显示/隐藏"""
+ if hasattr(self, 'quote_floating_widget'):
+ if self.quote_floating_widget.isVisible():
+ self.quote_floating_widget.hide()
+ self.status_bar.showMessage("每日谏言悬浮窗口已隐藏", 2000)
+ else:
+ self.quote_floating_widget.show()
+ # 确保窗口在屏幕内
+ self.quote_floating_widget.move(100, 100)
+ self.status_bar.showMessage("每日谏言悬浮窗口已显示", 2000)
+
+ def on_quote_floating_closed(self):
+ """每日谏言悬浮窗口关闭时的处理"""
+ self.status_bar.showMessage("每日谏言悬浮窗口已关闭", 2000)
+
def toggle_weather_tools(self, checked):
"""切换天气工具组显示"""
if checked:
@@ -1805,6 +1834,12 @@ class WordStyleMainWindow(QMainWindow):
if hasattr(self, 'ribbon'):
# 直接调用WordRibbon中的刷新方法
self.ribbon.on_refresh_quote()
+
+ # 同时更新浮动窗口中的内容(如果浮动窗口存在且可见)
+ if hasattr(self, 'quote_floating_widget') and self.quote_floating_widget.isVisible():
+ # 调用浮动窗口的获取新内容方法
+ if hasattr(self.quote_floating_widget, 'fetch_and_update_quote'):
+ self.quote_floating_widget.fetch_and_update_quote()
def on_quote_fetched(self, quote_data):
"""处理名言获取成功"""
From 07ac34ee4685f97a4e35ae3fe587eb28b324d40e Mon Sep 17 00:00:00 2001
From: Maziang <929110464@qq.com>
Date: Thu, 20 Nov 2025 20:51:48 +0800
Subject: [PATCH 4/8] =?UTF-8?q?=E5=8F=A4=E8=AF=97=E5=8F=A5=E6=8F=92?=
=?UTF-8?q?=E5=85=A5=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/ui/quote_floating_widget.py | 32 ++++++++++++++++++++++++++++----
src/word_main_window.py | 15 +++++++++++++++
2 files changed, 43 insertions(+), 4 deletions(-)
diff --git a/src/ui/quote_floating_widget.py b/src/ui/quote_floating_widget.py
index ae740b7..ad37bfe 100644
--- a/src/ui/quote_floating_widget.py
+++ b/src/ui/quote_floating_widget.py
@@ -18,6 +18,7 @@ class QuoteFloatingWidget(QWidget):
# 定义信号
closed = pyqtSignal() # 窗口关闭信号
refresh_requested = pyqtSignal() # 刷新请求信号
+ insert_requested = pyqtSignal(str) # 插入请求信号,传递要插入的文本
def __init__(self, parent=None):
super().__init__(parent)
@@ -111,6 +112,13 @@ class QuoteFloatingWidget(QWidget):
bottom_layout.addWidget(self.refresh_btn)
bottom_layout.addStretch()
+
+ # 插入按钮
+ self.insert_btn = QPushButton("插入")
+ self.insert_btn.setObjectName("insertButton")
+ self.insert_btn.clicked.connect(self.on_insert_clicked)
+ bottom_layout.addWidget(self.insert_btn)
+
main_layout.addLayout(bottom_layout)
# 设置主布局
@@ -184,7 +192,7 @@ class QuoteFloatingWidget(QWidget):
color: white;
border-radius: 6px;
}}
- QPushButton#refreshButton {{
+ QPushButton#refreshButton, QPushButton#insertButton {{
background-color: {colors['accent']};
color: white;
border: none;
@@ -193,7 +201,7 @@ class QuoteFloatingWidget(QWidget):
font-size: 11px;
font-weight: 500;
}}
- QPushButton#refreshButton:hover {{
+ QPushButton#refreshButton:hover, QPushButton#insertButton:hover {{
background-color: {colors['accent_hover']};
}}
""")
@@ -254,7 +262,7 @@ class QuoteFloatingWidget(QWidget):
color: white;
border-radius: 6px;
}}
- QPushButton#refreshButton {{
+ QPushButton#refreshButton, QPushButton#insertButton {{
background-color: {colors['accent']};
color: white;
border: none;
@@ -263,7 +271,7 @@ class QuoteFloatingWidget(QWidget):
font-size: 11px;
font-weight: 500;
}}
- QPushButton#refreshButton:hover {{
+ QPushButton#refreshButton:hover, QPushButton#insertButton:hover {{
background-color: {colors['accent_hover']};
}}
""")
@@ -295,6 +303,22 @@ class QuoteFloatingWidget(QWidget):
# 同时直接获取新的内容并更新显示
self.fetch_and_update_quote()
+ def on_insert_clicked(self):
+ """插入按钮点击事件"""
+ # 发送插入请求信号,传递完整的诗句信息
+ quote = self.quote_data.get("quote", "")
+ author = self.quote_data.get("author", "佚名")
+ source = self.quote_data.get("source", "")
+
+ # 构造完整的诗句文本
+ if source:
+ full_quote_text = f"{quote} —— {author}《{source}》"
+ else:
+ full_quote_text = f"{quote} —— {author}"
+
+ if quote:
+ self.insert_requested.emit(full_quote_text)
+
def fetch_and_update_quote(self):
"""获取新的谏言内容并更新显示"""
try:
diff --git a/src/word_main_window.py b/src/word_main_window.py
index 8d8929a..4f4851d 100644
--- a/src/word_main_window.py
+++ b/src/word_main_window.py
@@ -121,6 +121,7 @@ class WordStyleMainWindow(QMainWindow):
self.quote_floating_widget.hide() # 默认隐藏
self.quote_floating_widget.closed.connect(self.on_quote_floating_closed)
self.quote_floating_widget.refresh_requested.connect(self.refresh_daily_quote)
+ self.quote_floating_widget.insert_requested.connect(self.insert_quote_to_cursor)
# 设置窗口属性
self.setWindowTitle("文档1 - MagicWord")
@@ -1800,6 +1801,20 @@ class WordStyleMainWindow(QMainWindow):
"""每日谏言悬浮窗口关闭时的处理"""
self.status_bar.showMessage("每日谏言悬浮窗口已关闭", 2000)
+ def insert_quote_to_cursor(self, quote_text):
+ """将古诗句插入到光标位置"""
+ if hasattr(self, 'text_edit'):
+ # 获取当前光标位置
+ cursor = self.text_edit.textCursor()
+
+ # 在光标位置插入文本
+ cursor.insertText(quote_text + "\n")
+
+ # 更新状态栏提示
+ # 从文本中提取诗句部分用于显示
+ quote_only = quote_text.split(" —— ")[0] if " —— " in quote_text else quote_text
+ self.status_bar.showMessage(f"已插入古诗句: {quote_only}", 3000)
+
def toggle_weather_tools(self, checked):
"""切换天气工具组显示"""
if checked:
From 2415e097486ee35fbf746dfc2a91af909752ff04 Mon Sep 17 00:00:00 2001
From: Maziang <929110464@qq.com>
Date: Thu, 20 Nov 2025 20:58:53 +0800
Subject: [PATCH 5/8] =?UTF-8?q?=E6=97=A5=E5=8E=86=E6=82=AC=E6=B5=AE?=
=?UTF-8?q?=E7=AA=97=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/ui/calendar_floating_widget.py | 482 +++++++++++++++++++++++++++++
src/ui/word_style_ui.py | 7 +
src/word_main_window.py | 48 ++-
3 files changed, 536 insertions(+), 1 deletion(-)
create mode 100644 src/ui/calendar_floating_widget.py
diff --git a/src/ui/calendar_floating_widget.py b/src/ui/calendar_floating_widget.py
new file mode 100644
index 0000000..f8ee8ca
--- /dev/null
+++ b/src/ui/calendar_floating_widget.py
@@ -0,0 +1,482 @@
+# -*- coding: utf-8 -*-
+
+"""
+日历悬浮窗口模块
+提供一个可拖拽的日历悬浮窗口,用于在应用程序中显示和选择日期
+"""
+
+import sys
+from PyQt5.QtWidgets import (
+ QWidget, QCalendarWidget, QVBoxLayout, QHBoxLayout,
+ QPushButton, QLabel, QFrame
+)
+from PyQt5.QtCore import QDate, Qt, pyqtSignal, QPoint
+from PyQt5.QtGui import QFont
+
+# 导入主题管理器
+from .theme_manager import theme_manager
+
+
+class CalendarFloatingWidget(QWidget):
+ """日历悬浮窗口类"""
+
+ # 自定义信号
+ closed = pyqtSignal() # 窗口关闭信号
+ date_selected = pyqtSignal(str) # 日期字符串信号,用于插入功能
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.drag_position = None
+ self.is_dragging = False
+ 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(360, 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(10, 10, 10, 10)
+ content_layout.setSpacing(8)
+
+ # 标题栏
+ title_layout = QHBoxLayout()
+ title_layout.setContentsMargins(0, 0, 0, 0)
+ title_layout.setSpacing(0)
+
+ self.title_label = QLabel("日历")
+ self.title_label.setFont(QFont("Arial", 12, QFont.Bold))
+ title_layout.addWidget(self.title_label)
+ title_layout.addStretch()
+ # 添加一个小的固定空间,使关闭按钮向左移动
+ title_layout.addSpacing(25) # 向左移动25个单位
+
+ # 关闭按钮
+ 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.setObjectName("separator")
+ separator.setFixedHeight(1)
+ content_layout.addWidget(separator)
+
+ # 日历控件
+ self.calendar = QCalendarWidget()
+ self.calendar.setGridVisible(True)
+ self.calendar.setVerticalHeaderFormat(QCalendarWidget.NoVerticalHeader)
+ self.calendar.setNavigationBarVisible(True)
+ content_layout.addWidget(self.calendar)
+
+ # 当前日期显示
+ self.date_label = QLabel()
+ self.date_label.setAlignment(Qt.AlignCenter)
+ self.date_label.setFont(QFont("Arial", 10))
+ self.date_label.setObjectName("dateLabel")
+ self.update_date_label()
+ content_layout.addWidget(self.date_label)
+
+ # 操作按钮
+ button_layout = QHBoxLayout()
+ button_layout.setContentsMargins(0, 0, 0, 0)
+ button_layout.setSpacing(6)
+
+ self.today_btn = QPushButton("今天")
+ self.today_btn.setObjectName("todayButton")
+ button_layout.addWidget(self.today_btn)
+
+ self.insert_btn = QPushButton("插入")
+ self.insert_btn.setObjectName("insertButton")
+ button_layout.addWidget(self.insert_btn)
+
+ button_layout.addStretch()
+
+ self.clear_btn = QPushButton("清除")
+ self.clear_btn.setObjectName("clearButton")
+ button_layout.addWidget(self.clear_btn)
+
+ content_layout.addLayout(button_layout)
+
+ def setup_connections(self):
+ """设置信号连接"""
+ self.calendar.clicked.connect(self.on_date_selected)
+ self.today_btn.clicked.connect(self.on_today_clicked)
+ self.clear_btn.clicked.connect(self.on_clear_clicked)
+ self.close_btn.clicked.connect(self.close_window)
+ self.insert_btn.clicked.connect(self.on_insert_clicked)
+
+ 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: 10px;
+ }}
+ QLabel {{
+ color: {colors['text']};
+ background-color: transparent;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QLabel#dateLabel {{
+ color: {colors['text_secondary']};
+ font-size: 11px;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QFrame#separator {{
+ background-color: {colors['border']};
+ }}
+ QPushButton#closeButton {{
+ background-color: rgba(255, 255, 255, 0.1);
+ border: none;
+ color: {colors['text']};
+ font-size: 18px;
+ font-weight: bold;
+ border-radius: 6px;
+ padding: 2px 4px;
+ }}
+ QPushButton#closeButton:hover {{
+ background-color: #e81123;
+ color: white;
+ border-radius: 6px;
+ }}
+ QPushButton#todayButton, QPushButton#clearButton, QPushButton#insertButton {{
+ background-color: {colors['accent']};
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 6px 16px;
+ font-size: 11px;
+ font-weight: 500;
+ }}
+ QPushButton#todayButton:hover, QPushButton#clearButton:hover, QPushButton#insertButton:hover {{
+ background-color: {colors['accent_hover']};
+ }}
+ """)
+
+ # 更新日历控件样式
+ self.calendar.setStyleSheet(f"""
+ QCalendarWidget {{
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 4px;
+ }}
+ QCalendarWidget QToolButton {{
+ height: 30px;
+ width: 80px;
+ color: {colors['text']};
+ font-size: 12px;
+ font-weight: bold;
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 4px;
+ }}
+ QCalendarWidget QToolButton:hover {{
+ background-color: {colors['surface_hover']};
+ }}
+ QCalendarWidget QMenu {{
+ width: 150px;
+ left: 20px;
+ color: {colors['text']};
+ font-size: 12px;
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ }}
+ QCalendarWidget QSpinBox {{
+ width: 80px;
+ font-size: 12px;
+ background-color: {colors['surface']};
+ selection-background-color: {colors['accent']};
+ selection-color: white;
+ border: 1px solid {colors['border']};
+ border-radius: 4px;
+ color: {colors['text']};
+ }}
+ QCalendarWidget QSpinBox::up-button {{
+ subcontrol-origin: border;
+ subcontrol-position: top right;
+ width: 20px;
+ }}
+ QCalendarWidget QSpinBox::down-button {{
+ subcontrol-origin: border;
+ subcontrol-position: bottom right;
+ width: 20px;
+ }}
+ QCalendarWidget QSpinBox::up-arrow {{
+ width: 10px;
+ height: 10px;
+ }}
+ QCalendarWidget QSpinBox::down-arrow {{
+ width: 10px;
+ height: 10px;
+ }}
+ QCalendarWidget QWidget {{
+ alternate-background-color: {colors['surface']};
+ }}
+ QCalendarWidget QAbstractItemView:enabled {{
+ font-size: 12px;
+ selection-background-color: {colors['accent']};
+ selection-color: white;
+ background-color: {colors['surface']};
+ color: {colors['text']};
+ }}
+ QCalendarWidget QWidget#qt_calendar_navigationbar {{
+ background-color: {colors['surface']};
+ }}
+ """)
+ else:
+ # 浅色主题样式
+ self.main_frame.setStyleSheet(f"""
+ QFrame#mainFrame {{
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ }}
+ QLabel {{
+ color: {colors['text']};
+ background-color: transparent;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QLabel#dateLabel {{
+ color: {colors['text_secondary']};
+ font-size: 11px;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QFrame#separator {{
+ background-color: {colors['border']};
+ }}
+ QPushButton#closeButton {{
+ background-color: rgba(0, 0, 0, 0.05);
+ border: none;
+ color: {colors['text']};
+ font-size: 18px;
+ font-weight: bold;
+ border-radius: 6px;
+ padding: 2px 4px;
+ }}
+ QPushButton#closeButton:hover {{
+ background-color: #e81123;
+ color: white;
+ border-radius: 6px;
+ }}
+ QPushButton#todayButton, QPushButton#clearButton, QPushButton#insertButton {{
+ background-color: {colors['accent']};
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 6px 16px;
+ font-size: 11px;
+ font-weight: 500;
+ }}
+ QPushButton#todayButton:hover, QPushButton#clearButton:hover, QPushButton#insertButton:hover {{
+ background-color: {colors['accent_hover']};
+ }}
+ """)
+
+ # 更新日历控件样式
+ self.calendar.setStyleSheet(f"""
+ QCalendarWidget {{
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 4px;
+ }}
+ QCalendarWidget QToolButton {{
+ height: 30px;
+ width: 80px;
+ color: {colors['text']};
+ font-size: 12px;
+ font-weight: bold;
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 4px;
+ }}
+ QCalendarWidget QToolButton:hover {{
+ background-color: {colors['surface_hover']};
+ }}
+ QCalendarWidget QMenu {{
+ width: 150px;
+ left: 20px;
+ color: {colors['text']};
+ font-size: 12px;
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ }}
+ QCalendarWidget QSpinBox {{
+ width: 80px;
+ font-size: 12px;
+ background-color: {colors['surface']};
+ selection-background-color: {colors['accent']};
+ selection-color: white;
+ border: 1px solid {colors['border']};
+ border-radius: 4px;
+ color: {colors['text']};
+ }}
+ QCalendarWidget QSpinBox::up-button {{
+ subcontrol-origin: border;
+ subcontrol-position: top right;
+ width: 20px;
+ }}
+ QCalendarWidget QSpinBox::down-button {{
+ subcontrol-origin: border;
+ subcontrol-position: bottom right;
+ width: 20px;
+ }}
+ QCalendarWidget QSpinBox::up-arrow {{
+ width: 10px;
+ height: 10px;
+ }}
+ QCalendarWidget QSpinBox::down-arrow {{
+ width: 10px;
+ height: 10px;
+ }}
+ QCalendarWidget QWidget {{
+ alternate-background-color: {colors['surface']};
+ }}
+ QCalendarWidget QAbstractItemView:enabled {{
+ font-size: 12px;
+ selection-background-color: {colors['accent']};
+ selection-color: white;
+ background-color: {colors['surface']};
+ color: {colors['text']};
+ }}
+ QCalendarWidget QWidget#qt_calendar_navigationbar {{
+ background-color: {colors['surface']};
+ }}
+ """)
+
+ 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 on_date_selected(self, date):
+ """日期选择事件"""
+ self.update_date_label(date)
+
+ def on_today_clicked(self):
+ """今天按钮点击事件"""
+ today = QDate.currentDate()
+ self.calendar.setSelectedDate(today)
+ self.update_date_label(today)
+
+ def on_clear_clicked(self):
+ """清除按钮点击事件"""
+ self.calendar.setSelectedDate(QDate())
+ self.date_label.setText("未选择日期")
+
+ def on_insert_clicked(self):
+ """插入按钮点击事件"""
+ selected_date = self.calendar.selectedDate()
+ if selected_date.isValid():
+ # 发送信号,将选中的日期传递给主窗口
+ date_str = selected_date.toString("yyyy年MM月dd日 dddd")
+ self.date_selected.emit(date_str)
+
+ def update_date_label(self, date=None):
+ """更新日期显示标签"""
+ if date is None:
+ date = self.calendar.selectedDate()
+
+ if date.isValid():
+ date_str = date.toString("yyyy年MM月dd日 (ddd)")
+ self.date_label.setText(f"选中日期: {date_str}")
+ else:
+ self.date_label.setText("未选择日期")
+
+ def get_selected_date(self):
+ """获取选中的日期"""
+ return self.calendar.selectedDate()
+
+ def set_selected_date(self, date):
+ """设置选中的日期"""
+ if isinstance(date, str):
+ date = QDate.fromString(date, "yyyy-MM-dd")
+ self.calendar.setSelectedDate(date)
+ self.update_date_label(date)
+
+ def close_window(self):
+ """关闭窗口 - 只是隐藏而不是销毁"""
+ try:
+ self.closed.emit()
+ self.hide() # 隐藏窗口而不是销毁
+ except Exception as e:
+ print(f"Error in close_window: {e}")
+
+
+def main():
+ """测试函数"""
+ from PyQt5.QtWidgets import QApplication
+
+ app = QApplication(sys.argv)
+
+ # 创建并显示窗口
+ widget = CalendarFloatingWidget()
+ widget.show()
+
+ # 移动到屏幕中心
+ screen_geometry = app.desktop().screenGeometry()
+ widget.move(
+ (screen_geometry.width() - widget.width()) // 2,
+ (screen_geometry.height() - widget.height()) // 2
+ )
+
+ sys.exit(app.exec_())
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py
index 07ad4ff..65e0525 100644
--- a/src/ui/word_style_ui.py
+++ b/src/ui/word_style_ui.py
@@ -711,10 +711,17 @@ class WordRibbon(QFrame):
self.floating_quote_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }")
self.floating_quote_btn.setToolTip("切换每日谏言悬浮窗口")
+ # 日历悬浮窗口按钮
+ self.floating_calendar_btn = QPushButton("📅 悬浮")
+ self.floating_calendar_btn.setFixedSize(60, 30)
+ self.floating_calendar_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }")
+ self.floating_calendar_btn.setToolTip("切换日历悬浮窗口")
+
control_layout.addWidget(self.city_combo)
control_layout.addWidget(self.refresh_weather_btn)
control_layout.addWidget(self.floating_weather_btn)
control_layout.addWidget(self.floating_quote_btn)
+ control_layout.addWidget(self.floating_calendar_btn)
# 添加右侧弹性空间,确保内容居中
control_layout.addStretch()
diff --git a/src/word_main_window.py b/src/word_main_window.py
index 4f4851d..6a9abcc 100644
--- a/src/word_main_window.py
+++ b/src/word_main_window.py
@@ -21,6 +21,7 @@ from input_handler.input_processor import InputProcessor
from ui.calendar_widget import CalendarWidget
from ui.weather_floating_widget import WeatherFloatingWidget
from ui.quote_floating_widget import QuoteFloatingWidget
+from ui.calendar_floating_widget import CalendarFloatingWidget
# 导入主题管理器
from ui.theme_manager import theme_manager
@@ -116,13 +117,17 @@ class WordStyleMainWindow(QMainWindow):
self.weather_floating_widget.closed.connect(self.on_weather_floating_closed)
self.weather_floating_widget.refresh_requested.connect(self.refresh_weather)
- # 初始化每日谏言悬浮窗口
self.quote_floating_widget = QuoteFloatingWidget(self)
self.quote_floating_widget.hide() # 默认隐藏
self.quote_floating_widget.closed.connect(self.on_quote_floating_closed)
self.quote_floating_widget.refresh_requested.connect(self.refresh_daily_quote)
self.quote_floating_widget.insert_requested.connect(self.insert_quote_to_cursor)
+ self.calendar_floating_widget = CalendarFloatingWidget(self)
+ self.calendar_floating_widget.hide() # 默认隐藏
+ self.calendar_floating_widget.closed.connect(self.on_calendar_floating_closed)
+ self.calendar_floating_widget.date_selected.connect(self.insert_date_to_cursor)
+
# 设置窗口属性
self.setWindowTitle("文档1 - MagicWord")
self.setGeometry(100, 100, 1200, 800)
@@ -149,6 +154,14 @@ class WordStyleMainWindow(QMainWindow):
self.ribbon.on_refresh_weather = self.refresh_weather
self.ribbon.on_city_changed = self.on_city_changed
+ # 连接Ribbon的悬浮窗口按钮信号
+ if hasattr(self.ribbon, 'floating_weather_btn'):
+ self.ribbon.floating_weather_btn.clicked.connect(self.toggle_floating_weather)
+ if hasattr(self.ribbon, 'floating_quote_btn'):
+ self.ribbon.floating_quote_btn.clicked.connect(self.toggle_floating_quote)
+ if hasattr(self.ribbon, 'floating_calendar_btn'):
+ self.ribbon.floating_calendar_btn.clicked.connect(self.toggle_floating_calendar)
+
# 初始化时刷新天气
self.refresh_weather()
@@ -723,6 +736,11 @@ class WordStyleMainWindow(QMainWindow):
toggle_floating_quote_action = QAction('每日谏言悬浮窗口', self)
toggle_floating_quote_action.triggered.connect(self.toggle_floating_quote)
weather_menu.addAction(toggle_floating_quote_action)
+
+ # 日历悬浮窗口切换动作
+ toggle_floating_calendar_action = QAction('日历悬浮窗口', self)
+ toggle_floating_calendar_action.triggered.connect(self.toggle_floating_calendar)
+ weather_menu.addAction(toggle_floating_calendar_action)
# 插入菜单
insert_menu = menubar.addMenu('插入(I)')
@@ -1800,6 +1818,22 @@ class WordStyleMainWindow(QMainWindow):
def on_quote_floating_closed(self):
"""每日谏言悬浮窗口关闭时的处理"""
self.status_bar.showMessage("每日谏言悬浮窗口已关闭", 2000)
+
+ def toggle_floating_calendar(self):
+ """切换日历悬浮窗口的显示/隐藏状态"""
+ if hasattr(self, 'calendar_floating_widget'):
+ if self.calendar_floating_widget.isVisible():
+ self.calendar_floating_widget.hide()
+ self.status_bar.showMessage("日历悬浮窗口已隐藏", 2000)
+ else:
+ self.calendar_floating_widget.show()
+ # 确保窗口在屏幕内
+ self.calendar_floating_widget.move(100, 100)
+ self.status_bar.showMessage("日历悬浮窗口已显示", 2000)
+
+ def on_calendar_floating_closed(self):
+ """日历悬浮窗口关闭事件"""
+ self.status_bar.showMessage("日历悬浮窗口已关闭", 2000)
def insert_quote_to_cursor(self, quote_text):
"""将古诗句插入到光标位置"""
@@ -1814,6 +1848,18 @@ class WordStyleMainWindow(QMainWindow):
# 从文本中提取诗句部分用于显示
quote_only = quote_text.split(" —— ")[0] if " —— " in quote_text else quote_text
self.status_bar.showMessage(f"已插入古诗句: {quote_only}", 3000)
+
+ def insert_date_to_cursor(self, date_str):
+ """在光标位置插入日期"""
+ try:
+ # 在光标位置插入日期
+ cursor = self.text_edit.textCursor()
+ cursor.insertText(date_str)
+
+ # 更新状态栏
+ self.status_bar.showMessage(f"已插入日期: {date_str}", 2000)
+ except Exception as e:
+ print(f"插入日期时出错: {e}")
def toggle_weather_tools(self, checked):
"""切换天气工具组显示"""
From cd39d74ace6c318c61cefbb265cbd3b33b580090 Mon Sep 17 00:00:00 2001
From: Maziang <929110464@qq.com>
Date: Thu, 20 Nov 2025 21:06:46 +0800
Subject: [PATCH 6/8] =?UTF-8?q?=E5=A4=A9=E6=B0=94=E6=82=AC=E6=B5=AE?=
=?UTF-8?q?=E7=AA=97=E5=8F=A3=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/ui/weather_floating_widget.py | 71 ++++++++++++++++---------------
src/word_main_window.py | 13 +++++-
2 files changed, 48 insertions(+), 36 deletions(-)
diff --git a/src/ui/weather_floating_widget.py b/src/ui/weather_floating_widget.py
index 047c619..bb373de 100644
--- a/src/ui/weather_floating_widget.py
+++ b/src/ui/weather_floating_widget.py
@@ -203,44 +203,45 @@ class WeatherFloatingWidget(QDialog):
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;
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}}
QLabel {{
color: {colors['text']};
background-color: transparent;
- padding: 3px 5px; // 适度增加padding使布局更舒适
- margin: 1px;
+ padding: 4px 6px;
+ margin: 2px;
}}
QLabel#temperatureLabel {{
color: {colors['accent']};
- font-size: 19px; // 适度增大字体大小
+ font-size: 20px;
font-weight: bold;
- padding: 5px 7px; // 适度增加padding使布局更舒适
- margin: 2px;
+ padding: 6px 8px;
+ margin: 3px;
}}
QLabel#cityLabel {{
color: {colors['text_secondary']};
- font-size: 12px; // 适度增大字体大小
- padding: 3px 5px; // 适度增加padding使布局更舒适
- margin: 1px;
+ font-size: 12px;
+ padding: 4px 6px;
+ margin: 2px;
}}
QLabel#weatherDescLabel {{
color: {colors['text']};
- font-size: 12px; // 适度增大字体大小
+ font-size: 12px;
font-weight: 500;
- padding: 3px 5px; // 适度增加padding使布局更舒适
- margin: 1px;
+ padding: 4px 6px;
+ margin: 2px;
}}
QLabel#detailLabel {{
color: {colors['text_secondary']};
- font-size: 11px; // 适度增大字体大小
- padding: 3px 5px; // 适度增加padding使布局更舒适
- margin: 1px;
+ font-size: 11px;
+ padding: 4px 6px;
+ margin: 2px;
}}
QFrame#separator {{
background-color: {colors['border']};
@@ -263,9 +264,9 @@ class WeatherFloatingWidget(QDialog):
background-color: {colors['accent']};
color: white;
border: none;
- border-radius: 6px; // 适度增加圆角
- padding: 5px 14px; // 适度增加padding使按钮更舒适
- font-size: 11px; // 适度增大字体大小
+ border-radius: 6px;
+ padding: 6px 16px;
+ font-size: 11px;
font-weight: 500;
}}
QPushButton#refreshButton:hover, QPushButton#detailButton:hover {{
@@ -275,28 +276,28 @@ class WeatherFloatingWidget(QDialog):
background-color: {colors['surface']};
color: {colors['text']};
border: 1px solid {colors['border']};
- border-radius: 6px; // 适度增加圆角
- padding: 4px 7px; // 适度增加padding使布局更舒适
- font-size: 11px; // 适度增大字体大小
+ border-radius: 6px;
+ padding: 4px 7px;
+ font-size: 11px;
font-weight: 500;
- min-height: 24px; // 适度增加最小高度使布局更舒适
+ min-height: 24px;
}}
QComboBox#cityCombo:hover {{
border-color: {colors['accent']};
}}
QComboBox#cityCombo::drop-down {{
border: none;
- width: 14px; // 适度增加宽度
+ width: 14px;
}}
QComboBox#cityCombo::down-arrow {{
image: none;
border-left: 2px solid transparent;
border-right: 2px solid transparent;
- border-top: 5px solid {colors['text']}; // 适度增加箭头大小
+ border-top: 5px solid {colors['text']};
}}
""")
else:
- # 浅色主题样式
+ # 浅色主题样式 - 与每日谏言悬浮窗口保持一致
self.main_frame.setStyleSheet(f"""
QFrame#mainFrame {{
background-color: {colors['surface']};
@@ -357,9 +358,9 @@ class WeatherFloatingWidget(QDialog):
background-color: {colors['accent']};
color: white;
border: none;
- border-radius: 6px; // 适度增加圆角
- padding: 5px 14px; // 适度增加padding使按钮更舒适
- font-size: 11px; // 适度增大字体大小
+ border-radius: 6px;
+ padding: 6px 16px;
+ font-size: 11px;
font-weight: 500;
}}
QPushButton#refreshButton:hover, QPushButton#detailButton:hover {{
@@ -369,24 +370,24 @@ class WeatherFloatingWidget(QDialog):
background-color: {colors['surface']};
color: {colors['text']};
border: 1px solid {colors['border']};
- border-radius: 6px; // 适度增加圆角
- padding: 4px 7px; // 适度增加padding使布局更舒适
- font-size: 11px; // 适度增大字体大小
+ border-radius: 6px;
+ padding: 4px 7px;
+ font-size: 11px;
font-weight: 500;
- min-height: 24px; // 适度增加最小高度使布局更舒适
+ min-height: 24px;
}}
QComboBox#cityCombo:hover {{
border-color: {colors['accent']};
}}
QComboBox#cityCombo::drop-down {{
border: none;
- width: 14px; // 适度增加宽度
+ width: 14px;
}}
QComboBox#cityCombo::down-arrow {{
image: none;
border-left: 2px solid transparent;
border-right: 2px solid transparent;
- border-top: 5px solid {colors['text']}; // 适度增加箭头大小
+ border-top: 5px solid {colors['text']};
}}
""")
diff --git a/src/word_main_window.py b/src/word_main_window.py
index 6a9abcc..7744c81 100644
--- a/src/word_main_window.py
+++ b/src/word_main_window.py
@@ -1655,7 +1655,18 @@ class WordStyleMainWindow(QMainWindow):
"""手动刷新天气信息"""
try:
# 获取当前选择的城市
- current_city = self.ribbon.city_combo.currentText()
+ current_city = "自动定位" # 默认值
+
+ # 安全地获取当前城市选择
+ if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'city_combo'):
+ current_city = self.ribbon.city_combo.currentText()
+ elif hasattr(self, 'weather_floating_widget') and hasattr(self.weather_floating_widget, 'city_combo'):
+ # 如果Ribbon中的city_combo不可用,尝试从天气悬浮窗口获取
+ current_city = self.weather_floating_widget.city_combo.currentText()
+ else:
+ # 如果都没有,使用默认值
+ current_city = "自动定位"
+
print(f"刷新天气 - 当前选择的城市: {current_city}")
if current_city == '自动定位':
From db65657f15bb60679263bc78fe1b975c025e95c2 Mon Sep 17 00:00:00 2001
From: Maziang <929110464@qq.com>
Date: Thu, 20 Nov 2025 21:56:38 +0800
Subject: [PATCH 7/8] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=B4=AA=E5=90=83?=
=?UTF-8?q?=E8=9B=87=E6=B8=B8=E6=88=8F=E5=8F=8A=E5=85=B6=E6=8E=A7=E5=88=B6?=
=?UTF-8?q?=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/ui/snake_game.py | 437 ++++++++++++++++++++++++++++++++++++++++
src/word_main_window.py | 26 +++
test_snake_game.py | 22 ++
test_speed_control.py | 71 +++++++
4 files changed, 556 insertions(+)
create mode 100644 src/ui/snake_game.py
create mode 100644 test_snake_game.py
create mode 100644 test_speed_control.py
diff --git a/src/ui/snake_game.py b/src/ui/snake_game.py
new file mode 100644
index 0000000..6c8dd5f
--- /dev/null
+++ b/src/ui/snake_game.py
@@ -0,0 +1,437 @@
+# snake_game.py
+"""
+贪吃蛇小游戏模块
+用户用WASD或方向键控制贪吃蛇移动
+"""
+
+import sys
+import random
+from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QLabel, QMessageBox
+from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QRect
+from PyQt5.QtGui import QPainter, QColor, QFont, QBrush, QPen
+
+
+class SnakeGame(QWidget):
+ """贪吃蛇游戏画布"""
+
+ # 游戏常量
+ GRID_SIZE = 20 # 网格大小
+ GRID_WIDTH = 30 # 水平网格数
+ GRID_HEIGHT = 20 # 垂直网格数
+ GAME_SPEED = 150 # 游戏速度(毫秒)
+ MIN_SPEED = 50 # 最小速度(毫秒)
+ MAX_SPEED = 300 # 最大速度(毫秒)
+ SPEED_STEP = 10 # 速度调节步长(毫秒)
+
+ # 信号
+ score_changed = pyqtSignal(int)
+ game_over = pyqtSignal(int)
+ speed_changed = pyqtSignal(int) # 速度改变信号
+
+ # 方向常量
+ UP = (0, -1)
+ DOWN = (0, 1)
+ LEFT = (-1, 0)
+ RIGHT = (1, 0)
+
+ def __init__(self):
+ super().__init__()
+ self.init_game()
+ self.init_ui()
+
+ def init_ui(self):
+ """初始化UI"""
+ self.setFixedSize(
+ self.GRID_WIDTH * self.GRID_SIZE,
+ self.GRID_HEIGHT * self.GRID_SIZE
+ )
+ self.setStyleSheet("background-color: #1a1a1a;")
+ self.setFocus() # 获取焦点以接收键盘输入
+
+ def init_game(self):
+ """初始化游戏状态"""
+ # 蛇的初始位置(从中间开始)
+ self.snake = [
+ (self.GRID_WIDTH // 2, self.GRID_HEIGHT // 2),
+ (self.GRID_WIDTH // 2 - 1, self.GRID_HEIGHT // 2),
+ (self.GRID_WIDTH // 2 - 2, self.GRID_HEIGHT // 2),
+ ]
+
+ # 方向
+ self.direction = self.RIGHT
+ self.next_direction = self.RIGHT
+
+ # 食物位置
+ self.food = self.generate_food()
+
+ # 分数
+ self.score = 0
+
+ # 游戏速度
+ self.current_speed = self.GAME_SPEED
+
+ # 游戏状态
+ self.is_running = False
+ self.is_game_over = False
+
+ # 游戏定时器
+ self.game_timer = QTimer()
+ self.game_timer.timeout.connect(self.update_game)
+
+ def generate_food(self):
+ """生成食物位置"""
+ while True:
+ x = random.randint(0, self.GRID_WIDTH - 1)
+ y = random.randint(0, self.GRID_HEIGHT - 1)
+ if (x, y) not in self.snake:
+ return (x, y)
+
+ def start_game(self):
+ """开始游戏"""
+ if not self.is_running:
+ self.is_running = True
+ self.is_game_over = False
+ self.game_timer.start(self.current_speed)
+ self.setFocus()
+
+ def pause_game(self):
+ """暂停游戏"""
+ if self.is_running:
+ self.is_running = False
+ self.game_timer.stop()
+
+ def resume_game(self):
+ """恢复游戏"""
+ if not self.is_running and not self.is_game_over:
+ self.is_running = True
+ self.game_timer.start(self.current_speed)
+
+ def restart_game(self):
+ """重新开始游戏"""
+ self.game_timer.stop()
+ self.init_game()
+ self.score_changed.emit(0)
+ self.speed_changed.emit(self.current_speed)
+ self.update()
+ # 重新启动游戏
+ self.start_game()
+
+ def increase_speed(self):
+ """增加游戏速度"""
+ if self.current_speed > self.MIN_SPEED:
+ self.current_speed = max(self.current_speed - self.SPEED_STEP, self.MIN_SPEED)
+ self.speed_changed.emit(self.current_speed)
+ if self.is_running:
+ self.game_timer.setInterval(self.current_speed)
+
+ def decrease_speed(self):
+ """降低游戏速度"""
+ if self.current_speed < self.MAX_SPEED:
+ self.current_speed = min(self.current_speed + self.SPEED_STEP, self.MAX_SPEED)
+ self.speed_changed.emit(self.current_speed)
+ if self.is_running:
+ self.game_timer.setInterval(self.current_speed)
+
+ def update_game(self):
+ """更新游戏状态"""
+ if not self.is_running:
+ return
+
+ # 更新方向
+ self.direction = self.next_direction
+
+ # 计算新的头部位置
+ head_x, head_y = self.snake[0]
+ dx, dy = self.direction
+ new_head = (head_x + dx, head_y + dy)
+
+ # 检查碰撞
+ if self.check_collision(new_head):
+ self.is_running = False
+ self.is_game_over = True
+ self.game_timer.stop()
+ self.game_over.emit(self.score)
+ self.update()
+ return
+
+ # 添加新的头部
+ self.snake.insert(0, new_head)
+
+ # 检查是否吃到食物
+ if new_head == self.food:
+ self.score += 10
+ self.score_changed.emit(self.score)
+ self.food = self.generate_food()
+ else:
+ # 移除尾部
+ self.snake.pop()
+
+ self.update()
+
+ def check_collision(self, position):
+ """检查碰撞"""
+ x, y = position
+
+ # 检查边界碰撞
+ if x < 0 or x >= self.GRID_WIDTH or y < 0 or y >= self.GRID_HEIGHT:
+ return True
+
+ # 检查自身碰撞
+ if position in self.snake:
+ return True
+
+ return False
+
+ def keyPressEvent(self, event):
+ """处理键盘输入"""
+ if event.isAutoRepeat():
+ return
+
+ key = event.key()
+
+ # 使用WASD或方向键控制方向
+ if key in (Qt.Key_W, Qt.Key_Up):
+ # 上键:向上
+ if self.direction != self.DOWN:
+ self.next_direction = self.UP
+ elif key in (Qt.Key_S, Qt.Key_Down):
+ # 下键:向下
+ if self.direction != self.UP:
+ self.next_direction = self.DOWN
+ elif key in (Qt.Key_A, Qt.Key_Left):
+ # 左键:向左(不用于调速)
+ if self.direction != self.RIGHT:
+ self.next_direction = self.LEFT
+ elif key in (Qt.Key_D, Qt.Key_Right):
+ # 右键:向右(不用于调速)
+ if self.direction != self.LEFT:
+ self.next_direction = self.RIGHT
+ elif key == Qt.Key_Space:
+ # 空格键暂停/恢复
+ if self.is_running:
+ self.pause_game()
+ elif not self.is_game_over:
+ self.resume_game()
+ elif key == Qt.Key_R:
+ # R键重新开始
+ if self.is_game_over:
+ self.restart_game()
+ elif key == Qt.Key_Plus or key == Qt.Key_Equal:
+ # + 或 = 键加速
+ self.increase_speed()
+ elif key == Qt.Key_Minus:
+ # - 键减速
+ self.decrease_speed()
+
+ event.accept()
+
+ def paintEvent(self, event):
+ """绘制游戏"""
+ painter = QPainter(self)
+ painter.setRenderHint(QPainter.Antialiasing)
+
+ # 绘制网格背景
+ self.draw_grid(painter)
+
+ # 绘制食物
+ self.draw_food(painter)
+
+ # 绘制蛇
+ self.draw_snake(painter)
+
+ # 如果游戏结束,显示游戏结束提示
+ if self.is_game_over:
+ self.draw_game_over(painter)
+
+ def draw_grid(self, painter):
+ """绘制网格"""
+ painter.setPen(QPen(QColor(50, 50, 50), 1))
+
+ # 绘制竖线
+ for x in range(self.GRID_WIDTH + 1):
+ painter.drawLine(
+ x * self.GRID_SIZE, 0,
+ x * self.GRID_SIZE, self.GRID_HEIGHT * self.GRID_SIZE
+ )
+
+ # 绘制横线
+ for y in range(self.GRID_HEIGHT + 1):
+ painter.drawLine(
+ 0, y * self.GRID_SIZE,
+ self.GRID_WIDTH * self.GRID_SIZE, y * self.GRID_SIZE
+ )
+
+ def draw_snake(self, painter):
+ """绘制蛇"""
+ # 绘制蛇身
+ for i, (x, y) in enumerate(self.snake):
+ if i == 0:
+ # 蛇头 - 更亮的绿色
+ painter.fillRect(
+ x * self.GRID_SIZE + 1,
+ y * self.GRID_SIZE + 1,
+ self.GRID_SIZE - 2,
+ self.GRID_SIZE - 2,
+ QColor(0, 255, 0)
+ )
+ # 绘制眼睛
+ painter.fillRect(
+ x * self.GRID_SIZE + 4,
+ y * self.GRID_SIZE + 4,
+ 3, 3,
+ QColor(255, 0, 0)
+ )
+ else:
+ # 蛇身 - 稍暗的绿色
+ painter.fillRect(
+ x * self.GRID_SIZE + 1,
+ y * self.GRID_SIZE + 1,
+ self.GRID_SIZE - 2,
+ self.GRID_SIZE - 2,
+ QColor(0, 200, 0)
+ )
+
+ def draw_food(self, painter):
+ """绘制食物"""
+ x, y = self.food
+ painter.fillRect(
+ x * self.GRID_SIZE + 3,
+ y * self.GRID_SIZE + 3,
+ self.GRID_SIZE - 6,
+ self.GRID_SIZE - 6,
+ QColor(255, 0, 0)
+ )
+
+ def draw_game_over(self, painter):
+ """绘制游戏结束界面"""
+ # 半透明黑色背景
+ painter.fillRect(self.rect(), QColor(0, 0, 0, 200))
+
+ # 绘制文本
+ painter.setPen(QColor(255, 255, 255))
+ font = QFont("Arial", 30, QFont.Bold)
+ painter.setFont(font)
+
+ text = "游戏结束"
+ fm = painter.fontMetrics()
+ text_width = fm.width(text)
+ text_height = fm.height()
+
+ x = (self.width() - text_width) // 2
+ y = (self.height() - text_height) // 2 - 20
+
+ painter.drawText(x, y, text)
+
+ # 绘制分数
+ font.setPointSize(20)
+ painter.setFont(font)
+ score_text = f"最终分数: {self.score}"
+ fm = painter.fontMetrics()
+ score_width = fm.width(score_text)
+ score_x = (self.width() - score_width) // 2
+ score_y = y + 50
+
+ painter.drawText(score_x, score_y, score_text)
+
+ # 绘制提示
+ font.setPointSize(12)
+ painter.setFont(font)
+ hint_text = "按R键重新开始"
+ fm = painter.fontMetrics()
+ hint_width = fm.width(hint_text)
+ hint_x = (self.width() - hint_width) // 2
+ hint_y = score_y + 40
+
+ painter.drawText(hint_x, hint_y, hint_text)
+
+
+class SnakeGameWindow(QMainWindow):
+ """贪吃蛇游戏窗口"""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("贪吃蛇游戏")
+ self.setGeometry(200, 200, 700, 550)
+
+ # 创建中央控件
+ central_widget = QWidget()
+ self.setCentralWidget(central_widget)
+
+ # 创建布局
+ layout = QVBoxLayout(central_widget)
+ layout.setContentsMargins(10, 10, 10, 10)
+
+ # 创建游戏画布
+ self.game_widget = SnakeGame()
+ layout.addWidget(self.game_widget)
+
+ # 创建控制面板
+ control_layout = QVBoxLayout()
+
+ # 分数标签
+ self.score_label = QLabel("分数: 0")
+ self.score_label.setStyleSheet("font-size: 16px; font-weight: bold;")
+ control_layout.addWidget(self.score_label)
+
+ # 速度标签
+ self.speed_label = QLabel("速度: 正常")
+ self.speed_label.setStyleSheet("font-size: 14px; color: #0066cc;")
+ control_layout.addWidget(self.speed_label)
+
+ # 提示标签
+ self.hint_label = QLabel(
+ "控制方法: W/↑ 上 S/↓ 下 A/← 左 D/→ 右 | 空格暂停 | +/- 调速 | R重新开始"
+ )
+ self.hint_label.setStyleSheet("font-size: 12px; color: gray;")
+ control_layout.addWidget(self.hint_label)
+
+ layout.addLayout(control_layout)
+
+ # 连接信号
+ self.game_widget.score_changed.connect(self.update_score)
+ self.game_widget.game_over.connect(self.on_game_over)
+ self.game_widget.speed_changed.connect(self.update_speed)
+
+ # 设置窗口样式
+ self.setStyleSheet("""
+ QMainWindow {
+ background-color: #f0f0f0;
+ }
+ QLabel {
+ color: #333;
+ }
+ """)
+
+ # 启动游戏
+ self.game_widget.start_game()
+
+ def update_score(self, score):
+ """更新分数显示"""
+ self.score_label.setText(f"分数: {score}")
+
+ def update_speed(self, speed_ms):
+ """更新速度显示"""
+ # 将毫秒转换为速度等级
+ if speed_ms <= 50:
+ speed_level = "极快"
+ elif speed_ms <= 100:
+ speed_level = "很快"
+ elif speed_ms <= 150:
+ speed_level = "正常"
+ elif speed_ms <= 200:
+ speed_level = "稍慢"
+ else:
+ speed_level = "很慢"
+
+ self.speed_label.setText(f"速度: {speed_level} ({speed_ms}ms)")
+
+ def on_game_over(self, final_score):
+ """游戏结束处理"""
+ pass # 游戏结束信息已在游戏画布中显示
+
+ def keyPressEvent(self, event):
+ """处理键盘输入"""
+ if event.key() == Qt.Key_R:
+ self.game_widget.restart_game()
+ else:
+ super().keyPressEvent(event)
diff --git a/src/word_main_window.py b/src/word_main_window.py
index 7744c81..fab2880 100644
--- a/src/word_main_window.py
+++ b/src/word_main_window.py
@@ -813,6 +813,17 @@ class WordStyleMainWindow(QMainWindow):
# 开发工具菜单
developer_menu = menubar.addMenu('开发工具(Q)')
+ # 应用选项菜单
+ app_menu = menubar.addMenu('应用选项(O)')
+
+ # 小游戏子菜单
+ games_menu = app_menu.addMenu('小游戏')
+
+ # 贪吃蛇游戏
+ snake_game_action = QAction('贪吃蛇', self)
+ snake_game_action.triggered.connect(self.launch_snake_game)
+ games_menu.addAction(snake_game_action)
+
# 帮助菜单
help_menu = menubar.addMenu('帮助(H)')
@@ -2710,6 +2721,21 @@ class WordStyleMainWindow(QMainWindow):
# 显示替换结果
QMessageBox.information(self, "替换", f"已完成 {count} 处替换。")
+ def launch_snake_game(self):
+ """启动贪吃蛇游戏"""
+ try:
+ from ui.snake_game import SnakeGameWindow
+
+ # 创建游戏窗口
+ self.snake_game_window = SnakeGameWindow(self)
+ self.snake_game_window.show()
+ except Exception as e:
+ QMessageBox.critical(
+ self,
+ "错误",
+ f"无法启动贪吃蛇游戏:{str(e)}"
+ )
+
def show_about(self):
"""显示关于对话框"""
# 创建自定义对话框
diff --git a/test_snake_game.py b/test_snake_game.py
new file mode 100644
index 0000000..fc416aa
--- /dev/null
+++ b/test_snake_game.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python3
+# test_snake_game.py
+"""测试贪吃蛇游戏"""
+
+import sys
+import os
+
+# 添加项目根目录到Python路径
+project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, project_root)
+
+from PyQt5.QtWidgets import QApplication
+from src.ui.snake_game import SnakeGameWindow
+
+def main():
+ app = QApplication(sys.argv)
+ game_window = SnakeGameWindow()
+ game_window.show()
+ sys.exit(app.exec_())
+
+if __name__ == '__main__':
+ main()
diff --git a/test_speed_control.py b/test_speed_control.py
new file mode 100644
index 0000000..9f08bae
--- /dev/null
+++ b/test_speed_control.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+"""
+测试贪吃蛇游戏的速度调节功能
+"""
+
+import sys
+sys.path.insert(0, '/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design')
+
+from src.ui.snake_game import SnakeGame, SnakeGameWindow
+from PyQt5.QtWidgets import QApplication
+from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QKeyEvent
+
+def test_snake_game():
+ """测试贪吃蛇游戏"""
+ app = QApplication(sys.argv)
+
+ # 创建游戏窗口
+ window = SnakeGameWindow()
+ window.show()
+
+ # 获取游戏实例
+ game = window.game_widget
+
+ # 测试初始速度
+ print(f"初始速度: {game.current_speed}ms")
+ assert game.current_speed == game.GAME_SPEED, "初始速度应该是150ms"
+
+ # 测试增加速度(按上键)
+ print("\n测试增加速度...")
+ initial_speed = game.current_speed
+ game.increase_speed()
+ print(f"按上键后速度: {game.current_speed}ms (从 {initial_speed}ms)")
+ assert game.current_speed < initial_speed, "速度应该增加(毫秒数减少)"
+
+ # 测试降低速度(按下键)
+ print("\n测试降低速度...")
+ current_speed = game.current_speed
+ game.decrease_speed()
+ print(f"按下键后速度: {game.current_speed}ms (从 {current_speed}ms)")
+ assert game.current_speed > current_speed, "速度应该降低(毫秒数增加)"
+
+ # 测试速度限制
+ print("\n测试速度限制...")
+
+ # 测试最小速度限制
+ game.current_speed = game.MIN_SPEED
+ game.increase_speed()
+ print(f"最小速度限制测试: {game.current_speed}ms (应该 >= {game.MIN_SPEED}ms)")
+ assert game.current_speed >= game.MIN_SPEED, "速度不应该低于最小值"
+
+ # 测试最大速度限制
+ game.current_speed = game.MAX_SPEED
+ game.decrease_speed()
+ print(f"最大速度限制测试: {game.current_speed}ms (应该 <= {game.MAX_SPEED}ms)")
+ assert game.current_speed <= game.MAX_SPEED, "速度不应该超过最大值"
+
+ print("\n✓ 所有测试通过!")
+ print(f"速度范围: {game.MIN_SPEED}ms - {game.MAX_SPEED}ms")
+ print(f"速度步长: {game.SPEED_STEP}ms")
+
+ window.close()
+
+if __name__ == '__main__':
+ try:
+ test_snake_game()
+ except Exception as e:
+ print(f"✗ 测试失败: {e}")
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)
From adca960a879ec84e531c4ab473c92c9852979acc Mon Sep 17 00:00:00 2001
From: Maziang <929110464@qq.com>
Date: Thu, 20 Nov 2025 22:40:50 +0800
Subject: [PATCH 8/8] =?UTF-8?q?=E6=B7=BB=E5=8A=A0DeepSeek=20AI=E5=AF=B9?=
=?UTF-8?q?=E8=AF=9D=E7=AA=97=E5=8F=A3=E5=8F=8AAPI=E5=AF=86=E9=92=A5?=
=?UTF-8?q?=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 4 +
src/deepseek_dialog_window.py | 627 ++++++++++++++++++++++++++++++++++
src/word_main_window.py | 49 +++
3 files changed, 680 insertions(+)
create mode 100644 src/deepseek_dialog_window.py
diff --git a/.gitignore b/.gitignore
index f661aa2..2d96080 100644
--- a/.gitignore
+++ b/.gitignore
@@ -198,6 +198,10 @@ temp/
*.orig
# Project specific
+resources/config/deepseek_api.json
+*.key
+*.secret
+config/*.json
# Documentation folder
doc/
diff --git a/src/deepseek_dialog_window.py b/src/deepseek_dialog_window.py
new file mode 100644
index 0000000..22cf9f1
--- /dev/null
+++ b/src/deepseek_dialog_window.py
@@ -0,0 +1,627 @@
+"""
+DeepSeek对话窗口模块
+提供与DeepSeek AI对话的功能
+"""
+
+import os
+import json
+import requests
+import threading
+from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTextEdit,
+ QLineEdit, QPushButton, QLabel, QMessageBox,
+ QSplitter, QScrollArea, QWidget, QFrame)
+from PyQt5.QtCore import Qt, pyqtSignal, QTimer
+from PyQt5.QtGui import QFont, QTextCursor
+
+
+class DeepSeekDialogWindow(QDialog):
+ """DeepSeek对话窗口"""
+
+ closed = pyqtSignal() # 窗口关闭信号
+ streaming_finished = pyqtSignal() # 流式输出完成信号
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.parent = parent
+ self.api_key = ""
+ self.conversation_history = []
+
+ # 流式输出相关变量
+ self.is_streaming = False
+ self.current_streaming_content = ""
+ self.streaming_message_id = ""
+ self.streaming_thread = None
+ self.streaming_timer = None
+
+ # 从本地加载API密钥
+ self.load_api_key()
+
+ # 如果没有API密钥,显示设置对话框
+ if not self.api_key:
+ self.show_api_key_dialog()
+
+ self.init_ui()
+
+ def load_api_key(self):
+ """从本地文件加载API密钥"""
+ config_file = os.path.join(os.path.dirname(__file__), "..", "resources", "config", "deepseek_api.json")
+
+ try:
+ if os.path.exists(config_file):
+ with open(config_file, 'r', encoding='utf-8') as f:
+ config = json.load(f)
+ self.api_key = config.get('api_key', '')
+ except Exception as e:
+ print(f"加载API密钥失败: {e}")
+
+ def save_api_key(self, api_key):
+ """保存API密钥到本地文件"""
+ config_dir = os.path.join(os.path.dirname(__file__), "..", "resources", "config")
+ config_file = os.path.join(config_dir, "deepseek_api.json")
+
+ try:
+ # 确保配置目录存在
+ os.makedirs(config_dir, exist_ok=True)
+
+ config = {'api_key': api_key}
+ with open(config_file, 'w', encoding='utf-8') as f:
+ json.dump(config, f, ensure_ascii=False, indent=2)
+
+ self.api_key = api_key
+ return True
+ except Exception as e:
+ QMessageBox.critical(self, "错误", f"保存API密钥失败: {e}")
+ return False
+
+ def show_api_key_dialog(self):
+ """显示API密钥输入对话框"""
+ dialog = QDialog(self)
+ dialog.setWindowTitle("DeepSeek API密钥设置")
+ dialog.setModal(True)
+ dialog.setFixedSize(400, 200)
+
+ layout = QVBoxLayout()
+
+ # 说明文本
+ info_label = QLabel("请输入您的DeepSeek API密钥:")
+ info_label.setWordWrap(True)
+ layout.addWidget(info_label)
+
+ # API密钥输入框
+ api_key_layout = QHBoxLayout()
+ api_key_label = QLabel("API密钥:")
+ self.api_key_input = QLineEdit()
+ self.api_key_input.setPlaceholderText("请输入您的DeepSeek API密钥")
+ self.api_key_input.setEchoMode(QLineEdit.Password)
+ api_key_layout.addWidget(api_key_label)
+ api_key_layout.addWidget(self.api_key_input)
+ layout.addLayout(api_key_layout)
+
+ # 按钮布局
+ button_layout = QHBoxLayout()
+
+ save_button = QPushButton("保存")
+ save_button.clicked.connect(lambda: self.save_and_close(dialog))
+
+ cancel_button = QPushButton("取消")
+ cancel_button.clicked.connect(dialog.reject)
+
+ button_layout.addWidget(save_button)
+ button_layout.addWidget(cancel_button)
+ layout.addLayout(button_layout)
+
+ dialog.setLayout(layout)
+
+ if dialog.exec_() == QDialog.Accepted:
+ return True
+ else:
+ QMessageBox.warning(self, "警告", "未设置API密钥,无法使用对话功能")
+ return False
+
+ def save_and_close(self, dialog):
+ """保存API密钥并关闭对话框"""
+ api_key = self.api_key_input.text().strip()
+ if not api_key:
+ QMessageBox.warning(self, "警告", "请输入有效的API密钥")
+ return
+
+ if self.save_api_key(api_key):
+ QMessageBox.information(self, "成功", "API密钥已保存")
+ dialog.accept()
+
+ def init_ui(self):
+ """初始化用户界面"""
+ self.setWindowTitle("DeepSeek AI对话")
+ self.setMinimumSize(800, 600)
+
+ # 主布局
+ main_layout = QVBoxLayout()
+
+ # 标题
+ title_label = QLabel("DeepSeek AI对话助手")
+ title_font = QFont()
+ title_font.setPointSize(16)
+ title_font.setBold(True)
+ title_label.setFont(title_font)
+ title_label.setAlignment(Qt.AlignCenter)
+ title_label.setStyleSheet("padding: 10px; background-color: #f0f0f0;")
+ main_layout.addWidget(title_label)
+
+ # 分割器
+ splitter = QSplitter(Qt.Vertical)
+
+ # 对话显示区域
+ self.create_conversation_area(splitter)
+
+ # 输入区域
+ self.create_input_area(splitter)
+
+ splitter.setSizes([400, 200])
+ main_layout.addWidget(splitter)
+
+ # 状态栏
+ status_layout = QHBoxLayout()
+ self.status_label = QLabel("就绪")
+ self.status_label.setStyleSheet("color: #666; padding: 5px;")
+ status_layout.addWidget(self.status_label)
+ status_layout.addStretch()
+
+ # API密钥管理按钮
+ api_key_button = QPushButton("管理API密钥")
+ api_key_button.clicked.connect(self.manage_api_key)
+ status_layout.addWidget(api_key_button)
+
+ main_layout.addLayout(status_layout)
+
+ self.setLayout(main_layout)
+
+ # 设置样式
+ self.apply_theme("light") # 默认使用浅色主题
+
+ # 添加主题切换按钮
+ theme_button = QPushButton("切换主题")
+ theme_button.clicked.connect(self.toggle_theme)
+ status_layout.addWidget(theme_button)
+
+ # 连接信号
+ self.streaming_finished.connect(self.on_streaming_finished)
+
+ def toggle_theme(self):
+ """切换黑白主题"""
+ if hasattr(self, 'current_theme') and self.current_theme == "dark":
+ self.apply_theme("light")
+ else:
+ self.apply_theme("dark")
+
+ def apply_theme(self, theme):
+ """应用主题样式"""
+ self.current_theme = theme
+
+ if theme == "dark":
+ # 深色主题样式
+ self.setStyleSheet("""
+ QDialog {
+ background-color: #1e1e1e;
+ color: #ffffff;
+ }
+ QLabel {
+ color: #ffffff;
+ }
+ QTextEdit {
+ background-color: #2d2d2d;
+ color: #ffffff;
+ border: 1px solid #444;
+ border-radius: 4px;
+ padding: 10px;
+ font-family: 'Microsoft YaHei', sans-serif;
+ font-size: 12px;
+ }
+ QLineEdit {
+ background-color: #2d2d2d;
+ color: #ffffff;
+ border: 1px solid #444;
+ border-radius: 4px;
+ padding: 8px;
+ font-size: 12px;
+ }
+ QPushButton {
+ background-color: #0078d7;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 8px 16px;
+ font-size: 12px;
+ }
+ QPushButton:hover {
+ background-color: #106ebe;
+ }
+ QPushButton:pressed {
+ background-color: #005a9e;
+ }
+ QScrollArea {
+ background-color: #1e1e1e;
+ border: none;
+ }
+ QScrollBar:vertical {
+ background-color: #2d2d2d;
+ width: 15px;
+ margin: 0px;
+ }
+ QScrollBar::handle:vertical {
+ background-color: #555;
+ border-radius: 7px;
+ min-height: 20px;
+ }
+ QScrollBar::handle:vertical:hover {
+ background-color: #666;
+ }
+ """)
+ else:
+ # 浅色主题样式
+ self.setStyleSheet("""
+ QDialog {
+ background-color: #ffffff;
+ color: #000000;
+ }
+ QLabel {
+ color: #000000;
+ }
+ QTextEdit {
+ background-color: #ffffff;
+ color: #000000;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 10px;
+ font-family: 'Microsoft YaHei', sans-serif;
+ font-size: 12px;
+ }
+ QLineEdit {
+ background-color: #ffffff;
+ color: #000000;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 8px;
+ font-size: 12px;
+ }
+ QPushButton {
+ background-color: #0078d7;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 8px 16px;
+ font-size: 12px;
+ }
+ QPushButton:hover {
+ background-color: #106ebe;
+ }
+ QPushButton:pressed {
+ background-color: #005a9e;
+ }
+ QScrollArea {
+ background-color: #ffffff;
+ border: none;
+ }
+ QScrollBar:vertical {
+ background-color: #f0f0f0;
+ width: 15px;
+ margin: 0px;
+ }
+ QScrollBar::handle:vertical {
+ background-color: #c0c0c0;
+ border-radius: 7px;
+ min-height: 20px;
+ }
+ QScrollBar::handle:vertical:hover {
+ background-color: #a0a0a0;
+ }
+ """)
+
+ def create_conversation_area(self, parent):
+ """创建对话显示区域"""
+ scroll_area = QScrollArea()
+ scroll_area.setWidgetResizable(True)
+
+ conversation_widget = QWidget()
+ conversation_layout = QVBoxLayout()
+
+ # 对话显示文本框
+ self.conversation_text = QTextEdit()
+ self.conversation_text.setReadOnly(True)
+ self.conversation_text.setFont(QFont("Microsoft YaHei", 10))
+ conversation_layout.addWidget(self.conversation_text)
+
+ conversation_widget.setLayout(conversation_layout)
+ scroll_area.setWidget(conversation_widget)
+
+ parent.addWidget(scroll_area)
+
+ def create_input_area(self, parent):
+ """创建输入区域"""
+ input_widget = QWidget()
+ input_layout = QVBoxLayout()
+
+ # 输入框
+ input_label = QLabel("输入您的问题:")
+ self.input_edit = QTextEdit()
+ self.input_edit.setMaximumHeight(80)
+ self.input_edit.setPlaceholderText("请输入您的问题...")
+
+ # 按钮布局
+ button_layout = QHBoxLayout()
+
+ send_button = QPushButton("发送")
+ send_button.clicked.connect(self.send_message)
+
+ clear_button = QPushButton("清空对话")
+ clear_button.clicked.connect(self.clear_conversation)
+
+ button_layout.addWidget(send_button)
+ button_layout.addWidget(clear_button)
+ button_layout.addStretch()
+
+ input_layout.addWidget(input_label)
+ input_layout.addWidget(self.input_edit)
+ input_layout.addLayout(button_layout)
+
+ input_widget.setLayout(input_layout)
+ parent.addWidget(input_widget)
+
+ def send_message(self):
+ """发送消息到DeepSeek API(流式输出)"""
+ if not self.api_key:
+ QMessageBox.warning(self, "警告", "请先设置API密钥")
+ self.show_api_key_dialog()
+ return
+
+ message = self.input_edit.toPlainText().strip()
+ if not message:
+ QMessageBox.warning(self, "警告", "请输入消息内容")
+ return
+
+ # 禁用发送按钮
+ self.input_edit.setEnabled(False)
+ self.status_label.setText("正在发送消息...")
+
+ try:
+ # 添加用户消息到对话历史
+ self.add_message_to_conversation("用户", message)
+
+ # 开始流式输出AI回复
+ self.start_streaming_response(message)
+
+ except Exception as e:
+ error_msg = f"发送消息失败: {str(e)}"
+ self.add_message_to_conversation("系统", error_msg)
+ self.status_label.setText("发送失败")
+ QMessageBox.critical(self, "错误", error_msg)
+
+ finally:
+ # 重新启用发送按钮
+ self.input_edit.setEnabled(True)
+
+ def start_streaming_response(self, message):
+ """开始流式输出AI回复"""
+ # 清空输入框
+ self.input_edit.clear()
+
+ # 开始流式输出
+ self.current_streaming_content = ""
+ self.is_streaming = True
+ self.status_label.setText("正在接收AI回复...")
+
+ # 添加AI助手的初始消息占位符
+ self.add_streaming_message_start()
+
+ # 在新线程中执行流式请求
+ import threading
+ self.streaming_thread = threading.Thread(target=self.call_deepseek_api_stream, args=(message,))
+ self.streaming_thread.daemon = True
+ self.streaming_thread.start()
+
+ # 启动定时器更新显示
+ self.streaming_timer = QTimer()
+ self.streaming_timer.timeout.connect(self.update_streaming_display)
+ self.streaming_timer.start(100) # 每100毫秒更新一次显示
+
+ def add_streaming_message_start(self):
+ """添加流式消息的开始部分"""
+ # 创建AI助手的消息占位符
+ self.streaming_message_id = f"ai_message_{len(self.conversation_history)}"
+
+ # 添加到对话历史
+ self.conversation_history.append({"sender": "AI助手", "message": "", "streaming": True})
+
+ # 在对话区域添加占位符
+ cursor = self.conversation_text.textCursor()
+ cursor.movePosition(QTextCursor.End)
+ self.conversation_text.setTextCursor(cursor)
+
+ # 添加AI助手的消息框架
+ self.conversation_text.insertHtml(
+ f''
+ f'
AI助手:正在思考...'
+ )
+
+ # 自动滚动到底部
+ self.conversation_text.ensureCursorVisible()
+
+ def update_streaming_display(self):
+ """更新流式显示"""
+ if hasattr(self, 'current_streaming_content') and self.current_streaming_content:
+ # 使用更简单的方法:重新构建整个对话历史
+ self.rebuild_conversation_display()
+
+ # 自动滚动到底部
+ self.conversation_text.ensureCursorVisible()
+
+ def rebuild_conversation_display(self):
+ """重新构建对话显示"""
+ html_content = ""
+
+ for msg in self.conversation_history:
+ sender = msg["sender"]
+ message = msg["message"]
+ is_streaming = msg.get("streaming", False)
+
+ # 根据发送者设置不同的样式
+ if sender == "用户":
+ bg_color = "#e3f2fd" if self.current_theme == "light" else "#2d3e50"
+ text_color = "#000" if self.current_theme == "light" else "#fff"
+ elif sender == "AI助手":
+ bg_color = "#f5f5f5" if self.current_theme == "light" else "#3d3d3d"
+ text_color = "#000" if self.current_theme == "light" else "#fff"
+ else: # 系统消息
+ bg_color = "#fff3cd" if self.current_theme == "light" else "#5d4e00"
+ text_color = "#856404" if self.current_theme == "light" else "#ffd700"
+
+ # 格式化消息内容
+ formatted_message = message.replace('\n', '
') if message else "正在思考..."
+
+ html_content += f'''
+
+ {sender}:
+ {formatted_message}
+
+ '''
+
+ # 设置HTML内容
+ self.conversation_text.setHtml(html_content)
+
+ def call_deepseek_api_stream(self, message):
+ """调用DeepSeek API(流式版本)"""
+ url = "https://api.deepseek.com/v1/chat/completions"
+
+ headers = {
+ "Content-Type": "application/json",
+ "Authorization": f"Bearer {self.api_key}"
+ }
+
+ # 构建对话历史
+ messages = [{"role": "user", "content": message}]
+
+ data = {
+ "model": "deepseek-chat",
+ "messages": messages,
+ "stream": True, # 启用流式输出
+ "temperature": 0.7,
+ "max_tokens": 2000
+ }
+
+ try:
+ response = requests.post(url, headers=headers, json=data, stream=True, timeout=30)
+
+ if response.status_code == 200:
+ for line in response.iter_lines():
+ if line:
+ line = line.decode('utf-8')
+ if line.startswith('data: '):
+ data_str = line[6:] # 去掉 'data: ' 前缀
+ if data_str == '[DONE]':
+ break
+
+ try:
+ data_obj = json.loads(data_str)
+ if 'choices' in data_obj and len(data_obj['choices']) > 0:
+ delta = data_obj['choices'][0].get('delta', {})
+ if 'content' in delta:
+ content = delta['content']
+ self.current_streaming_content += content
+ except json.JSONDecodeError:
+ pass
+ else:
+ error_msg = f"API调用失败: {response.status_code} - {response.text}"
+ self.current_streaming_content = f"错误: {error_msg}"
+
+ except Exception as e:
+ self.current_streaming_content = f"请求失败: {str(e)}"
+
+ finally:
+ # 流式输出结束
+ self.is_streaming = False
+ if hasattr(self, 'streaming_timer'):
+ self.streaming_timer.stop()
+
+ # 更新对话历史
+ if hasattr(self, 'current_streaming_content') and self.current_streaming_content:
+ # 更新对话历史中的消息
+ for msg in self.conversation_history:
+ if msg.get('streaming') and msg.get('sender') == 'AI助手':
+ msg['message'] = self.current_streaming_content
+ msg['streaming'] = False
+ break
+
+ # 使用信号槽机制安全地更新UI
+ self.streaming_finished.emit()
+ else:
+ self.status_label.setText("接收失败")
+
+ def call_deepseek_api(self, message):
+ """调用DeepSeek API(非流式版本,保留作为备用)"""
+ url = "https://api.deepseek.com/v1/chat/completions"
+
+ headers = {
+ "Content-Type": "application/json",
+ "Authorization": f"Bearer {self.api_key}"
+ }
+
+ # 构建对话历史
+ messages = [{"role": "user", "content": message}]
+
+ data = {
+ "model": "deepseek-chat",
+ "messages": messages,
+ "stream": False,
+ "temperature": 0.7,
+ "max_tokens": 2000
+ }
+
+ response = requests.post(url, headers=headers, json=data, timeout=30)
+
+ if response.status_code == 200:
+ result = response.json()
+ return result["choices"][0]["message"]["content"]
+ else:
+ error_msg = f"API调用失败: {response.status_code} - {response.text}"
+ raise Exception(error_msg)
+
+ def add_message_to_conversation(self, sender, message):
+ """添加消息到对话显示区域"""
+ # 添加到对话历史
+ self.conversation_history.append({"sender": sender, "message": message})
+
+ # 使用新的对话显示系统
+ self.rebuild_conversation_display()
+
+ # 自动滚动到底部
+ self.conversation_text.ensureCursorVisible()
+
+ def on_streaming_finished(self):
+ """流式输出完成处理"""
+ # 安全地更新UI(在主线程中执行)
+ self.rebuild_conversation_display()
+ self.status_label.setText("消息接收完成")
+
+ def clear_conversation(self):
+ """清空对话历史"""
+ reply = QMessageBox.question(self, "确认", "确定要清空对话历史吗?",
+ QMessageBox.Yes | QMessageBox.No)
+
+ if reply == QMessageBox.Yes:
+ self.conversation_history.clear()
+ self.rebuild_conversation_display()
+ self.status_label.setText("对话已清空")
+
+ def manage_api_key(self):
+ """管理API密钥"""
+ self.show_api_key_dialog()
+
+ def closeEvent(self, event):
+ """关闭事件处理"""
+ # 发出关闭信号
+ self.closed.emit()
+
+ if self.parent:
+ # 通知父窗口对话窗口已关闭
+ if hasattr(self.parent, 'on_deepseek_dialog_closed'):
+ self.parent.on_deepseek_dialog_closed()
+ event.accept()
\ No newline at end of file
diff --git a/src/word_main_window.py b/src/word_main_window.py
index fab2880..ff65c45 100644
--- a/src/word_main_window.py
+++ b/src/word_main_window.py
@@ -93,6 +93,9 @@ class WordStyleMainWindow(QMainWindow):
self.learning_window = None # 学习模式窗口引用
self.sync_from_learning = False # 从学习模式同步内容的标记
+ # DeepSeek对话窗口引用
+ self.deepseek_dialog = None # DeepSeek对话窗口引用
+
# 统一文档内容管理
self.unified_document_content = "" # 统一文档内容
self.last_edit_mode = "typing" # 上次编辑模式
@@ -804,6 +807,12 @@ class WordStyleMainWindow(QMainWindow):
# 引用菜单
reference_menu = menubar.addMenu('引用(R)')
+ # DeepSeek AI对话功能
+ self.deepseek_dialog_action = QAction('DeepSeek AI对话', self)
+ self.deepseek_dialog_action.setShortcut('Ctrl+D')
+ self.deepseek_dialog_action.triggered.connect(self.open_deepseek_dialog)
+ reference_menu.addAction(self.deepseek_dialog_action)
+
# 邮件菜单
mail_menu = menubar.addMenu('邮件(M)')
@@ -2374,6 +2383,46 @@ class WordStyleMainWindow(QMainWindow):
self.status_bar.showMessage("学习模式窗口已关闭", 2000)
+ def open_deepseek_dialog(self):
+ """打开DeepSeek AI对话窗口"""
+ try:
+ from deepseek_dialog_window import DeepSeekDialogWindow
+
+ # 检查是否已存在对话窗口,如果存在则激活
+ if self.deepseek_dialog and self.deepseek_dialog.isVisible():
+ self.deepseek_dialog.activateWindow()
+ self.deepseek_dialog.raise_()
+ return
+
+ # 创建DeepSeek对话窗口
+ self.deepseek_dialog = DeepSeekDialogWindow(self)
+
+ # 连接对话窗口的关闭信号
+ self.deepseek_dialog.closed.connect(self.on_deepseek_dialog_closed)
+
+ # 显示对话窗口
+ self.deepseek_dialog.show()
+
+ # 更新菜单状态
+ self.deepseek_dialog_action.setChecked(True)
+
+ self.status_bar.showMessage("DeepSeek AI对话窗口已打开", 3000)
+
+ except ImportError as e:
+ QMessageBox.critical(self, "错误", f"无法加载DeepSeek对话窗口:\n{str(e)}")
+ except Exception as e:
+ QMessageBox.critical(self, "错误", f"打开DeepSeek对话窗口时出错:\n{str(e)}")
+
+ def on_deepseek_dialog_closed(self):
+ """DeepSeek对话窗口关闭时的回调"""
+ # 重置菜单状态
+ self.deepseek_dialog_action.setChecked(False)
+
+ # 清除对话窗口引用
+ self.deepseek_dialog = None
+
+ self.status_bar.showMessage("DeepSeek AI对话窗口已关闭", 2000)
+
def on_learning_content_changed(self, new_content, position):
"""学习模式内容变化时的回调 - 只在末尾追加新内容"""
# 设置同步标记,防止递归调用