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: -- 2.34.1 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']}; // 适度增加箭头大小 }} """) -- 2.34.1 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): """处理名言获取成功""" -- 2.34.1 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: -- 2.34.1 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): """切换天气工具组显示""" -- 2.34.1 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 == '自动定位': -- 2.34.1 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) -- 2.34.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): """学习模式内容变化时的回调 - 只在末尾追加新内容""" # 设置同步标记,防止递归调用 -- 2.34.1