final #96

Merged
p9o3yklam merged 10 commits from maziang into main 3 months ago

@ -4,21 +4,25 @@ import os
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTextEdit, QLabel, QFrame, QMenuBar,
QAction, QFileDialog, QMessageBox, QApplication,
QSplitter, QScrollArea, QStatusBar, QProgressBar)
QSplitter, QScrollArea, QStatusBar, QProgressBar, QTextBrowser, QSizePolicy,
QListWidget, QListWidgetItem, QDialog, QGraphicsScene, QGraphicsView)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QRect
from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor
from src.ui.components import CustomTitleBar, TextDisplayWidget
from src.typing_logic import TypingLogic
from src.file_parser import FileParser
from src.ui.theme_manager import theme_manager
# 修复导入路径
from ui.components import CustomTitleBar, TextDisplayWidget
from typing_logic import TypingLogic
from file_parser import FileParser
from ui.theme_manager import theme_manager
import tempfile
import hashlib
class LearningModeWindow(QMainWindow):
# 定义内容变化信号
content_changed = pyqtSignal(str, int) # 参数:内容,位置
# 定义关闭信号
closed = pyqtSignal()
def __init__(self, parent=None, imported_content="", current_position=0):
def __init__(self, parent=None, imported_content="", current_position=0, image_data=None, image_positions=None):
"""
学习模式窗口
- 顶部显示UI.png图片
@ -29,13 +33,18 @@ class LearningModeWindow(QMainWindow):
parent: 父窗口
imported_content: 从主窗口传递的导入内容
current_position: 当前学习进度位置
image_data: 图片数据字典 {文件名: 二进制数据}
image_positions: 图片位置信息列表
"""
super().__init__(parent)
self.parent_window = parent
self.imported_content = imported_content
self.current_position = current_position
self.image_data = image_data or {}
self.image_positions = image_positions or []
self.typing_logic = None
self.is_loading_file = False
self.extracted_images = [] # 用于存储提取的图片数据
# 初始化UI
self.initUI()
@ -60,6 +69,12 @@ class LearningModeWindow(QMainWindow):
# 重置打字逻辑
if self.typing_logic:
self.typing_logic.reset(self.imported_content)
# 设置图片数据到打字逻辑
if self.image_data:
self.typing_logic.set_image_data(self.image_data)
if self.image_positions:
self.typing_logic.set_image_positions(self.image_positions)
# 显示已学习的内容
display_text = self.imported_content[:self.current_position]
@ -144,38 +159,48 @@ class LearningModeWindow(QMainWindow):
ui_image_path = os.path.join(os.path.dirname(__file__), 'ui', 'UI.png')
if os.path.exists(ui_image_path):
pixmap = QPixmap(ui_image_path)
# 保存原始图片尺寸
self.original_pixmap = pixmap
# 设置图片完全铺满标签
self.image_label.setPixmap(pixmap)
self.image_label.setScaledContents(True) # 关键:让图片缩放填充整个标签
# 设置图片标签的尺寸策略,使其可以扩展
from PyQt5.QtWidgets import QSizePolicy
self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# 设置图片区域的最小高度为图片高度的1/3确保图片可见
min_height = max(200, pixmap.height() // 3)
self.image_label.setMinimumHeight(min_height)
# 重新设置窗口大小以适配图片
self.resize(pixmap.width(), self.height())
if not pixmap.isNull():
# 保存原始图片用于缩放计算
self.original_pixmap = pixmap
# 设置图片到标签
self.image_label.setPixmap(pixmap)
self.image_label.setScaledContents(True) # 关键:让图片缩放填充整个标签
# 设置大小策略
self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# 计算合适的最小高度(保持图片比例)
window_width = 900 # 默认窗口宽度
original_width = pixmap.width()
original_height = pixmap.height()
if original_width > 0:
min_height = int(window_width * original_height / original_width)
self.image_label.setMinimumHeight(min_height)
else:
self.image_label.setMinimumHeight(200)
else:
self.image_label.setText("UI图片加载失败")
self.image_label.setStyleSheet("""
QLabel {
background-color: #f8f9fa;
color: #666666;
font-size: 14px;
qproperty-alignment: AlignCenter;
}
""")
else:
self.image_label.setText("UI图片未找到")
self.image_label.setStyleSheet("""
QLabel {
background-color: #f8f9fa;
border: none;
color: #666666;
font-size: 14px;
padding: 20px;
qproperty-alignment: AlignCenter;
}
""")
self.image_label.setMinimumHeight(200)
# 直接添加图片标签到主布局,不使用滚动区域
# 添加到主布局
main_layout.addWidget(self.image_label)
def resizeEvent(self, event):
@ -210,6 +235,7 @@ class LearningModeWindow(QMainWindow):
创建输入区域
- 创建文本显示组件
- 设置与主系统相同的样式
- 创建图片列表区域
"""
# 创建文本显示组件(复用主系统的组件)
self.text_display_widget = TextDisplayWidget(self)
@ -226,7 +252,40 @@ class LearningModeWindow(QMainWindow):
# 连接文本变化信号
self.text_display_widget.text_display.textChanged.connect(self.on_text_changed)
main_layout.addWidget(self.text_display_widget, 1) # 占据剩余空间
# 创建图片显示区域
self.image_list_widget = QListWidget()
self.image_list_widget.setMaximumHeight(150)
self.image_list_widget.setStyleSheet("""
QListWidget {
background-color: #f8f8f8;
border: 1px solid #d0d0d0;
border-radius: 4px;
font-size: 11px;
}
QListWidget::item {
padding: 5px;
border-bottom: 1px solid #e0e0e0;
}
QListWidget::item:selected {
background-color: #e3f2fd;
color: #1976d2;
}
""")
self.image_list_widget.setVisible(False) # 默认隐藏
self.image_list_widget.itemDoubleClicked.connect(self.on_image_item_double_clicked)
# 创建布局容器
input_container = QWidget()
input_layout = QVBoxLayout()
input_layout.setContentsMargins(0, 0, 0, 0)
input_layout.setSpacing(5)
input_layout.addWidget(self.text_display_widget, 1) # 文本显示区域占据剩余空间
input_layout.addWidget(self.image_list_widget) # 图片列表区域
input_container.setLayout(input_layout)
main_layout.addWidget(input_container, 1) # 占据剩余空间
def create_menu_bar(self):
"""
@ -301,38 +360,93 @@ class LearningModeWindow(QMainWindow):
)
if file_path:
self.is_loading_file = True
try:
self.is_loading_file = True
# 使用文件解析器
parser = FileParser()
content = parser.parse_file(file_path)
# 获取文件扩展名
_, ext = os.path.splitext(file_path)
ext = ext.lower()
if content:
# 存储导入的内容
self.imported_content = content
self.current_position = 0
# 重置打字逻辑
if self.typing_logic:
self.typing_logic.reset(content)
# 清空文本显示
self.text_display_widget.text_display.clear()
# 更新状态
self.status_label.setText(f"已导入: {os.path.basename(file_path)}")
self.progress_label.setText(f"进度: 0% (0/{len(content)} 字符)")
# 显示成功消息
QMessageBox.information(self, "导入成功",
f"文件导入成功!\n文件: {os.path.basename(file_path)}\n字符数: {len(content)}\n\n开始打字以显示学习内容。")
# 对于docx文件直接解析而不转换为txt
if ext == '.docx':
# 直接解析docx文件内容
content = FileParser.parse_docx(file_path)
# 提取图片数据
images = FileParser.extract_images_from_docx(file_path)
else:
QMessageBox.warning(self, "导入失败", "无法解析文件内容,请检查文件格式。")
# 其他文件类型使用原来的转换方法
result = FileParser.parse_and_convert_to_txt(file_path)
content = result['content']
images = result.get('images', [])
if not content:
QMessageBox.warning(self, "导入失败", "文件内容为空或解析失败!")
return
# 保存导入的内容
self.imported_content = content
self.current_position = 0
# 保存提取的图片数据
self.extracted_images = images
# 设置打字逻辑
if self.typing_logic:
self.typing_logic.reset(content)
# 如果有图片,设置图片数据到打字逻辑
if images:
image_data_dict = {}
image_positions = []
# 为每张图片生成位置信息 - 改进位置计算逻辑
for i, (filename, image_data) in enumerate(images):
image_data_dict[filename] = image_data
# 改进图片位置计算,确保图片能在用户早期打字时显示
content_length = len(content)
if content_length == 0:
content_length = 1000 # 备用长度
if len(images) == 1:
# 只有一张图片放在文档开始位置附近前10%),确保用户能快速看到
image_pos = max(10, content_length // 10)
else:
# 多张图片:前几张放在较前位置,确保用户能看到
if i < 3:
# 前3张图片放在文档前30%
segment = content_length // 3
image_pos = max(10, segment * (i + 1) // 4)
else:
# 其余图片均匀分布
remaining_start = content_length // 2
remaining_index = i - 3
remaining_count = len(images) - 3
if remaining_count > 0:
segment = (content_length - remaining_start) // (remaining_count + 1)
image_pos = remaining_start + segment * (remaining_index + 1)
else:
image_pos = content_length // 2
image_positions.append({
'start_pos': image_pos,
'end_pos': image_pos + 50, # 图片占位符长度
'filename': filename
})
# 设置图片数据到打字逻辑
self.typing_logic.set_image_data(image_data_dict)
self.typing_logic.set_image_positions(image_positions)
# 显示图片列表
self.display_image_list(images)
# 显示初始内容(空)
self.text_display_widget.text_display.clear()
self.status_label.setText("已导入文件,请开始打字学习...")
self.progress_label.setText("进度: 0%")
except Exception as e:
QMessageBox.critical(self, "导入错误", f"导入文件时出错:\n{str(e)}")
QMessageBox.critical(self, "导入错误", f"导入文件时发生错误:\n{str(e)}")
finally:
self.is_loading_file = False
@ -343,6 +457,7 @@ class LearningModeWindow(QMainWindow):
- 根据导入的内容逐步显示
- 更新学习进度
- 同步内容到打字模式
- 处理图片插入
"""
# 如果正在加载文件,跳过处理
if self.is_loading_file:
@ -398,6 +513,8 @@ class LearningModeWindow(QMainWindow):
f"进度: {progress:.1f}% ({self.current_position}/{len(self.imported_content)} 字符)"
)
# 只在用户新输入的字符上同步到打字模式
if self.parent_window and hasattr(self.parent_window, 'text_edit'):
# 获取用户这一轮新输入的字符(与上一轮相比的新内容)
@ -414,6 +531,8 @@ class LearningModeWindow(QMainWindow):
else:
self.status_label.setText("继续输入以显示更多内容...")
def show_about(self):
"""
显示关于对话框
@ -424,7 +543,8 @@ class LearningModeWindow(QMainWindow):
"• 顶部显示UI界面图片\n"
"• 下方为打字输入区域\n"
"• 导入文件后逐步显示内容\n"
"• 实时显示学习进度\n\n"
"• 实时显示学习进度\n"
"• 支持图片显示\n\n"
"使用方法:\n"
"1. 点击'文件'->'导入文件'选择学习材料\n"
"2. 在下方文本区域开始打字\n"
@ -452,4 +572,152 @@ class LearningModeWindow(QMainWindow):
if event.key() == Qt.Key_Escape:
self.close()
else:
super().keyPressEvent(event)
super().keyPressEvent(event)
def display_image_list(self, images):
"""
显示图片列表
"""
try:
# 清空之前的图片列表
self.image_list_widget.clear()
# 如果没有图片,隐藏图片列表区域
if not images:
self.image_list_widget.setVisible(False)
return
# 显示图片列表区域
self.image_list_widget.setVisible(True)
# 添加图片项到列表
for index, (filename, image_data) in enumerate(images):
# 创建缩略图
pixmap = QPixmap()
if pixmap.loadFromData(image_data):
# 创建缩略图
thumbnail = pixmap.scaled(60, 60, Qt.KeepAspectRatio, Qt.SmoothTransformation)
# 创建列表项
item = QListWidgetItem()
item.setIcon(QIcon(thumbnail))
item.setText(f"{filename} ({pixmap.width()}x{pixmap.height()})")
item.setData(Qt.UserRole, index) # 保存图片索引
self.image_list_widget.addItem(item)
else:
# 如果无法加载图片,显示默认文本
item = QListWidgetItem(f"{filename} (无法预览)")
item.setData(Qt.UserRole, index)
self.image_list_widget.addItem(item)
# 更新状态栏
self.status_label.setText(f"已提取 {len(images)} 张图片,双击查看大图")
except Exception as e:
self.status_label.setText(f"显示图片列表失败: {str(e)}")
def on_image_item_double_clicked(self, item):
"""
双击图片项时显示大图
"""
try:
# 获取图片索引
index = item.data(Qt.UserRole)
if 0 <= index < len(self.extracted_images):
image_filename, image_data = self.extracted_images[index]
self.show_image_viewer(image_filename, image_data)
except Exception as e:
self.status_label.setText(f"显示图片失败: {str(e)}")
def show_image_viewer(self, filename, image_data):
"""
显示图片查看器 - 支持缩放功能
"""
try:
# 创建自定义图片查看窗口
viewer = QDialog(self)
viewer.setWindowTitle(f"图片查看 - {filename}")
viewer.setModal(False)
# 设置窗口标志,保留标题栏以便用户可以移动和调整大小
viewer.setWindowFlags(Qt.Tool | Qt.WindowCloseButtonHint | Qt.WindowMinMaxButtonsHint)
# 设置窗口背景为黑色
viewer.setStyleSheet("""
QDialog {
background-color: #000000;
}
""")
# 创建场景和视图
scene = QGraphicsScene(viewer)
view = QGraphicsView(scene)
view.setStyleSheet("border: none;") # 移除视图边框
# 设置视图为可交互的,并启用滚动条
view.setDragMode(QGraphicsView.ScrollHandDrag)
view.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
# 创建布局
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(view)
viewer.setLayout(layout)
# 加载图片
pixmap = QPixmap()
if not pixmap.loadFromData(image_data):
self.status_label.setText(f"加载图片失败: {filename}")
return
# 将图片添加到场景
scene.addPixmap(pixmap)
# 设置视图大小和位置
if self:
parent_geometry = self.geometry()
screen_geometry = QApplication.primaryScreen().geometry()
# 设置窗口宽度与主窗口相同高度为屏幕高度的40%
window_width = parent_geometry.width()
window_height = int(screen_geometry.height() * 0.4)
# 计算位置:显示在主窗口正上方
x = parent_geometry.x()
y = parent_geometry.y() - window_height
# 确保不会超出屏幕边界
if y < screen_geometry.top():
y = parent_geometry.y() + 50 # 如果上方空间不足,显示在下方
# 调整宽度确保不超出屏幕
if x + window_width > screen_geometry.right():
window_width = screen_geometry.right() - x
viewer.setGeometry(x, y, window_width, window_height)
viewer.show()
# 设置视图适应图片大小
view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio)
# 重写视图的滚轮事件以支持缩放
def wheelEvent(event):
factor = 1.2
if event.angleDelta().y() > 0:
view.scale(factor, factor)
else:
view.scale(1.0/factor, 1.0/factor)
view.wheelEvent = wheelEvent
# 添加双击重置视图功能
def mouseDoubleClickEvent(event):
view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio)
view.mouseDoubleClickEvent = mouseDoubleClickEvent
except Exception as e:
self.status_label.setText(f"创建图片查看器失败: {str(e)}")
import traceback
traceback.print_exc()

