update #111

Merged
p9o3yklam merged 11 commits from main into huangjiunyuna 3 months ago

4
.gitignore vendored

@ -198,6 +198,10 @@ temp/
*.orig
# Project specific
resources/config/deepseek_api.json
*.key
*.secret
config/*.json
# Documentation folder
doc/

@ -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'<div id="{self.streaming_message_id}" style="background-color: #f5f5f5; padding: 10px; margin: 5px; border-radius: 10px;">'
f'<b>AI助手:</b><br><span style="color: #666;">正在思考...</span>'
)
# 自动滚动到底部
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', '<br>') if message else "正在思考..."
html_content += f'''
<div style="background-color: {bg_color}; color: {text_color}; padding: 10px; margin: 5px; border-radius: 10px;">
<b>{sender}:</b><br>
{formatted_message}
</div>
'''
# 设置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()

@ -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()

@ -0,0 +1,439 @@
#!/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() # 刷新请求信号
insert_requested = pyqtSignal(str) # 插入请求信号,传递要插入的文本
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()
# 插入按钮
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)
# 设置主布局
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, QPushButton#insertButton {{
background-color: {colors['accent']};
color: white;
border: none;
border-radius: 6px;
padding: 6px 16px;
font-size: 11px;
font-weight: 500;
}}
QPushButton#refreshButton:hover, QPushButton#insertButton: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, QPushButton#insertButton {{
background-color: {colors['accent']};
color: white;
border: none;
border-radius: 6px;
padding: 6px 16px;
font-size: 11px;
font-weight: 500;
}}
QPushButton#refreshButton:hover, QPushButton#insertButton: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 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:
# 尝试获取古诗词
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()

@ -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)

@ -0,0 +1,620 @@
# -*- 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(360, 280) # 调整窗口尺寸使其更紧凑
# 创建主框架,用于实现圆角和阴影效果
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(6) # 减小间距使布局更紧凑
# 设置最小尺寸策略
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()
# 添加一个小的固定空间,使关闭按钮向左移动
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.setFrameShape(QFrame.HLine)
separator.setObjectName("separator")
content_layout.addWidget(separator)
# 天气图标和温度显示区域
weather_display_layout = QHBoxLayout()
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", 24)) # 稍微减小字体大小
self.weather_icon_label.setAlignment(Qt.AlignCenter)
self.weather_icon_label.setFixedSize(50, 50) # 减小尺寸
weather_display_layout.addWidget(self.weather_icon_label)
# 温度和城市信息
temp_city_layout = QVBoxLayout()
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", 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", 11)) # 稍微减小字体大小
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", 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(6) # 减小间距使布局更紧凑
details_layout.setContentsMargins(2, 2, 2, 2) # 减小内边距
self.humidity_label = QLabel("湿度: 45%")
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", 10)) # 稍微减小字体大小
self.wind_label.setObjectName("detailLabel")
details_layout.addWidget(self.wind_label)
content_layout.addLayout(details_layout)
# 城市选择区域
city_layout = QHBoxLayout()
city_layout.setSpacing(6) # 减小间距使布局更紧凑
city_layout.setContentsMargins(0, 0, 0, 0)
self.city_combo = QComboBox()
self.city_combo.setObjectName("cityCombo")
# 添加所有省会城市,与主窗口保持一致
self.city_combo.addItems([
'自动定位',
'北京', '上海', '广州', '深圳', '杭州', '南京', '武汉', '成都', '西安', # 一线城市
'天津', '重庆', '苏州', '青岛', '大连', '宁波', '厦门', '无锡', '佛山', # 新一线城市
'石家庄', '太原', '呼和浩特', '沈阳', '长春', '哈尔滨', # 东北华北
'合肥', '福州', '南昌', '济南', '郑州', '长沙', '南宁', '海口', # 华东华中华南
'贵阳', '昆明', '拉萨', '兰州', '西宁', '银川', '乌鲁木齐' # 西南西北
])
self.city_combo.setFixedWidth(100) # 减小城市选择框宽度使布局更紧凑
city_layout.addWidget(self.city_combo)
city_layout.addStretch()
content_layout.addLayout(city_layout)
# 按钮区域
button_layout = QHBoxLayout()
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)
# 添加弹性空间
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: 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#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: 4px 6px;
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: 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, 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 7px;
font-size: 11px;
font-weight: 500;
min-height: 24px;
}}
QComboBox#cityCombo:hover {{
border-color: {colors['accent']};
}}
QComboBox#cityCombo::drop-down {{
border: none;
width: 14px;
}}
QComboBox#cityCombo::down-arrow {{
image: none;
border-left: 2px solid transparent;
border-right: 2px 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: 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#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: 4px 6px;
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: 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, 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 7px;
font-size: 11px;
font-weight: 500;
min-height: 24px;
}}
QComboBox#cityCombo:hover {{
border-color: {colors['accent']};
}}
QComboBox#cityCombo::drop-down {{
border: none;
width: 14px;
}}
QComboBox#cityCombo::down-arrow {{
image: none;
border-left: 2px solid transparent;
border-right: 2px solid transparent;
border-top: 5px solid {colors['text']};
}}
""")
def on_theme_changed(self, is_dark):
"""主题切换槽函数"""
self.apply_theme()
def mousePressEvent(self, event):
"""鼠标按下事件,用于拖拽"""
if event.button() == Qt.LeftButton:
# 检查是否点击在标题栏区域
if event.pos().y() <= 40: # 假设标题栏高度为40像素
self.is_dragging = True
self.drag_position = event.globalPos() - self.frameGeometry().topLeft()
event.accept()
def mouseMoveEvent(self, event):
"""鼠标移动事件,用于拖拽"""
if self.is_dragging and event.buttons() == Qt.LeftButton:
self.move(event.globalPos() - self.drag_position)
event.accept()
def mouseReleaseEvent(self, event):
"""鼠标释放事件"""
self.is_dragging = False
def update_weather(self, weather_data):
"""更新天气信息"""
self.weather_data = weather_data
if weather_data and 'error' not in weather_data:
# 获取天气数据
city = weather_data.get('city', '未知城市')
current_data = weather_data.get('current', {})
temp = current_data.get('temp', 'N/A')
desc = current_data.get('weather', 'N/A')
humidity = current_data.get('humidity', 'N/A')
wind_scale = current_data.get('wind_scale', 'N/A')
# 更新显示
self.city_label.setText(city)
self.temperature_label.setText(f"{temp}°C")
self.weather_desc_label.setText(desc)
self.humidity_label.setText(f"湿度: {humidity}%")
self.wind_label.setText(f"风速: {wind_scale}")
# 更新天气图标
emoji = self.get_weather_emoji(desc)
self.weather_icon_label.setText(emoji)
else:
# 显示错误信息
self.city_label.setText("获取失败")
self.temperature_label.setText("--°C")
self.weather_desc_label.setText("无法获取天气数据")
self.humidity_label.setText("湿度: --%")
self.wind_label.setText("风速: --级")
self.weather_icon_label.setText("")
def get_weather_emoji(self, weather_desc):
"""根据天气描述返回对应的emoji"""
if not weather_desc:
return "🌞"
weather_desc_lower = weather_desc.lower()
# 天气图标映射
weather_emoji_map = {
'': '🌞',
'多云': '',
'': '☁️',
'': '🌧️',
'小雨': '🌦️',
'中雨': '🌧️',
'大雨': '⛈️',
'暴雨': '🌩️',
'': '❄️',
'小雪': '🌨️',
'中雪': '❄️',
'大雪': '☃️',
'': '🌫️',
'': '😷',
'': '💨',
'大风': '🌪️',
'': '⛈️',
'雷阵雨': '⛈️',
'冰雹': '🌨️',
'沙尘': '🌪️'
}
for key, emoji in weather_emoji_map.items():
if key in weather_desc_lower:
return emoji
# 默认返回晴天图标
return '🌞'
def on_refresh_clicked(self):
"""刷新按钮点击事件"""
self.refresh_requested.emit()
def on_city_changed(self, city_name):
"""城市选择变化事件"""
# 发射城市变化信号,通知主窗口更新天气
if hasattr(self.parent(), 'on_city_changed'):
self.parent().on_city_changed(city_name)
def set_current_city(self, city_name):
"""设置当前城市"""
# 阻止信号发射,避免循环调用
self.city_combo.blockSignals(True)
index = self.city_combo.findText(city_name)
if index >= 0:
self.city_combo.setCurrentIndex(index)
self.city_combo.blockSignals(False)
def close_window(self):
"""关闭窗口 - 只是隐藏而不是销毁"""
try:
self.closed.emit()
self.hide() # 隐藏窗口而不是销毁
except Exception as e:
print(f"Error in close_window: {e}")
def show_detailed_weather(self):
"""显示详细天气信息对话框"""
# 检查是否有天气数据
if not self.weather_data or 'error' in self.weather_data:
from PyQt5.QtWidgets import QMessageBox
QMessageBox.information(self, "天气信息", "暂无天气数据,请先刷新天气信息")
return
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit
weather_data = self.weather_data
# 创建对话框
dialog = QDialog(self)
dialog.setWindowTitle("详细天气")
dialog.setMinimumWidth(400)
layout = QVBoxLayout()
# 城市信息
city_label = QLabel(f"<h2>{weather_data.get('city', '未知城市')}</h2>")
layout.addWidget(city_label)
# 当前天气信息
current_layout = QVBoxLayout()
current_layout.addWidget(QLabel("<b>当前天气:</b>"))
# 获取温度信息,支持嵌套结构
current_data = weather_data.get('current', {})
temp = current_data.get('temp', 'N/A')
if temp != 'N/A' and isinstance(temp, str):
temp = float(temp) if temp.replace('.', '').isdigit() else temp
# 从预报数据中获取最高和最低气温
temp_range = ""
temp_max = 'N/A'
temp_min = 'N/A'
if 'forecast' in weather_data and weather_data['forecast']:
forecast_data = weather_data['forecast'][0] # 今天的预报
if isinstance(forecast_data, dict):
temp_max = forecast_data.get('temp_max', 'N/A')
temp_min = forecast_data.get('temp_min', 'N/A')
if temp_max != 'N/A' and temp_min != 'N/A':
temp_range = f" ({temp_min}°C~{temp_max}°C)"
current_info = f"""
当前温度: {temp}°C{temp_range}
最高气温: {temp_max}°C
最低气温: {temp_min}°C
天气状况: {current_data.get('weather', 'N/A')}
"""
current_text = QTextEdit()
current_text.setPlainText(current_info.strip())
current_text.setReadOnly(True)
current_layout.addWidget(current_text)
layout.addLayout(current_layout)
# 生活提示信息(替换原来的天气预报)
life_tips = weather_data.get('life_tips', [])
if life_tips:
tips_layout = QVBoxLayout()
tips_layout.addWidget(QLabel("<b>生活提示:</b>"))
tips_text = QTextEdit()
tips_info = ""
for tip in life_tips:
tips_info += f"{tip}\n"
tips_text.setPlainText(tips_info.strip())
tips_text.setReadOnly(True)
tips_layout.addWidget(tips_text)
layout.addLayout(tips_layout)
# 按钮
button_layout = QHBoxLayout()
refresh_button = QPushButton("刷新")
refresh_button.clicked.connect(lambda: self.refresh_weather_and_close(dialog))
close_button = QPushButton("关闭")
close_button.clicked.connect(dialog.close)
button_layout.addWidget(refresh_button)
button_layout.addWidget(close_button)
layout.addLayout(button_layout)
dialog.setLayout(layout)
dialog.exec_()
def refresh_weather_and_close(self, dialog):
"""刷新天气并关闭对话框"""
self.on_refresh_clicked()
dialog.close()
def closeEvent(self, event):
"""窗口关闭事件 - 只是隐藏而不是销毁"""
self.closed.emit()
self.hide() # 隐藏窗口而不是销毁
event.ignore()
def show_at_position(self, x, y):
"""在指定位置显示窗口"""
self.move(x, y)
self.show()
def update_position(self, x, y):
"""更新窗口位置"""
self.move(x, y)

@ -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()
@ -698,8 +699,29 @@ 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("切换天气悬浮窗口")
# 每日谏言悬浮窗口按钮
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("切换每日谏言悬浮窗口")
# 日历悬浮窗口按钮
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()
@ -852,6 +874,10 @@ class WordRibbon(QFrame):
"""刷新天气按钮点击处理"""
pass
def on_floating_weather(self):
"""悬浮窗口按钮点击处理"""
pass
def on_city_changed(self, city):
"""城市选择变化处理"""
pass

@ -19,6 +19,9 @@ 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.quote_floating_widget import QuoteFloatingWidget
from ui.calendar_floating_widget import CalendarFloatingWidget
# 导入主题管理器
from ui.theme_manager import theme_manager
@ -90,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" # 上次编辑模式
@ -108,6 +114,23 @@ 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.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)
@ -134,6 +157,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()
@ -440,8 +471,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 +729,21 @@ 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)
# 每日谏言悬浮窗口切换动作
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)')
@ -755,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)')
@ -764,6 +822,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)')
@ -928,6 +997,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'):
@ -1602,7 +1675,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 == '自动定位':
@ -1623,9 +1707,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 +1811,87 @@ 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_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_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):
"""将古诗句插入到光标位置"""
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 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):
"""切换天气工具组显示"""
if checked:
@ -1755,6 +1926,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):
"""处理名言获取成功"""
@ -2206,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):
"""学习模式内容变化时的回调 - 只在末尾追加新内容"""
# 设置同步标记,防止递归调用
@ -2553,6 +2770,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):
"""显示关于对话框"""
# 创建自定义对话框

@ -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()

@ -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)
Loading…
Cancel
Save