@ -51,7 +51,61 @@ class NetworkService:
# 5. 返回天气信息字典
return formatted_weather
else:
# 模拟天气数据无API密钥时
# 当没有API密钥时使用免费的天气API获取真实数据
# 首先尝试获取城市ID需要映射城市名到ID
city_id_map = {
"Beijing": "101010100",
"Shanghai": "101020100",
"Tianjin": "101030100",
"Chongqing": "101040100",
"Hong Kong": "101320101",
"Macau": "101330101"
}
# 尝试映射英文城市名到ID
city_id = city_id_map.get(city)
# 如果找不到映射,尝试直接使用城市名
if not city_id:
# 对于中国主要城市,直接使用拼音映射
city_pinyin_map = {
"Beijing": "北京",
"Shanghai": "上海",
"Tianjin": "天津",
"Chongqing": "重庆"
}
chinese_city = city_pinyin_map.get(city, city)
# 使用免费天气API
try:
# 使用和风天气免费API的替代方案 - sojson天气API
weather_url = f"http://t.weather.sojson.com/api/weather/city/101010100" # 默认北京
weather_response = self.session.get(weather_url, timeout=5, verify=False)
weather_data = weather_response.json()
if weather_data.get("status") == 200:
# 解析天气数据
current_data = weather_data.get("data", {})
wendu = current_data.get("wendu", "N/A")
shidu = current_data.get("shidu", "N/A")
forecast = current_data.get("forecast", [])
# 获取第一个预报项作为当前天气
current_weather = forecast[0] if forecast else {}
weather_type = current_weather.get("type", "")
formatted_weather = {
"city": city,
"temperature": float(wendu) if wendu != "N/A" else 20,
"description": weather_type,
"humidity": shidu.replace("%", "") if shidu != "N/A" else "60",
"wind_speed": "3.5" # 默认风速
}
return formatted_weather
except Exception as e:
print(f"获取免费天气数据时出错: {e}")
# 如果以上都失败,返回默认数据
return {
"city": city,
"temperature": 20,

@ -13,6 +13,9 @@ from PyQt5.QtWidgets import (
from PyQt5.QtCore import QDate, Qt, pyqtSignal
from PyQt5.QtGui import QFont
# 导入主题管理器
from .theme_manager import theme_manager
class CalendarWidget(QWidget):
"""日历组件类"""
@ -24,6 +27,7 @@ class CalendarWidget(QWidget):
super().__init__(parent)
self.setup_ui()
self.setup_connections()
self.setup_theme()
def setup_ui(self):
"""设置UI界面"""
@ -266,6 +270,295 @@ class CalendarWidget(QWidget):
date = QDate.fromString(date, "yyyy-MM-dd")
self.calendar.setSelectedDate(date)
self.update_date_label(date)
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()
if is_dark:
# 深色主题样式
self.setStyleSheet("""
QWidget {
background-color: #2c2c2e;
color: #f0f0f0;
}
""")
# 更新日历控件样式
self.calendar.setStyleSheet("""
QCalendarWidget {
background-color: #2c2c2e;
border: 1px solid #404040;
border-radius: 4px;
}
QCalendarWidget QToolButton {
height: 30px;
width: 80px;
color: #f0f0f0;
font-size: 12px;
font-weight: bold;
background-color: #3a3a3c;
border: 1px solid #4a4a4c;
border-radius: 4px;
}
QCalendarWidget QToolButton:hover {
background-color: #4a4a4c;
}
QCalendarWidget QMenu {
width: 150px;
left: 20px;
color: #f0f0f0;
font-size: 12px;
background-color: #3a3a3c;
border: 1px solid #4a4a4c;
}
QCalendarWidget QSpinBox {
width: 80px;
font-size: 12px;
background-color: #3a3a3c;
selection-background-color: #0a84ff;
selection-color: #ffffff;
border: 1px solid #4a4a4c;
border-radius: 4px;
color: #f0f0f0;
}
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: #3a3a3c;
}
QCalendarWidget QAbstractItemView:enabled {
font-size: 12px;
selection-background-color: #0a84ff;
selection-color: #ffffff;
background-color: #2c2c2e;
color: #f0f0f0;
}
QCalendarWidget QWidget#qt_calendar_navigationbar {
background-color: #3a3a3c;
}
""")
# 更新标签样式
self.date_label.setStyleSheet("QLabel { color: #a0a0a0; }")
# 更新按钮样式
self.close_btn.setStyleSheet("""
QPushButton {
background-color: #3a3a3c;
border: 1px solid #4a4a4c;
border-radius: 12px;
font-size: 16px;
font-weight: bold;
color: #f0f0f0;
}
QPushButton:hover {
background-color: #4a4a4c;
}
""")
self.today_btn.setStyleSheet("""
QPushButton {
background-color: #0a84ff;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
}
QPushButton:hover {
background-color: #0066cc;
}
""")
self.clear_btn.setStyleSheet("""
QPushButton {
background-color: #3a3a3c;
color: #f0f0f0;
border: 1px solid #4a4a4c;
border-radius: 4px;
padding: 5px 10px;
}
QPushButton:hover {
background-color: #4a4a4c;
}
""")
self.insert_btn.setStyleSheet("""
QPushButton {
background-color: #32d74b;
color: #000000;
border: none;
border-radius: 4px;
padding: 5px 10px;
}
QPushButton:hover {
background-color: #24b334;
}
""")
else:
# 浅色主题样式
self.setStyleSheet("""
QWidget {
background-color: white;
color: #333333;
}
""")
# 更新日历控件样式
self.calendar.setStyleSheet("""
QCalendarWidget {
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
}
QCalendarWidget QToolButton {
height: 30px;
width: 80px;
color: #333;
font-size: 12px;
font-weight: bold;
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 4px;
}
QCalendarWidget QToolButton:hover {
background-color: #e0e0e0;
}
QCalendarWidget QMenu {
width: 150px;
left: 20px;
color: #333;
font-size: 12px;
background-color: white;
border: 1px solid #ccc;
}
QCalendarWidget QSpinBox {
width: 80px;
font-size: 12px;
background-color: #f0f0f0;
selection-background-color: #0078d7;
selection-color: white;
border: 1px solid #ccc;
border-radius: 4px;
color: #333;
}
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: #f0f0f0;
}
QCalendarWidget QAbstractItemView:enabled {
font-size: 12px;
selection-background-color: #0078d7;
selection-color: white;
background-color: white;
color: #333;
}
QCalendarWidget QWidget#qt_calendar_navigationbar {
background-color: #f8f8f8;
}
""")
# 更新标签样式
self.date_label.setStyleSheet("QLabel { color: #666; }")
# 更新按钮样式
self.close_btn.setStyleSheet("""
QPushButton {
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 12px;
font-size: 16px;
font-weight: bold;
color: #333;
}
QPushButton:hover {
background-color: #e0e0e0;
}
""")
self.today_btn.setStyleSheet("""
QPushButton {
background-color: #0078d7;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
}
QPushButton:hover {
background-color: #005a9e;
}
""")
self.clear_btn.setStyleSheet("""
QPushButton {
background-color: #f0f0f0;
color: #333;
border: 1px solid #ccc;
border-radius: 4px;
padding: 5px 10px;
}
QPushButton:hover {
background-color: #e0e0e0;
}
""")
self.insert_btn.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
}
QPushButton:hover {
background-color: #45a049;
}
""")
def on_theme_changed(self, is_dark):
"""主题切换槽函数"""
self.apply_theme()
if __name__ == "__main__":

@ -206,7 +206,6 @@ class ThemeManager(QObject):
color: #f0f0f0;
padding: 4px 0;
margin: 2px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
QMenu::item {
@ -528,7 +527,6 @@ class ThemeManager(QObject):
color: #333333;
padding: 4px 0;
margin: 2px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
QMenu::item {

@ -158,13 +158,9 @@ class WordRibbon(QFrame):
font_layout.addWidget(self.font_size_combo)
self.bold_btn = self.create_toggle_button("B", "bold")
self.bold_btn.clicked.connect(self.on_bold_clicked)
self.italic_btn = self.create_toggle_button("I", "italic")
self.italic_btn.clicked.connect(self.on_italic_clicked)
self.underline_btn = self.create_toggle_button("U", "underline")
self.underline_btn.clicked.connect(self.on_underline_clicked)
self.color_btn = self.create_color_button("A", "color")
self.color_btn.clicked.connect(self.on_color_clicked)
font_layout.addWidget(self.bold_btn)
font_layout.addWidget(self.italic_btn)
@ -192,6 +188,26 @@ class WordRibbon(QFrame):
self.init_style_preview_group()
self.add_separator()
# ---------- 快速样式组 ----------
quick_style_group, quick_style_layout = self.create_ribbon_group("快速样式")
# 创建样式按钮
self.heading1_btn = self.create_style_button("标题1")
self.heading2_btn = self.create_style_button("标题2")
self.heading3_btn = self.create_style_button("标题3")
self.heading4_btn = self.create_style_button("标题4")
self.body_text_btn = self.create_style_button("正文")
# 添加到布局
quick_style_layout.addWidget(self.heading1_btn)
quick_style_layout.addWidget(self.heading2_btn)
quick_style_layout.addWidget(self.heading3_btn)
quick_style_layout.addWidget(self.heading4_btn)
quick_style_layout.addWidget(self.body_text_btn)
quick_style_layout.addStretch()
layout.addWidget(quick_style_group)
self.add_separator()
# ---------- 编辑组 ----------
@ -221,57 +237,7 @@ class WordRibbon(QFrame):
"""字体大小变化处理"""
pass
def on_bold_clicked(self):
"""粗体按钮点击处理"""
pass
def on_italic_clicked(self):
"""斜体按钮点击处理"""
pass
def on_underline_clicked(self):
"""下划线按钮点击处理"""
pass
def on_color_clicked(self):
"""字体颜色按钮点击处理"""
pass
def on_heading1_clicked(self):
"""一级标题按钮点击处理"""
pass
def on_heading2_clicked(self):
"""二级标题按钮点击处理"""
pass
def on_heading3_clicked(self):
"""三级标题按钮点击处理"""
pass
def on_heading4_clicked(self):
"""四级标题按钮点击处理"""
pass
def on_body_text_clicked(self):
"""正文按钮点击处理"""
pass
def on_align_left_clicked(self):
"""左对齐按钮点击处理"""
pass
def on_align_center_clicked(self):
"""居中对齐按钮点击处理"""
pass
def on_align_right_clicked(self):
"""右对齐按钮点击处理"""
pass
def on_align_justify_clicked(self):
"""两端对齐按钮点击处理"""
pass
def init_style_preview_group(self):
"""加入 Word 风格的样式预览区域"""
@ -750,7 +716,6 @@ class WordRibbon(QFrame):
# 刷新按钮
self.refresh_weather_btn = QPushButton("🔄 刷新")
self.refresh_weather_btn.clicked.connect(self.on_refresh_weather)
self.refresh_weather_btn.setFixedSize(60, 30) # 增大刷新按钮尺寸
self.refresh_weather_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }")
self.refresh_weather_btn.setToolTip("刷新天气")
@ -805,7 +770,6 @@ class WordRibbon(QFrame):
# 刷新按钮
self.refresh_quote_btn = QPushButton("刷新箴言")
self.refresh_quote_btn.clicked.connect(self.on_refresh_quote)
self.refresh_quote_btn.setFixedSize(80, 25)
# 添加到第一行布局

@ -6,7 +6,8 @@ from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTextEdit, QLabel, QSplitter, QFrame, QMenuBar,
QAction, QFileDialog, QMessageBox, QApplication,
QDialog, QLineEdit, QCheckBox, QPushButton, QListWidget,
QListWidgetItem, QScrollArea)
QListWidgetItem, QScrollArea, QSizePolicy,
QGraphicsScene, QGraphicsView)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QRect, QByteArray, QBuffer, QIODevice
from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor, QTextDocument, QImage, QTextImageFormat, QTextFormat, QTextBlockFormat
@ -323,6 +324,11 @@ class WordStyleMainWindow(QMainWindow):
# 更新功能区下拉框样式
if hasattr(self, 'ribbon'):
self.update_ribbon_styles(is_dark)
# 更新日历组件样式
if hasattr(self, 'calendar_widget') and self.calendar_widget is not None:
# 日历组件有自己的主题管理机制,只需触发其主题更新
self.calendar_widget.apply_theme()
def update_ribbon_styles(self, is_dark):
"""更新功能区样式"""
@ -694,6 +700,21 @@ class WordStyleMainWindow(QMainWindow):
insert_image_action.triggered.connect(self.insert_image_in_typing_mode)
insert_menu.addAction(insert_image_action)
# 插入天气信息功能
insert_weather_action = QAction('插入天气信息', self)
insert_weather_action.triggered.connect(self.insert_weather_info)
insert_menu.addAction(insert_weather_action)
# 插入每日一句名言功能
insert_quote_action = QAction('插入每日一句名言', self)
insert_quote_action.triggered.connect(self.insert_daily_quote)
insert_menu.addAction(insert_quote_action)
# 插入古诗词功能
insert_poetry_action = QAction('插入古诗词', self)
insert_poetry_action.triggered.connect(self.insert_chinese_poetry)
insert_menu.addAction(insert_poetry_action)
# 绘图菜单
paint_menu = menubar.addMenu('绘图(D)')
@ -1833,7 +1854,7 @@ class WordStyleMainWindow(QMainWindow):
self.status_bar.showMessage("新建文档 - 打字模式,可以自由开始打字", 3000)
def import_file(self):
"""导入文件 - 仅在导入时存储内容,不立即显示"""
"""导入文件 - 根据模式决定是否立即显示"""
file_path, _ = QFileDialog.getOpenFileName(
self, "导入文件", "",
"文档文件 (*.docx *.txt *.pdf *.html);;所有文件 (*.*)"
@ -1854,7 +1875,7 @@ class WordStyleMainWindow(QMainWindow):
if result.get('is_temp_file', False):
self.temp_files.append(txt_path)
# 存储完整内容但不立即显示
# 存储完整内容
self.imported_content = content
self.displayed_chars = 0
@ -1896,8 +1917,9 @@ class WordStyleMainWindow(QMainWindow):
self.status_bar.showMessage(f"已导入: {os.path.basename(txt_path)},开始打字逐步显示学习内容!", 5000)
else:
# 打字模式:不显示导入内容,保持当前内容
self.status_bar.showMessage(f"已导入: {os.path.basename(txt_path)},切换到学习模式查看内容", 5000)
# 打字模式:直接显示完整内容
self.text_edit.setPlainText(content)
self.status_bar.showMessage(f"已导入: {os.path.basename(txt_path)}", 5000)
# 提取并显示图片(如果有)
if images:
@ -1914,7 +1936,7 @@ class WordStyleMainWindow(QMainWindow):
content = parser.parse_file(file_path)
if content:
# 存储完整内容但不立即显示
# 存储完整内容
self.imported_content = content
self.displayed_chars = 0
@ -1929,8 +1951,9 @@ class WordStyleMainWindow(QMainWindow):
self.status_bar.showMessage(f"已导入: {os.path.basename(file_path)},开始打字逐步显示学习内容!", 5000)
else:
# 打字模式:不显示导入内容,保持当前内容
self.status_bar.showMessage(f"已导入: {os.path.basename(file_path)},切换到学习模式查看内容", 5000)
# 打字模式:直接显示完整内容
self.text_edit.setPlainText(content)
self.status_bar.showMessage(f"已导入: {os.path.basename(file_path)}", 5000)
except Exception as fallback_e:
QMessageBox.critical(self, "错误", f"无法导入文件:\n{str(e)}\n\n回退方法也失败:\n{str(fallback_e)}")
@ -1955,7 +1978,7 @@ class WordStyleMainWindow(QMainWindow):
# 设置文件加载标志
self.is_loading_file = True
# 存储完整内容但不立即显示
# 存储完整内容
self.imported_content = content
self.displayed_chars = 0
@ -2129,8 +2152,17 @@ class WordStyleMainWindow(QMainWindow):
current_position = 0
self.imported_content = current_text
# 创建学习模式窗口,直接传递导入内容
self.learning_window = LearningModeWindow(self, imported_content, current_position)
# 准备图片数据
image_data = None
image_positions = None
if hasattr(self, 'typing_logic') and self.typing_logic:
if hasattr(self.typing_logic, 'image_data'):
image_data = self.typing_logic.image_data
if hasattr(self.typing_logic, 'image_positions'):
image_positions = self.typing_logic.image_positions
# 创建学习模式窗口,传递导入内容和图片数据
self.learning_window = LearningModeWindow(self, imported_content, current_position, image_data, image_positions)
# 连接学习模式窗口的内容变化信号
self.learning_window.content_changed.connect(self.on_learning_content_changed)
@ -2718,46 +2750,37 @@ class WordStyleMainWindow(QMainWindow):
self.status_bar.showMessage("已切换到黑色模式", 2000)
def show_image_viewer(self, filename, image_data):
"""显示图片查看器 - 图片真正铺满整个窗口上方"""
"""显示图片查看器 - 支持缩放功能"""
try:
# 创建自定义图片查看窗口
viewer = QDialog(self)
viewer.setWindowTitle(f"图片查看 - {filename}")
viewer.setModal(False)
# 移除窗口边框和标题栏装饰,设置为工具窗口样式
viewer.setWindowFlags(Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
# 设置窗口标志,保留标题栏以便用户可以移动和调整大小
viewer.setWindowFlags(Qt.Tool | Qt.WindowCloseButtonHint | Qt.WindowMinMaxButtonsHint)
# 设置窗口背景为黑色,完全无边距
# 设置窗口背景为黑色
viewer.setStyleSheet("""
QDialog {
background-color: #000000;
border: none;
margin: 0px;
padding: 0px;
}
""")
# 创建布局,完全移除边距
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0) # 移除布局边距
layout.setSpacing(0) # 移除组件间距
layout.setAlignment(Qt.AlignCenter) # 布局居中对齐
# 创建场景和视图
scene = QGraphicsScene(viewer)
view = QGraphicsView(scene)
view.setStyleSheet("border: none;") # 移除视图边框
# 创建图片标签,设置为完全填充模式
image_label = QLabel()
image_label.setAlignment(Qt.AlignCenter)
image_label.setScaledContents(True) # 关键:允许图片缩放以填充标签
image_label.setMinimumSize(1, 1) # 设置最小尺寸
image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # 设置大小策略为扩展
image_label.setStyleSheet("""
QLabel {
border: none;
margin: 0px;
padding: 0px;
background-color: #000000;
}
""")
# 设置视图为可交互的,并启用滚动条
view.setDragMode(QGraphicsView.ScrollHandDrag)
view.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
# 创建布局
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(view)
viewer.setLayout(layout)
# 加载图片
pixmap = QPixmap()
@ -2765,10 +2788,10 @@ class WordStyleMainWindow(QMainWindow):
self.status_bar.showMessage(f"加载图片失败: {filename}", 3000)
return
layout.addWidget(image_label)
viewer.setLayout(layout)
# 将图片添加到场景
scene.addPixmap(pixmap)
# 计算位置和大小
# 设置视图大小和位置
if self:
parent_geometry = self.geometry()
screen_geometry = QApplication.primaryScreen().geometry()
@ -2793,42 +2816,24 @@ class WordStyleMainWindow(QMainWindow):
viewer.show()
# 关键:强制图片立即填充整个标签区域
def force_image_fill():
try:
if pixmap and not pixmap.isNull():
# 获取标签的实际大小
label_size = image_label.size()
if label_size.width() > 10 and label_size.height() > 10: # 确保尺寸有效
# 完全填充,忽略宽高比,真正铺满
scaled_pixmap = pixmap.scaled(
label_size,
Qt.IgnoreAspectRatio, # 关键:忽略宽高比,强制填充
Qt.SmoothTransformation
)
image_label.setPixmap(scaled_pixmap)
print(f"图片已强制缩放至 {label_size.width()}x{label_size.height()}")
# 确保标签完全填充布局
image_label.setMinimumSize(label_size)
except Exception as e:
print(f"图片缩放失败: {e}")
# 设置视图适应图片大小
view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio)
# 使用多个定时器确保图片正确填充
from PyQt5.QtCore import QTimer
QTimer.singleShot(50, force_image_fill) # 50毫秒后执行
QTimer.singleShot(200, force_image_fill) # 200毫秒后执行
QTimer.singleShot(1000, force_image_fill) # 1000毫秒后再执行一次
# 重写视图的滚轮事件以支持缩放
def wheelEvent(event):
factor = 1.2
if event.angleDelta().y() > 0:
view.scale(factor, factor)
else:
view.scale(1.0/factor, 1.0/factor)
# 连接窗口大小变化事件
viewer.resizeEvent = lambda event: force_image_fill()
view.wheelEvent = wheelEvent
# 添加点击关闭功能
def close_viewer():
viewer.close()
# 添加双击重置视图功能
def mouseDoubleClickEvent(event):
view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio)
image_label.mousePressEvent = lambda event: close_viewer()
viewer.mousePressEvent = lambda event: close_viewer()
view.mouseDoubleClickEvent = mouseDoubleClickEvent
except Exception as e:
self.status_bar.showMessage(f"创建图片查看器失败: {str(e)}", 3000)
@ -3152,7 +3157,6 @@ class WordStyleMainWindow(QMainWindow):
max-width: 100%;
height: auto;
border-radius: 3px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
.image-caption {{
margin-top: 8px;
@ -3279,6 +3283,8 @@ class WordStyleMainWindow(QMainWindow):
if file_path:
try:
import os
import tempfile
from docx import Document
from docx.shared import Inches
@ -3303,10 +3309,6 @@ class WordStyleMainWindow(QMainWindow):
break
if img_data:
# 创建临时图片文件
import tempfile
import os
# 检测图片类型
if img_name.lower().endswith('.png'):
img_ext = '.png'
@ -3404,8 +3406,8 @@ class WordStyleMainWindow(QMainWindow):
# 为每张图片创建位置信息 - 修复位置计算,确保早期显示
content_length = len(self.imported_content)
if content_length == 0:
content_length = len(content) if 'content' in locals() else 1000 # 备用长度
#if content_length == 0:
#content_length = len(content) if 'content' in locals() else 1000 # 备用长度
# 修复图片位置计算,确保图片能在用户早期打字时显示
if len(images) == 1:
@ -3496,7 +3498,7 @@ class WordStyleMainWindow(QMainWindow):
# 如果日历组件可见,调整其大小和位置以适应窗口底部
if hasattr(self, 'calendar_widget') and self.calendar_widget.isVisible():
calendar_height = 250 # 减小高度使比例更美观
calendar_height = 350 # 增加高度以确保所有日期都能完整显示
self.calendar_widget.setGeometry(0, self.height() - calendar_height,
self.width(), calendar_height)
@ -3507,7 +3509,7 @@ class WordStyleMainWindow(QMainWindow):
self.calendar_widget.hide()
else:
# 设置日历组件位置在窗口底部
calendar_height = 250 # 减小高度使比例更美观
calendar_height = 350 # 增加高度以确保所有日期都能完整显示
# 将日历组件放置在窗口底部,占据整个宽度
self.calendar_widget.setGeometry(0, self.height() - calendar_height,
@ -3515,6 +3517,95 @@ class WordStyleMainWindow(QMainWindow):
self.calendar_widget.show()
self.calendar_widget.raise_() # 确保日历组件在最上层显示
def insert_weather_info(self):
"""在光标位置插入天气信息"""
# 检查是否处于打字模式
if self.view_mode != "typing":
self.status_bar.showMessage("请在打字模式下使用插入天气信息功能", 3000)
return
# 检查是否已经定位了天气(即是否有有效的天气数据)
if not hasattr(self, 'current_weather_data') or not self.current_weather_data:
# 弹出对话框提示用户先定位天气
QMessageBox.information(self, "附加工具", "先定位天气")
return
try:
# 直接使用已经获取到的天气数据
weather_data = self.current_weather_data
# 格式化天气信息
if 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')
weather_info = f"天气: {desc}, 温度: {temp}°C, 城市: {city}"
else:
weather_info = "天气信息获取失败"
# 在光标位置插入天气信息
cursor = self.text_edit.textCursor()
cursor.insertText(weather_info)
# 更新状态栏
self.status_bar.showMessage("已插入天气信息", 2000)
except Exception as e:
QMessageBox.warning(self, "错误", f"插入天气信息失败: {str(e)}")
def insert_daily_quote(self):
"""在光标位置插入每日一句名言"""
# 检查是否处于打字模式
if self.view_mode != "typing":
self.status_bar.showMessage("请在打字模式下使用插入每日一句名言功能", 3000)
return
try:
# 使用与Ribbon界面相同的API获取每日一言确保内容一致
from ui.word_style_ui import daily_sentence_API
quote_api = daily_sentence_API("https://api.nxvav.cn/api/yiyan")
quote_data = quote_api.get_sentence('json')
# 处理获取到的数据
if quote_data and isinstance(quote_data, dict):
quote_text = quote_data.get('yiyan', '暂无每日一言')
quote_info = quote_text
else:
quote_info = "每日一句名言获取失败"
# 在光标位置插入名言信息
cursor = self.text_edit.textCursor()
cursor.insertText(quote_info)
# 更新状态栏
self.status_bar.showMessage("已插入每日一句名言", 2000)
except Exception as e:
QMessageBox.warning(self, "错误", f"插入每日一句名言失败: {str(e)}")
def insert_chinese_poetry(self):
"""在光标位置插入古诗词"""
# 检查是否处于打字模式
if self.view_mode != "typing":
self.status_bar.showMessage("请在打字模式下使用插入古诗词功能", 3000)
return
try:
# 获取古诗词
poetry_data = self.ribbon.get_chinese_poetry()
# 在光标位置插入古诗词
cursor = self.text_edit.textCursor()
cursor.insertText(poetry_data)
# 更新状态栏
self.status_bar.showMessage("已插入古诗词", 2000)
except Exception as e:
QMessageBox.warning(self, "错误", f"插入古诗词失败: {str(e)}")
if __name__ == "__main__":
app = QApplication(sys.argv)

@ -0,0 +1,44 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import os
# 添加src目录到Python路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from ui.word_style_ui import daily_sentence_API
from services.network_service import NetworkService
def test_daily_sentence_api():
print("测试 daily_sentence_API 类...")
try:
# 使用与Ribbon界面相同的API获取每日一言
quote_api = daily_sentence_API("https://api.nxvav.cn/api/yiyan")
quote_data = quote_api.get_sentence('json')
print("API返回的数据:")
print(quote_data)
# 处理获取到的数据
if quote_data and isinstance(quote_data, dict):
quote_text = quote_data.get('yiyan', '暂无每日一言')
print(f"解析后的每日一言: {quote_text}")
else:
print("获取每日一言失败")
except Exception as e:
print(f"测试 daily_sentence_API 类时出错: {e}")
def test_network_service_quote():
print("\n测试 NetworkService 类的 get_daily_quote 方法...")
try:
network_service = NetworkService()
quote = network_service.get_daily_quote()
print(f"NetworkService 获取的每日一言: {quote}")
except Exception as e:
print(f"测试 NetworkService 类时出错: {e}")
if __name__ == "__main__":
test_daily_sentence_api()
test_network_service_quote()

@ -0,0 +1,60 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import os
# 添加src目录到Python路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from word_main_window import WordStyleMainWindow
from PyQt5.QtWidgets import QApplication, QMessageBox
from unittest.mock import patch
def test_insert_weather_without_location():
"""测试在未定位天气时插入天气信息"""
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
# 创建主窗口实例
window = WordStyleMainWindow()
# 模拟没有定位天气的情况删除current_weather_data属性
if hasattr(window, 'current_weather_data'):
delattr(window, 'current_weather_data')
# 模拟用户点击插入天气信息按钮
with patch('PyQt5.QtWidgets.QMessageBox.information') as mock_info:
window.insert_weather_info()
# 验证是否弹出了"先定位天气"对话框
mock_info.assert_called_once_with(window, "附加工具", "先定位天气")
print("测试通过:未定位天气时正确弹出提示对话框")
def test_insert_weather_with_location():
"""测试在已定位天气时插入天气信息"""
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
# 创建主窗口实例
window = WordStyleMainWindow()
# 模拟已定位天气的情况
window.current_weather_data = {
'city': '北京',
'temperature': 25,
'description': '晴天'
}
# 模拟用户点击插入天气信息按钮
# 注意这个测试不会真正插入文本因为我们没有设置完整的UI环境
window.insert_weather_info()
print("测试通过:已定位天气时正确执行插入操作")
if __name__ == "__main__":
test_insert_weather_without_location()
test_insert_weather_with_location()
print("所有测试完成!")
Loading…
Cancel
Save