You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Curriculum_Design/src/word_main_window.py

3719 lines
158 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# word_main_window.py
import sys
import os
import platform
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTextEdit, QLabel, QSplitter, QFrame, QMenuBar,
QAction, QFileDialog, QMessageBox, QApplication,
QDialog, QLineEdit, QCheckBox, QPushButton, QListWidget,
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
from ui.word_style_ui import (WordRibbon, WordStatusBar, WordTextEdit,
)
from services.network_service import NetworkService
from typing_logic import TypingLogic
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.theme_manager import theme_manager
class WeatherFetchThread(QThread):
weather_fetched = pyqtSignal(dict)
def __init__(self):
super().__init__()
self.weather_api = WeatherAPI()
def run(self):
try:
# 使用智能定位获取天气数据,自动获取用户位置
weather_data = self.weather_api.get_weather_data()
if weather_data:
self.weather_fetched.emit(weather_data)
else:
# 使用模拟数据作为后备
mock_data = {
'city': '北京',
'temperature': 25,
'description': '晴天',
'humidity': 45,
'wind_scale': 2,
'forecast': []
}
self.weather_fetched.emit(mock_data)
except Exception as e:
self.weather_fetched.emit({'error': str(e)})
class QuoteFetchThread(QThread):
quote_fetched = pyqtSignal(dict)
def __init__(self):
super().__init__()
self.network_service = NetworkService()
def run(self):
try:
quote_data = self.network_service.get_daily_quote()
self.quote_fetched.emit(quote_data)
except Exception as e:
self.quote_fetched.emit({'error': str(e)})
class WordStyleMainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.current_file_path = None
self.is_modified = False
self.typing_logic = None
self.is_loading_file = False # 添加文件加载标志
self.imported_content = "" # 存储导入的完整内容
self.displayed_chars = 0 # 已显示的字符数
self.extracted_images = [] # 存储提取的图片数据
self.image_list_widget = None # 图片列表控件
# 视图模式:"typing" - 打字模式,"learning" - 学习模式
self.view_mode = "typing" # 默认打字模式
# 初始化模式切换相关变量
self.typing_mode_content = "" # 打字模式下的内容
self.learning_progress = 0 # 学习进度
self.learning_text = "" # 学习模式下的文本内容
self.cursor_position = 0 # 光标位置
# 学习模式窗口引用和同步标记
self.learning_window = None # 学习模式窗口引用
self.sync_from_learning = False # 从学习模式同步内容的标记
# 统一文档内容管理
self.unified_document_content = "" # 统一文档内容
self.last_edit_mode = "typing" # 上次编辑模式
# 临时文件管理
self.temp_files = [] # 跟踪创建的临时文件
# 初始化输入处理器
self.input_processor = InputProcessor()
# 初始化网络服务和WeatherAPI
self.network_service = NetworkService()
self.weather_api = WeatherAPI()
# 初始化日历组件
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.setWindowTitle("文档1 - MagicWord")
self.setGeometry(100, 100, 1200, 800)
# 初始化主题
self.init_theme()
# 设置应用程序图标
self.set_window_icon()
# 初始化UI
self.setup_ui()
# 连接信号
self.connect_signals()
# 初始化网络服务
self.init_network_services()
# 初始化打字逻辑
self.init_typing_logic()
# 连接Ribbon的天气功能
self.ribbon.on_refresh_weather = self.refresh_weather
self.ribbon.on_city_changed = self.on_city_changed
# 初始化时刷新天气
self.refresh_weather()
def init_theme(self):
"""初始化主题"""
# 连接主题切换信号
theme_manager.theme_changed.connect(self.on_theme_changed)
# 启用系统主题自动检测
theme_manager.enable_auto_detection(True)
# 应用当前主题
self.apply_theme()
def apply_theme(self):
"""应用主题样式"""
is_dark = theme_manager.is_dark_theme()
# 应用主题样式表
stylesheet = theme_manager.get_theme_stylesheet(is_dark)
if stylesheet.strip(): # 只在有样式内容时应用
self.setStyleSheet(stylesheet)
# 更新组件样式
self.update_component_styles(is_dark)
def update_component_styles(self, is_dark):
"""更新组件样式"""
colors = theme_manager.get_current_theme_colors()
# 更新菜单栏样式 - 使用微软蓝
if hasattr(self, 'menubar'):
self.menubar.setStyleSheet("""
QMenuBar {
background-color: #0078d7;
border: 1px solid #005a9e;
font-size: 12px;
color: #ffffff;
}
QMenuBar::item {
background-color: transparent;
padding: 4px 10px;
color: #ffffff;
}
QMenuBar::item:selected {
background-color: #106ebe;
}
QMenuBar::item:pressed {
background-color: #005a9e;
color: #ffffff;
}
#startMenu {
background-color: white;
color: #000000;
}
""")
# 更新文件菜单样式
if hasattr(self, 'file_menu') and self.file_menu is not None:
self.file_menu.setStyleSheet(f"""
QMenu {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
font-size: 12px;
color: {colors['text']};
}}
QMenu::item {{
padding: 4px 20px;
color: {colors['text']};
background-color: transparent;
}}
QMenu::item:selected {{
background-color: {colors['surface_hover']};
}}
QMenu::item:pressed {{
background-color: {colors['accent']};
color: {colors['surface']};
}}
""")
# 更新视图菜单样式
if hasattr(self, 'view_menu') and self.view_menu is not None:
self.view_menu.setStyleSheet(f"""
QMenu {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
font-size: 12px;
color: {colors['text']};
}}
QMenu::item {{
padding: 4px 20px;
color: {colors['text']};
background-color: transparent;
}}
QMenu::item:selected {{
background-color: {colors['surface_hover']};
}}
QMenu::item:pressed {{
background-color: {colors['accent']};
color: {colors['surface']};
}}
""")
# 更新开始菜单样式
if hasattr(self, 'start_menu') and self.start_menu is not None:
self.start_menu.setStyleSheet(f"""
QMenu {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
font-size: 12px;
color: {colors['text']};
}}
QMenu::item {{
padding: 4px 20px;
color: {colors['text']};
background-color: transparent;
}}
QMenu::item:selected {{
background-color: {colors['surface_hover']};
}}
QMenu::item:pressed {{
background-color: {colors['accent']};
color: {colors['surface']};
}}
""")
# 更新文本编辑器样式
if hasattr(self, 'text_edit'):
self.text_edit.setStyleSheet(f"""
QTextEdit {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 0px;
font-family: 'Calibri', 'Microsoft YaHei', '微软雅黑', sans-serif;
font-size: 12pt;
color: {colors['text']};
padding: 40px;
line-height: 1.5;
}}
""")
# 更新滚动区域样式
if hasattr(self, 'scroll_area'):
self.scroll_area.setStyleSheet(f"""
QScrollArea {{
background-color: {colors['background']};
border: none;
}}
QScrollArea QWidget {{
background-color: {colors['background']};
}}
""")
# 更新图片列表样式(如果已存在)
if hasattr(self, 'image_list_widget') and self.image_list_widget is not None:
self.image_list_widget.setStyleSheet(f"""
QListWidget {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 4px;
font-size: 11px;
color: {colors['text']};
}}
QListWidget::item {{
padding: 5px;
border-bottom: 1px solid {colors['border']};
color: {colors['text']};
}}
QListWidget::item:selected {{
background-color: {colors['accent']};
color: {colors['surface']};
}}
""")
# 更新功能区下拉框样式
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):
"""更新功能区样式"""
colors = theme_manager.get_current_theme_colors()
# 更新字体下拉框样式
if hasattr(self.ribbon, 'font_combo'):
self.ribbon.font_combo.setStyleSheet(f"""
QComboBox {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 2px;
color: {colors['text']};
padding: 2px 5px;
}}
QComboBox:hover {{
background-color: {colors['surface_hover']};
border: 1px solid {colors['accent']};
}}
QComboBox::drop-down {{
border: none;
width: 15px;
}}
QComboBox::down-arrow {{
image: none;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
border-top: 5px solid {colors['text']};
}}
""")
# 更新字体大小下拉框样式
if hasattr(self.ribbon, 'size_combo'):
self.ribbon.size_combo.setStyleSheet(f"""
QComboBox {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 2px;
color: {colors['text']};
padding: 2px 5px;
}}
QComboBox:hover {{
background-color: {colors['surface_hover']};
border: 1px solid {colors['accent']};
}}
QComboBox::drop-down {{
border: none;
width: 15px;
}}
QComboBox::down-arrow {{
image: none;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
border-top: 5px solid {colors['text']};
}}
""")
def on_theme_changed(self, is_dark):
"""主题切换槽函数"""
self.apply_theme()
def set_window_icon(self):
"""设置窗口图标"""
# 使用我们创建的Word风格图标
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 尝试不同的图标文件
icon_files = [
'app_icon_32*32.png',
'app_icon_64*64.png',
'app_icon_128*128.png',
'app_icon_256*256.png',
'app_icon.png'
]
icon_path = None
for icon_file in icon_files:
test_path = os.path.join(project_root, 'resources', 'icons', icon_file)
if os.path.exists(test_path):
icon_path = test_path
break
if icon_path and os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path))
else:
# 如果图标文件不存在创建简单的Word风格图标
icon = QIcon()
pixmap = QPixmap(32, 32)
pixmap.fill(QColor("#2B579A"))
icon.addPixmap(pixmap)
self.setWindowIcon(icon)
def on_city_changed(self, city):
"""城市选择变化处理"""
print(f"城市选择变化: {city}")
if city == '自动定位':
self.refresh_weather() # 重新自动定位
else:
# 手动选择城市
print(f"手动选择城市: {city}")
weather_data = self.weather_api.get_weather_data(city)
if weather_data:
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):
"""刷新天气"""
current_city = self.ribbon.city_combo.currentText()
print(f"当前选择的城市: {current_city}")
if current_city == '自动定位':
# 使用自动定位
print("使用自动定位")
location_info = self.weather_api.get_current_location()
if location_info:
if 'note' in location_info:
# 检测到特殊网络环境
print(f"网络环境提示: {location_info['note']}")
if '教育网' in location_info['note'] and location_info['city'].lower() in ['beijing', '北京', 'haidian', '海淀']:
print("建议:教育网环境下北京定位可能不准确,可手动选择天津")
# 可以选择提示用户手动选择,或者使用上次的定位
# 使用定位到的城市获取天气
actual_city = location_info.get('city', '北京')
weather_data = self.weather_api.get_weather_data(actual_city)
else:
# 定位失败,使用默认城市
weather_data = self.weather_api.get_weather_data()
else:
# 使用选中的城市
print(f"使用选中的城市: {current_city}")
weather_data = self.weather_api.get_weather_data(current_city)
if weather_data:
print(f"更新天气信息: {weather_data}")
# 格式化数据以匹配状态栏期望的格式
formatted_data = {
'city': weather_data['city'],
'temperature': weather_data['current']['temp'],
'description': weather_data['current']['weather'],
'humidity': weather_data['current']['humidity'],
'wind_scale': weather_data['current']['wind_scale'],
'life_tips': weather_data.get('life_tips', [])
}
print(f"格式化后的数据: {formatted_data}")
self.update_weather_display(formatted_data)
def setup_ui(self):
"""设置Word风格的UI界面"""
# 创建菜单栏
self.create_menu_bar()
# 创建Ribbon功能区
self.ribbon = WordRibbon()
# 创建中心部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 主布局
main_layout = QVBoxLayout()
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# 添加Ribbon
main_layout.addWidget(self.ribbon)
# 创建文档编辑区域
self.create_document_area(main_layout)
# 创建状态栏
self.status_bar = WordStatusBar()
self.setStatusBar(self.status_bar)
central_widget.setLayout(main_layout)
# 设置样式
self.setStyleSheet("""
QMainWindow {
background-color: #f3f2f1;
}
""")
# 创建日历组件并添加到窗口中(默认隐藏)
try:
from ui.calendar_widget import CalendarWidget
self.calendar_widget = CalendarWidget(self)
self.calendar_widget.hide() # 默认隐藏
except Exception as e:
print(f"创建日历组件失败: {e}")
def create_menu_bar(self):
"""创建菜单栏"""
menubar = self.menuBar()
menubar.setNativeMenuBar(False)
self.menubar = menubar # 保存为实例变量以便后续样式更新
# 文件菜单
file_menu = menubar.addMenu('文件(F)')
self.file_menu = file_menu # 保存为实例变量
# 新建
new_action = QAction('新建(N)', self)
new_action.setShortcut('Ctrl+N')
new_action.triggered.connect(self.new_document)
file_menu.addAction(new_action)
# 导入文件 - 改为导入功能
open_action = QAction('导入文件(I)...', self)
open_action.setShortcut('Ctrl+O')
open_action.triggered.connect(self.import_file)
file_menu.addAction(open_action)
# 保存
save_action = QAction('保存(S)', self)
save_action.setShortcut('Ctrl+S')
save_action.triggered.connect(self.save_file)
file_menu.addAction(save_action)
# 另存为
save_as_action = QAction('另存为(A)...', self)
save_as_action.triggered.connect(self.save_as_file)
file_menu.addAction(save_as_action)
file_menu.addSeparator()
# 退出
exit_action = QAction('退出(X)', self)
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# 开始菜单
start_menu = menubar.addMenu('开始(S)')
start_menu.setObjectName("startMenu")
self.start_menu = start_menu # 保存为实例变量
# 撤销
undo_action = QAction('撤销(U)', self)
undo_action.setShortcut('Ctrl+Z')
undo_action.triggered.connect(self.undo)
start_menu.addAction(undo_action)
# 重做
redo_action = QAction('重做(R)', self)
redo_action.setShortcut('Ctrl+Y')
redo_action.triggered.connect(self.redo)
start_menu.addAction(redo_action)
start_menu.addSeparator()
# 剪切
cut_action = QAction('剪切(T)', self)
cut_action.setShortcut('Ctrl+X')
cut_action.triggered.connect(self.cut)
start_menu.addAction(cut_action)
# 复制
copy_action = QAction('复制(C)', self)
copy_action.setShortcut('Ctrl+C')
copy_action.triggered.connect(self.copy)
start_menu.addAction(copy_action)
# 粘贴
paste_action = QAction('粘贴(P)', self)
paste_action.setShortcut('Ctrl+V')
paste_action.triggered.connect(self.paste)
start_menu.addAction(paste_action)
# 视图菜单
view_menu = menubar.addMenu('视图(V)')
self.view_menu = view_menu # 保存为实例变量
# 阅读视图
read_view_action = QAction('阅读视图', self)
read_view_action.triggered.connect(self.toggle_reading_view)
view_menu.addAction(read_view_action)
# 打印布局
print_layout_action = QAction('打印布局', self)
print_layout_action.setCheckable(True)
print_layout_action.setChecked(True)
print_layout_action.triggered.connect(self.toggle_print_layout)
view_menu.addAction(print_layout_action)
view_menu.addSeparator()
# 模式选择子菜单
theme_menu = view_menu.addMenu('模式')
# 白色模式
self.light_mode_action = QAction('白色模式', self)
self.light_mode_action.setCheckable(True)
self.light_mode_action.setChecked(not theme_manager.is_dark_theme()) # 根据当前主题设置
self.light_mode_action.triggered.connect(self.set_light_mode)
theme_menu.addAction(self.light_mode_action)
# 黑色模式
self.dark_mode_action = QAction('黑色模式', self)
self.dark_mode_action.setCheckable(True)
self.dark_mode_action.setChecked(theme_manager.is_dark_theme()) # 根据当前主题设置
self.dark_mode_action.triggered.connect(self.set_dark_mode)
theme_menu.addAction(self.dark_mode_action)
view_menu.addSeparator()
# 视图模式选择
view_mode_menu = view_menu.addMenu('视图模式')
# 打字模式
self.typing_mode_action = QAction('打字模式', self)
self.typing_mode_action.setCheckable(True)
self.typing_mode_action.setChecked(True) # 默认打字模式
self.typing_mode_action.triggered.connect(lambda: self.set_view_mode("typing"))
view_mode_menu.addAction(self.typing_mode_action)
# 学习模式
self.learning_mode_action = QAction('学习模式', self)
self.learning_mode_action.setCheckable(True)
self.learning_mode_action.setChecked(False)
# 设置学习模式快捷键 (Qt会自动在macOS上映射Ctrl为Cmd)
self.learning_mode_action.setShortcut('Ctrl+L')
self.learning_mode_action.triggered.connect(lambda: self.set_view_mode("learning"))
view_mode_menu.addAction(self.learning_mode_action)
view_menu.addSeparator()
# 附加工具功能
weather_menu = view_menu.addMenu('附加工具')
# 显示天气工具组
self.show_weather_tools_action = QAction('显示天气工具', self)
self.show_weather_tools_action.setCheckable(True)
self.show_weather_tools_action.setChecked(False) # 默认不显示
self.show_weather_tools_action.triggered.connect(self.toggle_weather_tools)
weather_menu.addAction(self.show_weather_tools_action)
# 显示每日一言工具组
self.show_quote_tools_action = QAction('显示每日一言工具', self)
self.show_quote_tools_action.setCheckable(True)
self.show_quote_tools_action.setChecked(False) # 默认不显示
self.show_quote_tools_action.triggered.connect(self.toggle_quote_tools)
weather_menu.addAction(self.show_quote_tools_action)
weather_menu.addSeparator()
# 刷新天气
refresh_weather_action = QAction('刷新天气', self)
refresh_weather_action.setShortcut('F5')
refresh_weather_action.triggered.connect(self.refresh_weather)
weather_menu.addAction(refresh_weather_action)
# 显示详细天气
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)
# 插入菜单
insert_menu = menubar.addMenu('插入(I)')
# 插入图片功能
insert_image_action = QAction('插入图片', self)
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)')
# 添加日历按钮
calendar_action = QAction('日历', self)
calendar_action.triggered.connect(self.toggle_calendar)
paint_menu.addAction(calendar_action)
# 设计菜单
design_menu = menubar.addMenu('设计(G)')
# 导出子菜单
export_menu = design_menu.addMenu('导出')
# 导出为HTML
export_html_action = QAction('导出为HTML', self)
export_html_action.triggered.connect(self.export_as_html)
export_menu.addAction(export_html_action)
# 导出为PDF
export_pdf_action = QAction('导出为PDF', self)
export_pdf_action.triggered.connect(self.export_as_pdf)
export_menu.addAction(export_pdf_action)
# 导出为TXT
export_txt_action = QAction('导出为TXT', self)
export_txt_action.triggered.connect(self.export_as_txt)
export_menu.addAction(export_txt_action)
# 导出为DOCX
export_docx_action = QAction('导出为DOCX', self)
export_docx_action.triggered.connect(self.export_as_docx)
export_menu.addAction(export_docx_action)
# 布局菜单
layout_menu = menubar.addMenu('布局(L)')
# 引用菜单
reference_menu = menubar.addMenu('引用(R)')
# 邮件菜单
mail_menu = menubar.addMenu('邮件(M)')
# 审阅菜单
review_menu = menubar.addMenu('审阅(W)')
# 开发工具菜单
developer_menu = menubar.addMenu('开发工具(Q)')
# 帮助菜单
help_menu = menubar.addMenu('帮助(H)')
# 关于
about_action = QAction('关于 MagicWord', self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
def create_document_area(self, main_layout):
"""创建文档编辑区域"""
# 创建滚动区域
from PyQt5.QtWidgets import QScrollArea
self.scroll_area = QScrollArea() # 保存为实例变量
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.scroll_area.setStyleSheet("""
QScrollArea {
background-color: #e1e1e1;
border: none;
}
QScrollArea QWidget {
background-color: #e1e1e1;
}
""")
# 创建文档容器
document_container = QWidget()
document_layout = QVBoxLayout()
document_layout.setContentsMargins(50, 50, 50, 50)
# 创建文本编辑区域使用Word风格的文本编辑器
self.text_edit = WordTextEdit()
self.text_edit.setMinimumHeight(600)
self.text_edit.setStyleSheet("""
QTextEdit {
background-color: #ffffff;
border: 1px solid #d0d0d0;
border-radius: 0px;
font-family: 'Calibri', 'Microsoft YaHei', '微软雅黑', sans-serif;
font-size: 12pt;
color: #000000;
padding: 40px;
line-height: 1.5;
}
""")
# 设置默认文档内容
self.text_edit.setPlainText("在此输入您的内容...")
# 连接输入处理器到文本编辑器
self.text_edit.set_input_processor(self.input_processor)
# 连接输入处理器的信号
self.input_processor.text_changed.connect(self.on_input_text_changed)
self.input_processor.key_pressed.connect(self.on_key_pressed)
document_layout.addWidget(self.text_edit)
# 创建图片显示区域
self.image_list_widget = QListWidget()
self.image_list_widget.setMaximumHeight(200)
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)
document_layout.addWidget(self.image_list_widget)
document_container.setLayout(document_layout)
self.scroll_area.setWidget(document_container)
main_layout.addWidget(self.scroll_area)
def init_network_services(self):
"""初始化网络服务"""
# 获取天气信息
self.weather_thread = WeatherFetchThread()
self.weather_thread.weather_fetched.connect(self.update_weather_display)
self.weather_thread.start()
# 获取每日名言
self.quote_thread = QuoteFetchThread()
self.quote_thread.quote_fetched.connect(self.update_quote_display)
self.quote_thread.start()
def init_typing_logic(self):
"""初始化打字逻辑"""
# 使用默认内容初始化打字逻辑
default_content = "欢迎使用MagicWord隐私学习软件\n\n这是一个仿Microsoft Word界面的学习工具。"
self.typing_logic = TypingLogic(default_content)
self.typing_logic.reset()
def connect_signals(self):
"""连接信号和槽"""
# 文本变化信号
self.text_edit.textChanged.connect(self.on_text_changed)
# 光标位置变化信号,用于更新按钮状态
self.text_edit.cursorPositionChanged.connect(self.update_format_buttons)
# Ribbon按钮信号
# 标签栏已删除,相关代码已移除
# 字体设置信号
if hasattr(self.ribbon, 'font_combo'):
self.ribbon.font_combo.currentFontChanged.connect(self.on_font_changed)
self.ribbon.font_size_combo.currentTextChanged.connect(self.on_font_size_changed)
self.ribbon.bold_btn.clicked.connect(self.on_bold_clicked)
self.ribbon.italic_btn.clicked.connect(self.on_italic_clicked)
self.ribbon.underline_btn.clicked.connect(self.on_underline_clicked)
self.ribbon.color_btn.clicked.connect(self.on_color_clicked)
# 样式按钮信号
if hasattr(self.ribbon, 'heading1_btn'):
self.ribbon.heading1_btn.clicked.connect(self.on_heading1_clicked)
if hasattr(self.ribbon, 'heading2_btn'):
self.ribbon.heading2_btn.clicked.connect(self.on_heading2_clicked)
if hasattr(self.ribbon, 'heading3_btn'):
self.ribbon.heading3_btn.clicked.connect(self.on_heading3_clicked)
if hasattr(self.ribbon, 'heading4_btn'):
self.ribbon.heading4_btn.clicked.connect(self.on_heading4_clicked)
if hasattr(self.ribbon, 'body_text_btn'):
self.ribbon.body_text_btn.clicked.connect(self.on_body_text_clicked)
# 查找和替换按钮信号
if hasattr(self.ribbon, 'find_btn'):
self.ribbon.find_btn.clicked.connect(self.show_find_dialog)
if hasattr(self.ribbon, 'replace_btn'):
self.ribbon.replace_btn.clicked.connect(self.show_replace_dialog)
# 页面布局信号已在菜单中直接连接,无需在此重复连接
# 段落对齐按钮信号
if hasattr(self.ribbon, 'align_left_btn'):
self.ribbon.align_left_btn.clicked.connect(self.on_align_left_clicked)
if hasattr(self.ribbon, 'align_center_btn'):
self.ribbon.align_center_btn.clicked.connect(self.on_align_center_clicked)
if hasattr(self.ribbon, 'align_right_btn'):
self.ribbon.align_right_btn.clicked.connect(self.on_align_right_clicked)
if hasattr(self.ribbon, 'align_justify_btn'):
self.ribbon.align_justify_btn.clicked.connect(self.on_align_justify_clicked)
# 天气功能信号
if hasattr(self.ribbon, 'city_combo'):
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'):
self.calendar_widget.date_selected.connect(self.insert_date_to_editor)
def on_text_changed(self):
"""文本变化处理 - 根据视图模式处理文本变化"""
# 如果正在加载文件,跳过处理
if self.is_loading_file:
return
# 检查是否是从学习模式同步内容,避免递归调用
if hasattr(self, 'sync_from_learning') and self.sync_from_learning:
return
# 根据当前视图模式处理
if self.view_mode == "learning":
# 学习模式:需要导入文件才能打字
if not self.imported_content:
# 没有导入文件时,清空文本并提示
current_text = self.text_edit.toPlainText()
if current_text and current_text != "在此输入您的内容...":
self.text_edit.clear()
self.status_bar.showMessage("学习模式需要先导入文件才能开始打字", 3000)
return
# 学习模式下处理导入内容的逐步显示
self.handle_learning_mode_typing()
elif self.view_mode == "typing":
# 打字模式:可以自由打字,不自动处理内容
# 只在用户主动操作时处理,避免内容被覆盖
pass
# 标记文档为已修改
if not self.is_modified:
self.is_modified = True
self.update_window_title()
def on_input_text_changed(self, text):
"""输入处理器文本变化处理"""
if self.view_mode == "learning" and self.imported_content:
# 在学习模式下,根据输入处理器的状态更新显示
current_text = self.text_edit.toPlainText()
expected_text = self.imported_content[:len(text)]
# 如果文本不匹配,更新显示
if current_text != expected_text:
cursor = self.text_edit.textCursor()
self.text_edit.setPlainText(expected_text)
# 保持光标在末尾
cursor.movePosition(QTextCursor.End)
self.text_edit.setTextCursor(cursor)
def on_key_pressed(self, key):
"""按键按下处理"""
# 更新状态栏显示当前按键
if key in ['\b', '\x7f']:
self.status_bar.showMessage("退格键已处理", 1000)
else:
self.status_bar.showMessage(f"按键: {key}", 1000)
def handle_learning_mode_typing(self):
"""学习模式下的打字处理 - 从上次中断处继续显示学习文档C内容到文档A"""
if self.imported_content and self.typing_logic:
current_text = self.text_edit.toPlainText()
# 获取当前光标位置
cursor = self.text_edit.textCursor()
cursor_position = cursor.position()
# 只有在光标位于文本末尾时才显示新内容
if cursor_position == len(current_text):
# 使用输入处理器的状态来显示文本
input_text = self.input_processor.input_buffer
chars_to_show = len(input_text)
# 更新已显示字符数
self.displayed_chars = chars_to_show
# 保存当前学习进度,以便在模式切换时恢复
self.learning_progress = self.displayed_chars
# 更新打字逻辑中的进度信息
if self.typing_logic:
self.typing_logic.typed_chars = self.displayed_chars
self.typing_logic.current_index = self.displayed_chars
# 获取应该显示的文本部分(基于输入处理器的状态)
display_text = self.imported_content[:self.displayed_chars]
# 临时禁用文本变化信号,避免递归
self.text_edit.textChanged.disconnect(self.on_text_changed)
try:
# 完全重置文本内容,确保图片能正确插入
self.text_edit.clear()
self.text_edit.setPlainText(display_text)
# 重置图片插入记录,确保每次都能重新插入图片
if hasattr(self, 'inserted_images'):
self.inserted_images.clear()
# 在文本中插入图片(如果有的话)
self.insert_images_in_text()
# 恢复光标位置到文本末尾
cursor = self.text_edit.textCursor()
cursor.movePosition(cursor.End)
self.text_edit.setTextCursor(cursor)
# 更新打字逻辑(支持中文整词匹配)
if display_text:
result = self.typing_logic.check_input(display_text)
self.typing_logic.update_position(display_text)
# 错误处理(支持中文整词显示)
if not result['correct'] and display_text:
expected_char = result.get('expected', '')
# 如果是中文文本,显示更友好的错误信息
if self.typing_logic._is_chinese_char(expected_char):
# 获取期望的中文词组
expected_word = self.typing_logic._get_chinese_word_at(result['position'])
self.status_bar.showMessage(f"输入错误,期望词组: '{expected_word}'", 3000)
else:
self.status_bar.showMessage(f"输入错误,期望字符: '{expected_char}'", 2000)
self.highlight_next_char(result['position'], expected_char)
# 更新统计信息
stats = self.typing_logic.get_statistics()
self.update_status_bar(stats)
# 检查是否完成
if self.displayed_chars >= len(self.imported_content):
self.on_lesson_complete()
return
# 更新状态栏显示进度
progress_percentage = (self.displayed_chars / len(self.imported_content)) * 100
self.status_bar.showMessage(f"学习进度: {progress_percentage:.1f}% ({self.displayed_chars}/{len(self.imported_content)})", 2000)
except Exception as e:
print(f"学习模式处理出错: {str(e)}")
import traceback
traceback.print_exc()
finally:
# 重新连接文本变化信号
self.text_edit.textChanged.connect(self.on_text_changed)
self.text_edit.textChanged.disconnect(self.on_text_changed)
try:
# 完全重置文本内容,确保图片能正确插入
self.text_edit.clear()
self.text_edit.setPlainText(display_text)
# 重置图片插入记录,确保每次都能重新插入图片
if hasattr(self, 'inserted_images'):
self.inserted_images.clear()
# 打印调试信息
if hasattr(self.typing_logic, 'image_positions'):
print(f"当前有 {len(self.typing_logic.image_positions)} 张图片需要插入")
for img in self.typing_logic.image_positions:
print(f"图片位置: {img['start_pos']}, 文件名: {img['filename']}")
else:
print("没有图片位置信息")
# 在文本中插入图片(如果有的话)
self.insert_images_in_text()
# 恢复光标位置到文本末尾
cursor = self.text_edit.textCursor()
cursor.movePosition(cursor.End)
self.text_edit.setTextCursor(cursor)
# 更新打字逻辑(只检查已显示的部分)
if display_text:
result = self.typing_logic.check_input(display_text)
self.typing_logic.update_position(display_text)
# 错误处理
if not result['correct'] and display_text:
expected_char = result.get('expected', '')
self.status_bar.showMessage(f"输入错误,期望字符: '{expected_char}'", 2000)
self.highlight_next_char(result['position'], expected_char)
# 更新统计信息
stats = self.typing_logic.get_statistics()
self.update_status_bar(stats)
# 检查是否完成
if self.displayed_chars >= len(self.imported_content):
self.on_lesson_complete()
return
# 更新状态栏显示进度
progress_percentage = (self.displayed_chars / len(self.imported_content)) * 100
self.status_bar.showMessage(f"逐步显示进度: {progress_percentage:.1f}% ({self.displayed_chars}/{len(self.imported_content)})", 2000)
except Exception as e:
print(f"学习模式处理出错: {str(e)}")
import traceback
traceback.print_exc()
finally:
# 重新连接文本变化信号
self.text_edit.textChanged.connect(self.on_text_changed)
# 如果有保存的学习进度,确保光标位置正确
if hasattr(self, 'learning_progress') and self.learning_progress > 0:
cursor = self.text_edit.textCursor()
cursor.movePosition(cursor.End)
self.text_edit.setTextCursor(cursor)
else:
# 如果光标位置没有超过显示的字符数,则正常处理打字逻辑
if current_text and current_text != "在此输入您的内容...": # 忽略默认文本
result = self.typing_logic.check_input(current_text)
self.typing_logic.update_position(current_text)
# 错误处理(支持中文整词显示)
if not result['correct']:
expected_char = result.get('expected', '')
# 如果是中文文本,显示更友好的错误信息
if self.typing_logic._is_chinese_char(expected_char):
# 获取期望的中文词组
expected_word = self.typing_logic._get_chinese_word_at(result['position'])
self.status_bar.showMessage(f"输入错误,期望词组: '{expected_word}'", 3000)
else:
self.status_bar.showMessage(f"输入错误,期望字符: '{expected_char}'", 2000)
self.highlight_next_char(result['position'], expected_char)
# 更新统计信息
stats = self.typing_logic.get_statistics()
self.update_status_bar(stats)
def handle_typing_mode_typing(self):
"""打字模式下的打字处理 - 允许自由输入到文档A"""
# 打字模式下,允许自由打字,不强制显示导入内容
if self.typing_logic and not self.is_loading_file:
current_text = self.text_edit.toPlainText()
if current_text and current_text != "在此输入您的内容...": # 忽略默认文本
result = self.typing_logic.check_input(current_text)
self.typing_logic.update_position(current_text)
# 错误处理(支持中文整词显示)
if not result['correct']:
expected_char = result.get('expected', '')
# 如果是中文文本,显示更友好的错误信息
if self.typing_logic._is_chinese_char(expected_char):
# 获取期望的中文词组
expected_word = self.typing_logic._get_chinese_word_at(result['position'])
self.status_bar.showMessage(f"输入错误,期望词组: '{expected_word}'", 3000)
else:
self.status_bar.showMessage(f"输入错误,期望字符: '{expected_char}'", 2000)
self.highlight_next_char(result['position'], expected_char)
# 更新统计信息
stats = self.typing_logic.get_statistics()
self.update_status_bar(stats)
# 保存打字内容到文档A
self.typing_mode_content = current_text
# 更新学习进度(用于打字模式显示)
if hasattr(self, 'learning_progress'):
self.learning_progress = len(current_text)
def on_text_changed_original(self):
"""文本变化处理 - 支持逐步显示模式和自由打字模式"""
# 如果正在加载文件,跳过处理
if self.is_loading_file:
return
# 如果有导入的内容,实现逐步显示
if self.imported_content and self.typing_logic:
current_text = self.text_edit.toPlainText()
# 计算应该显示的字符数(用户输入多少个字符就显示多少个)
input_length = len(current_text)
# 如果用户输入长度超过了导入内容长度,限制在导入内容长度内
if input_length > len(self.imported_content):
input_length = len(self.imported_content)
# 如果显示的字符数需要更新
if input_length != self.displayed_chars:
self.displayed_chars = input_length
# 获取应该显示的文本部分
display_text = self.imported_content[:self.displayed_chars]
# 临时禁用文本变化信号,避免递归
self.text_edit.textChanged.disconnect(self.on_text_changed)
# 保存当前光标位置
cursor = self.text_edit.textCursor()
original_position = cursor.position()
# 只添加新字符,而不是重置整个文本
if len(display_text) > len(current_text):
# 需要添加新字符
new_chars = display_text[len(current_text):]
cursor.movePosition(cursor.End)
cursor.insertText(new_chars)
elif len(display_text) < len(current_text):
# 需要删除字符(用户按了删除键等情况)
cursor.setPosition(len(display_text))
cursor.movePosition(cursor.End, cursor.KeepAnchor)
cursor.removeSelectedText()
# 恢复光标位置
cursor.setPosition(min(original_position, len(display_text)))
self.text_edit.setTextCursor(cursor)
# 在文本中插入图片(如果有的话)
# 注意:必须在更新文本后调用,且要处理图片插入对文本长度的影响
self.insert_images_in_text()
# 重新连接文本变化信号
self.text_edit.textChanged.connect(self.on_text_changed)
# 更新打字逻辑(只检查已显示的部分)
if display_text:
result = self.typing_logic.check_input(display_text)
self.typing_logic.update_position(display_text)
# 错误处理(支持中文整词显示)
if not result['correct'] and display_text:
expected_char = result.get('expected', '')
# 如果是中文文本,显示更友好的错误信息
if self.typing_logic._is_chinese_char(expected_char):
# 获取期望的中文词组
expected_word = self.typing_logic._get_chinese_word_at(result['position'])
self.status_bar.showMessage(f"输入错误,期望词组: '{expected_word}'", 3000)
else:
self.status_bar.showMessage(f"输入错误,期望字符: '{expected_char}'", 2000)
self.highlight_next_char(result['position'], expected_char)
# 更新统计信息
stats = self.typing_logic.get_statistics()
self.update_status_bar(stats)
# 检查当前位置是否有图片
self.check_and_show_image_at_position(self.displayed_chars)
# 在文本中插入图片(如果有的话)
self.insert_images_in_text()
# 检查是否完成
if self.displayed_chars >= len(self.imported_content):
self.on_lesson_complete()
return
# 更新状态栏显示进度
progress_percentage = (self.displayed_chars / len(self.imported_content)) * 100
self.status_bar.showMessage(f"逐步显示进度: {progress_percentage:.1f}% ({self.displayed_chars}/{len(self.imported_content)})", 2000)
else:
# 自由打字模式 - 没有导入内容时的处理
if self.typing_logic and not self.is_loading_file:
current_text = self.text_edit.toPlainText()
if current_text and current_text != "在此输入您的内容...": # 忽略默认文本
result = self.typing_logic.check_input(current_text)
self.typing_logic.update_position(current_text)
# 错误处理
if not result['correct']:
expected_char = result.get('expected', '')
self.status_bar.showMessage(f"输入错误,期望字符: '{expected_char}'", 2000)
self.highlight_next_char(result['position'], expected_char)
# 更新统计信息
stats = self.typing_logic.get_statistics()
self.update_status_bar(stats)
# 标记文档为已修改
if not self.is_modified:
self.is_modified = True
self.update_window_title()
def on_font_changed(self, font):
"""字体更改处理"""
cursor = self.text_edit.textCursor()
if cursor.hasSelection():
# 如果有选中文本,只更改选中文本的字体
fmt = cursor.charFormat()
fmt.setFontFamily(font.family())
cursor.setCharFormat(fmt)
else:
# 如果没有选中文本,更改整个文档的默认字体
self.text_edit.setFontFamily(font.family())
def on_font_size_changed(self, size):
"""字体大小更改处理"""
try:
font_size = int(size)
cursor = self.text_edit.textCursor()
if cursor.hasSelection():
# 如果有选中文本,只更改选中文本的字体大小
fmt = cursor.charFormat()
fmt.setFontPointSize(font_size)
cursor.setCharFormat(fmt)
else:
# 如果没有选中文本,更改整个文档的默认字体大小
self.text_edit.setFontPointSize(font_size)
except ValueError:
pass # 忽略无效的字体大小
def on_bold_clicked(self, checked):
"""粗体按钮点击处理"""
cursor = self.text_edit.textCursor()
if cursor.hasSelection():
# 如果有选中文本,只更改选中文本的粗体样式
fmt = cursor.charFormat()
fmt.setFontWeight(QFont.Bold if checked else QFont.Normal)
cursor.setCharFormat(fmt)
else:
# 如果没有选中文本,更改整个文档的默认粗体样式
self.text_edit.setFontWeight(QFont.Bold if checked else QFont.Normal)
def on_italic_clicked(self, checked):
"""斜体按钮点击处理"""
cursor = self.text_edit.textCursor()
if cursor.hasSelection():
# 如果有选中文本,只更改选中文本的斜体样式
fmt = cursor.charFormat()
fmt.setFontItalic(checked)
cursor.setCharFormat(fmt)
else:
# 如果没有选中文本,更改整个文档的默认斜体样式
self.text_edit.setFontItalic(checked)
def on_underline_clicked(self, checked):
"""下划线按钮点击处理"""
cursor = self.text_edit.textCursor()
if cursor.hasSelection():
# 如果有选中文本,只更改选中文本的下划线样式
fmt = cursor.charFormat()
fmt.setFontUnderline(checked)
cursor.setCharFormat(fmt)
else:
# 如果没有选中文本,更改整个文档的默认下划线样式
self.text_edit.setFontUnderline(checked)
def on_color_clicked(self):
"""字体颜色按钮点击处理 - 保留之前内容的颜色"""
from PyQt5.QtWidgets import QColorDialog
# 显示颜色选择对话框,默认使用当前文本颜色
current_color = self.text_edit.textColor()
color = QColorDialog.getColor(current_color, self, "选择字体颜色")
if color.isValid():
# 只设置后续输入的默认颜色,不影响已有内容
self.text_edit.setTextColor(color)
# 如果有选中文本,提示用户颜色只对新输入生效
cursor = self.text_edit.textCursor()
if cursor.hasSelection():
self.status_bar.showMessage("字体颜色已设置,新输入的文本将使用该颜色", 2000)
def on_heading1_clicked(self):
"""一级标题按钮点击处理"""
self.apply_heading_style(1)
def on_heading2_clicked(self):
"""二级标题按钮点击处理"""
self.apply_heading_style(2)
def on_heading3_clicked(self):
"""三级标题按钮点击处理"""
self.apply_heading_style(3)
def on_heading4_clicked(self):
"""四级标题按钮点击处理"""
self.apply_heading_style(4)
def on_body_text_clicked(self):
"""正文按钮点击处理"""
self.apply_body_text_style()
def on_align_left_clicked(self):
"""左对齐按钮点击处理"""
self.apply_alignment(Qt.AlignLeft)
def on_align_center_clicked(self):
"""居中对齐按钮点击处理"""
self.apply_alignment(Qt.AlignCenter)
def on_align_right_clicked(self):
"""右对齐按钮点击处理"""
self.apply_alignment(Qt.AlignRight)
def on_align_justify_clicked(self):
"""两端对齐按钮点击处理"""
self.apply_alignment(Qt.AlignJustify)
def insert_date_to_editor(self, date_str):
"""将选中的日期插入到编辑器中"""
# 获取当前光标位置
cursor = self.text_edit.textCursor()
# 在光标位置插入日期字符串
cursor.insertText(date_str)
# 更新文本编辑器的光标
self.text_edit.setTextCursor(cursor)
# 隐藏日历组件
if hasattr(self, 'calendar_widget'):
self.calendar_widget.hide()
# 更新状态栏提示
self.status_bar.showMessage(f"已插入日期: {date_str}", 2000)
def apply_heading_style(self, level):
"""应用标题样式"""
cursor = self.text_edit.textCursor()
# 创建字符格式
char_format = QTextCharFormat()
# 创建块格式(段落格式)
block_format = QTextBlockFormat()
block_format.setTopMargin(12)
block_format.setBottomMargin(6)
# 根据标题级别设置样式
if level == 1:
# 一级标题24pt, 加粗
char_format.setFontPointSize(24)
char_format.setFontWeight(QFont.Bold)
elif level == 2:
# 二级标题18pt, 加粗
char_format.setFontPointSize(18)
char_format.setFontWeight(QFont.Bold)
elif level == 3:
# 三级标题16pt, 加粗
char_format.setFontPointSize(16)
char_format.setFontWeight(QFont.Bold)
elif level == 4:
# 四级标题14pt, 加粗
char_format.setFontPointSize(14)
char_format.setFontWeight(QFont.Bold)
# 应用格式
if cursor.hasSelection():
# 如果有选中文本,只更改选中文本的格式
cursor.mergeCharFormat(char_format)
else:
# 如果没有选中文本,更改当前段落的格式
cursor.setBlockFormat(block_format)
cursor.mergeCharFormat(char_format)
# 将光标移动到段落末尾并添加换行
cursor.movePosition(QTextCursor.EndOfBlock)
cursor.insertText("\n")
# 设置文本编辑器的默认格式
self.text_edit.setCurrentCharFormat(char_format)
self.text_edit.textCursor().setBlockFormat(block_format)
def apply_body_text_style(self):
"""应用正文样式"""
cursor = self.text_edit.textCursor()
# 创建字符格式
char_format = QTextCharFormat()
char_format.setFontPointSize(12) # 正文字号
char_format.setFontWeight(QFont.Normal) # 正常粗细
# 创建块格式(段落格式)
block_format = QTextBlockFormat()
block_format.setTopMargin(0)
block_format.setBottomMargin(6)
# 应用格式
if cursor.hasSelection():
# 如果有选中文本,只更改选中文本的格式
cursor.mergeCharFormat(char_format)
else:
# 如果没有选中文本,更改当前段落的格式
cursor.setBlockFormat(block_format)
cursor.mergeCharFormat(char_format)
# 设置文本编辑器的默认格式
self.text_edit.setCurrentCharFormat(char_format)
self.text_edit.textCursor().setBlockFormat(block_format)
def apply_alignment(self, alignment):
"""应用段落对齐方式"""
cursor = self.text_edit.textCursor()
# 创建块格式(段落格式)
block_format = QTextBlockFormat()
block_format.setAlignment(alignment)
# 应用格式
if cursor.hasSelection():
# 如果有选中文本,更改选中文本所在段落的对齐方式
cursor.mergeBlockFormat(block_format)
else:
# 如果没有选中文本,更改当前段落的对齐方式
cursor.setBlockFormat(block_format)
# 更新文本编辑器的默认段落格式
self.text_edit.textCursor().setBlockFormat(block_format)
def update_weather_display(self, weather_data):
"""更新天气显示"""
if 'error' in weather_data:
self.status_bar.showMessage(f"天气数据获取失败: {weather_data['error']}", 3000)
# 更新工具栏天气显示为错误状态
if hasattr(self, 'ribbon'):
if hasattr(self.ribbon, 'weather_icon_label'):
self.ribbon.weather_icon_label.setText("")
if hasattr(self.ribbon, 'weather_temp_label'):
self.ribbon.weather_temp_label.setText("--°C")
else:
# 处理嵌套的天气数据结构
city = weather_data.get('city', '未知城市')
# 从current字段获取温度和天气状况
current_data = weather_data.get('current', {})
temp = current_data.get('temp', 'N/A')
desc = current_data.get('weather', 'N/A')
# 获取温度范围信息
temp_range = ""
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)"
# 在状态栏显示简要天气信息
weather_message = f"{city}: {desc}, {temp}°C{temp_range}"
self.status_bar.showMessage(weather_message, 5000)
# 更新工具栏天气图标和温度显示
if hasattr(self, 'ribbon'):
# 更新天气图标
if hasattr(self.ribbon, 'weather_icon_label') and desc != 'N/A':
emoji = self.ribbon.get_weather_emoji(desc)
self.ribbon.weather_icon_label.setText(emoji)
# 更新温度显示
if hasattr(self.ribbon, 'weather_temp_label') and temp != 'N/A':
# 计算平均温度(使用最高温和最低温的平均值)
avg_temp = temp
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':
try:
avg_temp = (float(temp_max) + float(temp_min)) / 2
avg_temp = round(avg_temp, 1)
except (ValueError, TypeError):
avg_temp = temp
temp_str = f"{avg_temp}°C" if isinstance(avg_temp, (int, float)) else f"{temp}°C"
self.ribbon.weather_temp_label.setText(temp_str)
# 存储天气数据供其他功能使用(确保包含生活提示)
self.current_weather_data = weather_data
print(f"update_weather_display - 存储的current_weather_data包含life_tips: {self.current_weather_data.get('life_tips', [])}")
def refresh_weather(self):
"""手动刷新天气信息"""
try:
# 获取当前选择的城市
current_city = self.ribbon.city_combo.currentText()
print(f"刷新天气 - 当前选择的城市: {current_city}")
if current_city == '自动定位':
# 使用自动定位
weather_data = self.weather_api.get_weather_data()
else:
# 使用选中的城市
weather_data = self.weather_api.get_weather_data(current_city)
if weather_data:
# 格式化天气数据为扁平结构便于update_weather_display使用
formatted_data = {
'city': weather_data['city'],
'current': weather_data['current'],
'forecast': weather_data['forecast'],
'life_tips': weather_data.get('life_tips', [])
}
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)
def show_detailed_weather(self):
"""显示详细天气信息对话框"""
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit
# 检查是否有天气数据
if not hasattr(self, 'current_weather_data') or not self.current_weather_data:
QMessageBox.information(self, "附加工具", "暂无天气数据,请先刷新天气信息")
return
weather_data = self.current_weather_data
print(f"详细天气对话框 - 天气数据: {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_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')
current_info = f"""
当前温度: {temp}°C
最高气温: {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', [])
print(f"详细天气对话框 - 生活提示: {life_tips}")
print(f"详细天气对话框 - 完整天气数据: {weather_data}")
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.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 insert_quote_to_cursor(self, quote_text):
"""将古诗句插入到光标位置"""
if hasattr(self, 'text_edit'):
# 获取当前光标位置
cursor = self.text_edit.textCursor()
# 在光标位置插入文本
cursor.insertText(quote_text + "\n")
# 更新状态栏提示
# 从文本中提取诗句部分用于显示
quote_only = quote_text.split(" —— ")[0] if " —— " in quote_text else quote_text
self.status_bar.showMessage(f"已插入古诗句: {quote_only}", 3000)
def toggle_weather_tools(self, checked):
"""切换天气工具组显示"""
if checked:
# 显示天气工具组
if hasattr(self, 'ribbon'):
self.ribbon.show_weather_group()
self.status_bar.showMessage("天气工具已显示", 2000)
else:
# 隐藏天气工具组
if hasattr(self, 'ribbon'):
self.ribbon.hide_weather_group()
self.status_bar.showMessage("天气工具已隐藏", 2000)
def toggle_quote_tools(self, checked):
"""切换每日一言工具组显示"""
if checked:
# 显示每日一言工具组
if hasattr(self, 'ribbon'):
self.ribbon.show_quote_group()
self.status_bar.showMessage("每日一言工具已显示", 2000)
# 如果当前没有显示内容,刷新一次
if hasattr(self.ribbon, 'quote_label') and self.ribbon.quote_label.text() == "每日一言: 暂无":
self.refresh_daily_quote()
else:
# 隐藏每日一言工具组
if hasattr(self, 'ribbon'):
self.ribbon.hide_quote_group()
self.status_bar.showMessage("每日一言工具已隐藏", 2000)
def refresh_daily_quote(self):
"""刷新每日一言 - 使用WordRibbon中的API"""
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):
"""处理名言获取成功"""
if 'error' not in quote_data:
content = quote_data.get('content', '获取名言失败')
author = quote_data.get('author', '未知')
quote_text = f"{content}{author}"
# 更新Ribbon中的每日一言显示
if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'quote_label'):
self.ribbon.quote_label.setText(f"{quote_text}")
# 更新状态栏
self.status_bar.showMessage(f"每日名言: {quote_text}", 10000)
else:
# 处理错误情况
error_msg = quote_data.get('error', '获取名言失败')
if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'quote_label'):
self.ribbon.quote_label.setText(f"获取失败")
self.status_bar.showMessage(f"每日名言获取失败: {error_msg}", 5000)
def on_quote_error(self, error_data):
"""处理名言获取错误"""
error_msg = error_data.get('error', '获取名言失败') if isinstance(error_data, dict) else str(error_data)
if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'quote_label'):
self.ribbon.quote_label.setText(f"获取失败")
self.status_bar.showMessage(f"每日名言获取失败: {error_msg}", 5000)
def update_quote_display(self, quote_data):
"""更新名言显示"""
if 'error' not in quote_data:
content = quote_data.get('content', '获取名言失败')
author = quote_data.get('author', '未知')
self.status_bar.showMessage(f"每日名言: {content} - {author}", 10000)
def update_status_bar(self, stats):
"""更新状态栏统计信息"""
if stats:
# 获取打字统计信息
total_chars = stats.get('total_chars', 0)
typed_chars = stats.get('typed_chars', 0)
error_count = stats.get('error_count', 0)
accuracy_rate = stats.get('accuracy_rate', 0)
# 计算进度百分比
progress_text = ""
if total_chars > 0:
progress_percentage = (typed_chars / total_chars) * 100
progress_text = f"进度: {progress_percentage:.1f}%"
# 计算准确率
accuracy_text = f"准确率: {accuracy_rate:.1%}"
# 显示统计信息
status_text = f"{progress_text} | {accuracy_text} | 已输入: {typed_chars}/{total_chars} | 错误: {error_count}"
self.status_bar.showMessage(status_text, 0) # 0表示不自动消失
# 更新字数统计标签(如果存在)
if hasattr(self.status_bar, 'words_label'):
self.status_bar.words_label.setText(f"总字数: {total_chars}")
# 更新进度标签(如果存在)
if hasattr(self.status_bar, 'progress_label'):
self.status_bar.progress_label.setText(f"进度: {progress_percentage:.1f}%" if total_chars > 0 else "进度: 0%")
def update_window_title(self):
"""更新窗口标题"""
file_name = "文档1"
if self.current_file_path:
file_name = os.path.basename(self.current_file_path)
modified = "*" if self.is_modified else ""
self.setWindowTitle(f"{file_name}{modified} - MagicWord")
def new_document(self):
"""新建文档 - 根据当前视图模式处理"""
self.text_edit.clear()
self.current_file_path = None
self.is_modified = False
self.update_window_title()
# 重置导入内容和进度
self.imported_content = ""
self.displayed_chars = 0
if hasattr(self, 'learning_progress'):
delattr(self, 'learning_progress')
# 根据当前模式重置打字逻辑
if self.typing_logic:
if self.view_mode == "learning":
# 学习模式:重置为默认内容,需要导入文件
self.typing_logic.reset("欢迎使用MagicWord隐私学习软件\n\n请先导入文件开始打字学习。")
self.status_bar.showMessage("新建文档 - 学习模式,请先导入文件开始打字学习", 3000)
elif self.view_mode == "typing":
# 打字模式:重置为默认内容,允许自由打字
self.typing_logic.reset("欢迎使用MagicWord隐私学习软件\n\n这是一个仿Microsoft Word界面的学习工具。")
self.status_bar.showMessage("新建文档 - 打字模式,可以自由开始打字", 3000)
def import_file(self):
"""导入文件 - 根据模式决定是否立即显示"""
file_path, _ = QFileDialog.getOpenFileName(
self, "导入文件", "",
"文档文件 (*.docx *.txt *.pdf *.html);;所有文件 (*.*)"
)
if file_path:
try:
# 使用新的转换方法将文件转换为txt格式
parser = FileParser()
result = parser.parse_and_convert_to_txt(file_path)
if result['success']:
content = result['content']
txt_path = result['txt_path']
images = result['images']
# 如果是临时文件,添加到跟踪列表
if result.get('is_temp_file', False):
self.temp_files.append(txt_path)
# 存储完整内容
self.imported_content = content
self.displayed_chars = 0
# 如果有提取的图片,设置到打字逻辑中
if images:
image_data_dict = {}
for filename, image_data in images:
image_data_dict[filename] = image_data
# 创建图片位置信息(简化处理,将图片放在文本末尾)
image_positions = []
current_pos = len(content)
for i, (filename, _) in enumerate(images):
# 在文本末尾添加图片标记
content += f"\n\n[图片: {filename}]\n"
image_positions.append({
'start_pos': current_pos,
'end_pos': current_pos + len(f"[图片: {filename}]"),
'filename': filename
})
current_pos += len(f"\n\n[图片: {filename}]\n")
# 更新导入的内容
self.imported_content = content
# 设置图片数据到打字逻辑
if self.typing_logic:
self.typing_logic.set_image_data(image_data_dict)
self.typing_logic.set_image_positions(image_positions)
# 清空文本编辑器
self.text_edit.clear()
# 根据当前模式进行处理
if self.view_mode == "learning":
# 学习模式:重置打字逻辑并准备显示导入内容
if self.typing_logic:
self.typing_logic.reset(content) # 重置打字状态并设置新内容
self.status_bar.showMessage(f"已导入: {os.path.basename(txt_path)},开始打字逐步显示学习内容!", 5000)
else:
# 打字模式:直接显示完整内容
self.text_edit.setPlainText(content)
self.status_bar.showMessage(f"已导入: {os.path.basename(txt_path)}", 5000)
# 提取并显示图片(如果有)
if images:
self.extract_and_display_images(file_path=None, images=images)
else:
# 转换失败,显示错误信息
raise Exception(result['error'])
except Exception as e:
# 如果新转换方法失败,回退到原来的解析方法
try:
parser = FileParser()
content = parser.parse_file(file_path)
if content:
# 存储完整内容
self.imported_content = content
self.displayed_chars = 0
# 清空文本编辑器
self.text_edit.clear()
# 根据当前模式进行处理
if self.view_mode == "learning":
# 学习模式:重置打字逻辑并准备显示导入内容
if self.typing_logic:
self.typing_logic.reset(content) # 重置打字状态并设置新内容
self.status_bar.showMessage(f"已导入: {os.path.basename(file_path)},开始打字逐步显示学习内容!", 5000)
else:
# 打字模式:直接显示完整内容
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)}")
return
# 设置当前文件路径(仅作为参考,不用于保存)
self.current_file_path = txt_path if 'txt_path' in locals() else file_path
self.is_modified = False
self.update_window_title()
# 更新字数统计
if hasattr(self.status_bar, 'words_label'):
self.status_bar.words_label.setText(f"总字数: {len(content)}")
except Exception as e:
# 如果新转换方法失败,回退到原来的解析方法
try:
parser = FileParser()
content = parser.parse_file(file_path)
if content:
# 设置文件加载标志
self.is_loading_file = True
# 存储完整内容
self.imported_content = content
self.displayed_chars = 0
# 创建空白副本 - 清空文本编辑器
self.text_edit.clear()
# 根据当前模式进行处理
if self.view_mode == "learning":
# 学习模式:设置学习内容到打字逻辑
if self.typing_logic:
self.typing_logic.reset(content) # 重置打字状态并设置新内容
self.status_bar.showMessage(f"已打开学习文件: {os.path.basename(file_path)},开始打字逐步显示学习内容!", 5000)
else:
# 打字模式:直接显示完整内容
self.text_edit.setPlainText(content)
self.status_bar.showMessage(f"已打开: {os.path.basename(file_path)}", 5000)
# 清除文件加载标志
self.is_loading_file = False
# 设置当前文件路径
self.current_file_path = file_path
self.is_modified = False
self.update_window_title()
# 更新字数统计
if hasattr(self.status_bar, 'words_label'):
self.status_bar.words_label.setText(f"总字数: {len(content)}")
except Exception as fallback_e:
# 确保在异常情况下也清除标志
self.is_loading_file = False
QMessageBox.critical(self, "错误", f"无法打开文件:\n{str(e)}\n\n回退方法也失败:\n{str(fallback_e)}")
# 清除文件加载标志
self.is_loading_file = False
# 设置当前文件路径
self.current_file_path = file_path
self.is_modified = False
self.update_window_title()
# 更新字数统计
if hasattr(self.status_bar, 'words_label'):
self.status_bar.words_label.setText(f"总字数: {len(content)}")
# 提取并显示图片(仅对.docx文件
if file_path.lower().endswith('.docx'):
self.extract_and_display_images(file_path)
else:
QMessageBox.warning(self, "警告", "无法读取文件内容或文件为空")
except Exception as e:
# 确保在异常情况下也清除标志
self.is_loading_file = False
QMessageBox.critical(self, "错误", f"打开文件失败: {str(e)}")
print(f"文件打开错误详情: {e}") # 调试信息
def save_file(self):
"""保存文件"""
if self.current_file_path:
try:
# 如果是.docx文件创建一个基本的Word文档
if self.current_file_path.endswith('.docx'):
from docx import Document
doc = Document()
doc.add_paragraph(self.text_edit.toPlainText())
doc.save(self.current_file_path)
else:
# 对于其他格式,保持原有逻辑
with open(self.current_file_path, 'w', encoding='utf-8') as f:
f.write(self.text_edit.toPlainText())
self.is_modified = False
self.update_window_title()
self.status_bar.showMessage("文件已保存", 3000)
except Exception as e:
QMessageBox.critical(self, "错误", f"保存文件失败: {str(e)}")
else:
self.save_as_file()
def save_as_file(self):
"""另存为"""
file_path, _ = QFileDialog.getSaveFileName(
self, "另存为", "", "Word文档 (*.docx);;文本文档 (*.txt);;所有文件 (*.*)"
)
if file_path:
# 如果用户没有指定扩展名,自动添加.docx扩展名
if not os.path.splitext(file_path)[1]:
file_path += ".docx"
try:
# 如果是.docx文件创建一个基本的Word文档
if file_path.endswith('.docx'):
from docx import Document
doc = Document()
doc.add_paragraph(self.text_edit.toPlainText())
doc.save(file_path)
else:
# 对于其他格式,保持原有逻辑
with open(file_path, 'w', encoding='utf-8') as f:
f.write(self.text_edit.toPlainText())
self.current_file_path = file_path
self.is_modified = False
self.update_window_title()
self.status_bar.showMessage(f"已保存: {os.path.basename(file_path)}", 3000)
except Exception as e:
QMessageBox.critical(self, "错误", f"保存文件失败: {str(e)}")
def undo(self):
"""撤销"""
self.text_edit.undo()
def redo(self):
"""重做"""
self.text_edit.redo()
def cut(self):
"""剪切"""
self.text_edit.cut()
def copy(self):
"""复制"""
self.text_edit.copy()
def paste(self):
"""粘贴"""
self.text_edit.paste()
def toggle_reading_view(self):
"""切换阅读视图"""
# 这里可以实现阅读视图的逻辑
self.status_bar.showMessage("阅读视图功能开发中...", 3000)
def toggle_print_layout(self):
"""切换打印布局"""
# 这里可以实现打印布局的逻辑
self.status_bar.showMessage("打印布局功能开发中...", 3000)
def set_view_mode(self, mode):
"""设置视图模式 - 打开学习模式窗口或切换到打字模式"""
if mode not in ["typing", "learning"]:
return
if mode == "learning":
# 学习模式:打开新的学习模式窗口
try:
from learning_mode_window import LearningModeWindow
# 准备传递给学习窗口的参数
imported_content = ""
current_position = 0
# 如果有已导入的内容,传递给学习窗口
if hasattr(self, 'imported_content') and self.imported_content:
imported_content = self.imported_content
if hasattr(self, 'learning_progress') and self.learning_progress > 0:
current_position = self.learning_progress
else:
# 如果没有导入内容,检查当前打字模式的内容
current_text = self.text_edit.toPlainText()
if current_text and current_text != "在此输入您的内容...":
# 将打字模式的内容作为学习模式的导入内容
imported_content = current_text
current_position = 0
self.imported_content = current_text
# 准备图片数据
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)
# 连接学习模式窗口的关闭信号
self.learning_window.closed.connect(self.on_learning_mode_closed)
# 显示学习模式窗口
self.learning_window.show()
# 更新菜单状态
self.learning_mode_action.setChecked(True)
self.typing_mode_action.setChecked(False)
# 切换到打字模式(主窗口保持打字模式)
self.view_mode = "typing"
self.status_bar.showMessage("学习模式窗口已打开", 3000)
except ImportError as e:
QMessageBox.critical(self, "错误", f"无法加载学习模式窗口:\n{str(e)}")
except Exception as e:
QMessageBox.critical(self, "错误", f"打开学习模式窗口时出错:\n{str(e)}")
elif mode == "typing":
# 打字模式:保持当前窗口状态
self.view_mode = "typing"
self.typing_mode_action.setChecked(True)
self.learning_mode_action.setChecked(False)
self.status_bar.showMessage("当前为打字模式", 2000)
def on_learning_mode_closed(self):
"""学习模式窗口关闭时的回调"""
# 重置菜单状态
self.learning_mode_action.setChecked(False)
self.typing_mode_action.setChecked(True)
self.view_mode = "typing"
# 清除学习窗口引用
self.learning_window = None
self.status_bar.showMessage("学习模式窗口已关闭", 2000)
def on_learning_content_changed(self, new_content, position):
"""学习模式内容变化时的回调 - 只在末尾追加新内容"""
# 设置同步标记,防止递归调用
self.sync_from_learning = True
try:
# 只在末尾追加新输入的内容,不修改已有内容
if new_content:
# 直接在末尾追加新内容
cursor = self.text_edit.textCursor()
cursor.movePosition(QTextCursor.End)
cursor.insertText(new_content)
# 更新导入内容(但不覆盖用户额外输入的内容)
if self.imported_content:
self.imported_content += new_content
else:
self.imported_content = new_content
self.learning_progress = len(self.imported_content) if self.imported_content else 0
# 重置打字逻辑(不追踪进度)
if self.typing_logic:
self.typing_logic.imported_content = self.imported_content
self.typing_logic.current_index = self.learning_progress
self.typing_logic.typed_chars = self.learning_progress
# 更新状态栏
self.status_bar.showMessage(f"从学习模式同步新内容: {new_content}", 3000)
finally:
# 重置同步标记
self.sync_from_learning = False
def set_page_color(self, color):
"""设置页面颜色"""
color_map = {
'white': '#ffffff',
'light_blue': '#e6f3ff',
'light_yellow': '#fffde6',
'light_green': '#e6ffe6'
}
if color in color_map:
bg_color = color_map[color]
# 更新文本编辑区域的背景色
current_style = self.text_edit.styleSheet()
# 移除旧的背景色设置
import re
current_style = re.sub(r'background-color:\s*#[a-fA-F0-9]+;', '', current_style)
# 添加新的背景色设置
new_style = current_style + f"\nbackground-color: {bg_color};"
self.text_edit.setStyleSheet(new_style)
self.status_bar.showMessage(f"页面颜色已设置为{color}", 2000)
def set_page_margins(self, margin_type):
"""设置页面边距"""
margin_map = {
'normal': (50, 50, 50, 50),
'narrow': (20, 20, 20, 20),
'wide': (80, 80, 80, 80)
}
if margin_type in margin_map:
margins = margin_map[margin_type]
# 更新文档容器的边距
# text_edit的父级是document_layout父级的父级是document_container
container = self.text_edit.parent().parent() # 获取文档容器
if container and hasattr(container, 'layout'):
layout = container.layout()
if layout:
layout.setContentsMargins(margins[0], margins[1], margins[2], margins[3])
self.status_bar.showMessage(f"页面边距已设置为{margin_type}", 2000)
def zoom_in(self):
"""放大"""
self.adjust_zoom_level(10)
def zoom_out(self):
"""缩小"""
self.adjust_zoom_level(-10)
def zoom_100(self):
"""实际大小"""
self.set_zoom_level(100)
def set_zoom_level(self, level):
"""设置缩放级别"""
if 10 <= level <= 500: # 限制缩放范围在10%到500%之间
# 获取当前字体大小并调整
current_font = self.text_edit.currentFont()
base_size = 12 # 基准字体大小
new_size = base_size * (level / 100)
# 应用新的字体大小
current_font.setPointSizeF(new_size)
self.text_edit.setFont(current_font)
self.status_bar.showMessage(f"缩放级别: {level}%", 2000)
def adjust_zoom_level(self, delta):
"""调整缩放级别"""
# 获取当前字体大小
current_font = self.text_edit.currentFont()
current_size = current_font.pointSizeF()
base_size = 12 # 基准字体大小
# 计算当前缩放百分比
current_zoom = (current_size / base_size) * 100
new_zoom = current_zoom + delta
# 限制缩放范围
if 10 <= new_zoom <= 500:
self.set_zoom_level(int(new_zoom))
def toggle_grid_lines(self):
"""切换网格线显示"""
self.status_bar.showMessage("网格线功能开发中...", 3000)
def toggle_ruler(self):
"""切换标尺显示"""
self.status_bar.showMessage("标尺功能开发中...", 3000)
def show_find_dialog(self):
"""显示查找对话框"""
# 创建查找对话框
dialog = QDialog(self)
dialog.setWindowTitle("查找")
dialog.setFixedSize(400, 150)
layout = QVBoxLayout()
# 查找内容输入
find_layout = QHBoxLayout()
find_label = QLabel("查找内容:")
self.find_edit = QLineEdit()
find_layout.addWidget(find_label)
find_layout.addWidget(self.find_edit)
layout.addLayout(find_layout)
# 选项
options_layout = QHBoxLayout()
self.case_sensitive_checkbox = QCheckBox("区分大小写")
self.whole_words_checkbox = QCheckBox("全字匹配")
options_layout.addWidget(self.case_sensitive_checkbox)
options_layout.addWidget(self.whole_words_checkbox)
options_layout.addStretch()
layout.addLayout(options_layout)
# 按钮
buttons_layout = QHBoxLayout()
find_next_btn = QPushButton("查找下一个")
find_next_btn.clicked.connect(lambda: self.find_text(dialog))
cancel_btn = QPushButton("取消")
cancel_btn.clicked.connect(dialog.close)
buttons_layout.addWidget(find_next_btn)
buttons_layout.addWidget(cancel_btn)
layout.addLayout(buttons_layout)
dialog.setLayout(layout)
dialog.exec_()
def find_text(self, dialog):
"""执行查找操作"""
search_text = self.find_edit.text()
if not search_text:
QMessageBox.warning(self, "查找", "请输入查找内容")
return
# 获取当前光标位置
cursor = self.text_edit.textCursor()
start_pos = cursor.position()
# 设置查找选项
flags = QTextDocument.FindFlags()
if self.case_sensitive_checkbox.isChecked():
flags |= QTextDocument.FindCaseSensitively
if self.whole_words_checkbox.isChecked():
flags |= QTextDocument.FindWholeWords
# 执行查找
found_cursor = self.text_edit.document().find(search_text, start_pos, flags)
if found_cursor.isNull():
# 如果没找到,从文档开始处重新查找
found_cursor = self.text_edit.document().find(search_text, 0, flags)
if not found_cursor.isNull():
# 选中找到的文本
self.text_edit.setTextCursor(found_cursor)
self.text_edit.ensureCursorVisible()
else:
QMessageBox.information(self, "查找", f"找不到 '{search_text}'")
def show_replace_dialog(self):
"""显示替换对话框"""
# 创建替换对话框
dialog = QDialog(self)
dialog.setWindowTitle("替换")
dialog.setFixedSize(400, 200)
layout = QVBoxLayout()
# 查找内容输入
find_layout = QHBoxLayout()
find_label = QLabel("查找内容:")
self.replace_find_edit = QLineEdit()
find_layout.addWidget(find_label)
find_layout.addWidget(self.replace_find_edit)
layout.addLayout(find_layout)
# 替换为输入
replace_layout = QHBoxLayout()
replace_label = QLabel("替换为:")
self.replace_edit = QLineEdit()
replace_layout.addWidget(replace_label)
replace_layout.addWidget(self.replace_edit)
layout.addLayout(replace_layout)
# 选项
options_layout = QHBoxLayout()
self.replace_case_sensitive_checkbox = QCheckBox("区分大小写")
self.replace_whole_words_checkbox = QCheckBox("全字匹配")
options_layout.addWidget(self.replace_case_sensitive_checkbox)
options_layout.addWidget(self.replace_whole_words_checkbox)
options_layout.addStretch()
layout.addLayout(options_layout)
# 按钮
buttons_layout = QHBoxLayout()
find_next_btn = QPushButton("查找下一个")
replace_btn = QPushButton("替换")
replace_all_btn = QPushButton("全部替换")
cancel_btn = QPushButton("取消")
find_next_btn.clicked.connect(lambda: self.find_text_for_replace(dialog))
replace_btn.clicked.connect(lambda: self.replace_text(dialog))
replace_all_btn.clicked.connect(lambda: self.replace_all_text(dialog))
cancel_btn.clicked.connect(dialog.close)
buttons_layout.addWidget(find_next_btn)
buttons_layout.addWidget(replace_btn)
buttons_layout.addWidget(replace_all_btn)
buttons_layout.addWidget(cancel_btn)
layout.addLayout(buttons_layout)
dialog.setLayout(layout)
dialog.exec_()
def find_text_for_replace(self, dialog):
"""在替换对话框中执行查找操作"""
search_text = self.replace_find_edit.text()
if not search_text:
QMessageBox.warning(self, "查找", "请输入查找内容")
return
# 获取当前光标位置
cursor = self.text_edit.textCursor()
start_pos = cursor.position()
# 设置查找选项
flags = QTextDocument.FindFlags()
if self.replace_case_sensitive_checkbox.isChecked():
flags |= QTextDocument.FindCaseSensitively
if self.replace_whole_words_checkbox.isChecked():
flags |= QTextDocument.FindWholeWords
# 执行查找
found_cursor = self.text_edit.document().find(search_text, start_pos, flags)
if found_cursor.isNull():
# 如果没找到,从文档开始处重新查找
found_cursor = self.text_edit.document().find(search_text, 0, flags)
if not found_cursor.isNull():
# 选中找到的文本
self.text_edit.setTextCursor(found_cursor)
self.text_edit.ensureCursorVisible()
else:
QMessageBox.information(self, "查找", f"找不到 '{search_text}'")
def replace_text(self, dialog):
"""替换当前选中的文本"""
search_text = self.replace_find_edit.text()
replace_text = self.replace_edit.text()
# 检查是否有选中的文本且与查找内容匹配
cursor = self.text_edit.textCursor()
selected_text = cursor.selectedText()
# 检查是否匹配(考虑大小写敏感选项)
match = False
if self.replace_case_sensitive_checkbox.isChecked():
match = selected_text == search_text
else:
match = selected_text.lower() == search_text.lower()
if match:
# 替换选中的文本
cursor.insertText(replace_text)
# 继续查找下一个
self.find_text_for_replace(dialog)
else:
# 如果没有匹配的选中文本,执行查找
self.find_text_for_replace(dialog)
def replace_all_text(self, dialog):
"""替换所有匹配的文本"""
search_text = self.replace_find_edit.text()
replace_text = self.replace_edit.text()
if not search_text:
QMessageBox.warning(self, "替换", "请输入查找内容")
return
# 设置查找选项
flags = QTextDocument.FindFlags()
if self.replace_case_sensitive_checkbox.isChecked():
flags |= QTextDocument.FindCaseSensitively
if self.replace_whole_words_checkbox.isChecked():
flags |= QTextDocument.FindWholeWords
# 保存当前光标位置
original_cursor = self.text_edit.textCursor()
# 从文档开始处查找并替换所有匹配项
count = 0
cursor = self.text_edit.textCursor()
cursor.movePosition(QTextCursor.Start)
self.text_edit.setTextCursor(cursor)
while True:
found_cursor = self.text_edit.document().find(search_text, cursor, flags)
if found_cursor.isNull():
break
# 替换文本
found_cursor.insertText(replace_text)
count += 1
cursor = found_cursor
# 恢复原始光标位置
self.text_edit.setTextCursor(original_cursor)
# 显示替换结果
QMessageBox.information(self, "替换", f"已完成 {count} 处替换。")
def show_about(self):
"""显示关于对话框"""
# 创建自定义对话框
dialog = QDialog(self)
dialog.setWindowTitle("关于 MagicWord")
dialog.setModal(True)
dialog.setFixedSize(500, 400)
# 创建布局
layout = QVBoxLayout()
# 添加图标和标题布局
header_layout = QHBoxLayout()
# 添加应用程序图标
try:
icon_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "resources", "icons", "app_icon_128X128.png")
if os.path.exists(icon_path):
icon_label = QLabel()
icon_label.setAlignment(Qt.AlignCenter)
pixmap = QPixmap(icon_path)
if not pixmap.isNull():
# 缩放图标到合适大小
scaled_pixmap = pixmap.scaled(80, 80, Qt.KeepAspectRatio, Qt.SmoothTransformation)
icon_label.setPixmap(scaled_pixmap)
header_layout.addWidget(icon_label)
except Exception as e:
print(f"加载图标失败: {e}")
# 添加标题和版本信息
title_layout = QVBoxLayout()
title_label = QLabel("MagicWord")
title_label.setStyleSheet("font-size: 24px; font-weight: bold; color: #0078d7;")
title_label.setAlignment(Qt.AlignCenter)
version_label = QLabel("版本 1.0")
version_label.setStyleSheet("font-size: 14px; color: #666;")
version_label.setAlignment(Qt.AlignCenter)
title_layout.addWidget(title_label)
title_layout.addWidget(version_label)
header_layout.addLayout(title_layout)
layout.addLayout(header_layout)
# 添加分隔线
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setFrameShadow(QFrame.Sunken)
separator.setStyleSheet("color: #ddd;")
layout.addWidget(separator)
# 添加关于信息
about_text = QLabel(
"隐私学习软件\n\n"
"基于 Microsoft Word 界面设计\n\n"
"功能特色:\n"
"• 仿Word界面设计\n"
"• 隐私学习模式\n"
"• 多格式文档支持\n"
"• 实时进度跟踪\n"
"• 天气和名言显示"
)
about_text.setAlignment(Qt.AlignCenter)
about_text.setStyleSheet("font-size: 12px; line-height: 1.5;")
layout.addWidget(about_text)
# 添加按钮布局
button_layout = QHBoxLayout()
button_layout.addStretch() # 添加弹性空间使按钮居中
# 添加"沃式烁"按钮
woshishuo_button = QPushButton("沃式烁")
woshishuo_button.clicked.connect(lambda: self.show_woshishuo_image())
button_layout.addWidget(woshishuo_button)
# 添加OK按钮
ok_button = QPushButton("OK")
ok_button.clicked.connect(dialog.accept)
button_layout.addWidget(ok_button)
button_layout.addStretch() # 添加弹性空间使按钮居中
layout.addLayout(button_layout)
dialog.setLayout(layout)
# 显示对话框
dialog.exec_()
def show_woshishuo_image(self):
"""显示沃式烁图片"""
try:
# 图片路径
image_path = os.path.join(os.path.dirname(__file__), "ui", "114514.png")
# 检查图片是否存在
if not os.path.exists(image_path):
QMessageBox.warning(self, "错误", "图片文件不存在: 114514.png")
return
# 创建图片查看对话框
image_dialog = QDialog(self)
image_dialog.setWindowTitle("沃式烁")
image_dialog.setModal(True)
image_dialog.setFixedSize(500, 400)
# 创建布局
layout = QVBoxLayout()
# 添加图片标签
image_label = QLabel()
image_label.setAlignment(Qt.AlignCenter)
image_label.setScaledContents(True)
# 加载图片
pixmap = QPixmap(image_path)
if pixmap.isNull():
QMessageBox.warning(self, "错误", "无法加载图片文件")
return
# 缩放图片以适应对话框
scaled_pixmap = pixmap.scaled(450, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation)
image_label.setPixmap(scaled_pixmap)
layout.addWidget(image_label)
# 添加关闭按钮
close_button = QPushButton("关闭")
close_button.clicked.connect(image_dialog.accept)
layout.addWidget(close_button)
image_dialog.setLayout(layout)
image_dialog.exec_()
except Exception as e:
QMessageBox.warning(self, "错误", f"显示图片时出错: {str(e)}")
def highlight_next_char(self, position, expected_char):
"""高亮显示下一个期望字符"""
if position < len(self.typing_logic.learning_content):
# 获取当前光标位置
cursor = self.text_edit.textCursor()
cursor.setPosition(position)
# 选择下一个字符
cursor.movePosition(cursor.Right, cursor.KeepAnchor, 1)
# 设置高亮格式
format = QTextCharFormat()
format.setBackground(QColor(255, 255, 0, 128)) # 黄色半透明背景
format.setForeground(QColor(255, 0, 0)) # 红色文字
format.setFontWeight(QFont.Bold)
# 应用格式
cursor.setCharFormat(format)
def on_lesson_complete(self):
"""课程完成处理"""
stats = self.typing_logic.get_statistics()
QMessageBox.information(
self, "恭喜",
"恭喜完成本课程学习!\n\n"
f"准确率: {stats['accuracy_rate']*100:.1f}%\n"
f"总字符数: {stats['total_chars']}\n"
f"错误次数: {stats['error_count']}"
)
# 重置状态
if self.typing_logic:
self.typing_logic.reset()
def on_image_item_double_clicked(self, item):
"""双击图片项时显示大图"""
try:
# 获取图片索引
row = self.image_list_widget.row(item)
if 0 <= row < len(self.extracted_images):
image_filename, image_data = self.extracted_images[row]
self.show_image_viewer(image_filename, image_data)
except Exception as e:
self.status_bar.showMessage(f"显示图片失败: {str(e)}", 3000)
def set_light_mode(self):
"""设置为白色模式"""
theme_manager.set_dark_theme(False)
self.light_mode_action.setChecked(True)
self.dark_mode_action.setChecked(False)
self.status_bar.showMessage("已切换到白色模式", 2000)
def set_dark_mode(self):
"""设置为黑色模式"""
theme_manager.set_dark_theme(True)
self.light_mode_action.setChecked(False)
self.dark_mode_action.setChecked(True)
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.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_bar.showMessage(f"加载图片失败: {filename}", 3000)
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_bar.showMessage(f"创建图片查看器失败: {str(e)}", 3000)
import traceback
traceback.print_exc()
def insert_images_in_text(self):
"""在文本中插入图片 - 修复图片显示逻辑"""
try:
if not self.typing_logic or not hasattr(self.typing_logic, 'image_positions'):
print("打字逻辑或图片位置信息不存在")
return
# 添加调试信息
print(f"当前显示字符数: {self.displayed_chars}")
print(f"图片位置信息数量: {len(self.typing_logic.image_positions)}")
print(f"图片数据数量: {len(self.typing_logic.image_data) if hasattr(self.typing_logic, 'image_data') else 0}")
# 检查是否已经插入过图片(避免重复插入)
if not hasattr(self, 'inserted_images'):
self.inserted_images = set()
# 获取当前显示的文本
current_text = self.text_edit.toPlainText()
current_length = len(current_text)
# 获取需要显示的图片列表
images_to_display = self.typing_logic.get_images_to_display(self.displayed_chars)
# 添加调试信息
print(f"需要显示的图片数量: {len(images_to_display)}")
if images_to_display:
for img in images_to_display:
print(f"图片信息: {img.get('filename', 'unknown')} at pos {img.get('start_pos', -1)}-{img.get('end_pos', -1)}")
# 检查当前显示位置是否有图片需要插入
for image_info in images_to_display:
image_key = f"{image_info['start_pos']}_{image_info['filename']}"
# 跳过已经插入过的图片
if image_key in self.inserted_images:
continue
# 当打字进度达到图片位置时插入图片 - 修复条件,确保图片能显示
if (self.displayed_chars >= image_info['start_pos'] or
(self.displayed_chars >= max(1, image_info['start_pos'] - 20) and self.displayed_chars > 0)):
# 在图片位置插入图片
cursor = self.text_edit.textCursor()
# 计算图片应该插入的位置(基于原始内容位置)
insert_position = image_info['start_pos']
# 确保插入位置有效(不能超过当前显示内容长度)
if insert_position >= 0 and insert_position <= current_length:
cursor.setPosition(insert_position)
# 创建图片格式
image_format = QTextImageFormat()
# 获取图片数据优先使用typing_logic中的数据
image_data = None
if hasattr(self.typing_logic, 'image_data') and image_info['filename'] in self.typing_logic.image_data:
image_data = self.typing_logic.image_data[image_info['filename']]
else:
image_data = image_info.get('data')
if image_data:
# 加载图片数据
pixmap = QPixmap()
if pixmap.loadFromData(image_data):
# 调整图片大小
scaled_pixmap = pixmap.scaled(200, 150, Qt.KeepAspectRatio, Qt.SmoothTransformation)
# 将图片保存到临时文件(使用更稳定的路径)
import tempfile
import os
temp_dir = tempfile.gettempdir()
# 确保文件名安全
safe_filename = "".join(c for c in image_info['filename'] if c.isalnum() or c in ('.', '_', '-'))
temp_file = os.path.join(temp_dir, safe_filename)
if scaled_pixmap.save(temp_file):
# 设置图片格式
image_format.setName(temp_file)
image_format.setWidth(200)
image_format.setHeight(150)
# 在光标位置插入图片
cursor.insertImage(image_format)
# 在图片后插入一个空格,让文字继续
cursor.insertText(" ")
# 标记这张图片已经插入过
self.inserted_images.add(image_key)
# 记录插入成功
print(f"图片 {image_info['filename']} 已在位置 {insert_position} 插入")
else:
print(f"保存临时图片文件失败: {temp_file}")
else:
print(f"加载图片数据失败: {image_info['filename']}")
# 重新设置光标到文本末尾
cursor.movePosition(cursor.End)
self.text_edit.setTextCursor(cursor)
except Exception as e:
print(f"插入图片失败: {str(e)}")
import traceback
traceback.print_exc()
def check_and_show_image_at_position(self, position):
"""检查指定位置是否有图片并显示 - 现在只在文本中显示,不弹出窗口"""
# 这个方法现在不需要了,因为图片会直接插入到文本中
pass
def show_image_at_position(self, image_info):
"""在指定位置显示图片 - 现在不需要弹出窗口了"""
# 这个方法现在不需要了,因为图片会直接插入到文本中
pass
def insert_image_in_typing_mode(self):
"""在打字模式下插入图片"""
try:
# 检查当前是否在打字模式下
if self.view_mode != "typing":
self.status_bar.showMessage("请在打字模式下使用插入图片功能", 3000)
return
# 打开文件对话框选择图片
from PyQt5.QtWidgets import QFileDialog
file_path, _ = QFileDialog.getOpenFileName(
self,
"选择图片文件",
"",
"图片文件 (*.png *.jpg *.jpeg *.bmp *.gif *.ico)"
)
if not file_path:
return
# 加载图片文件
pixmap = QPixmap(file_path)
if pixmap.isNull():
self.status_bar.showMessage("无法加载图片文件", 3000)
return
# 获取当前光标位置
cursor = self.text_edit.textCursor()
# 创建图片格式
image_format = QTextImageFormat()
# 调整图片大小
scaled_pixmap = pixmap.scaled(200, 150, Qt.KeepAspectRatio, Qt.SmoothTransformation)
# 将图片保存到临时文件
import tempfile
import os
temp_dir = tempfile.gettempdir()
filename = os.path.basename(file_path)
safe_filename = "".join(c for c in filename if c.isalnum() or c in ('.', '_', '-'))
temp_file = os.path.join(temp_dir, safe_filename)
if scaled_pixmap.save(temp_file):
# 设置图片格式
image_format.setName(temp_file)
image_format.setWidth(200)
image_format.setHeight(150)
# 在光标位置插入图片
cursor.insertImage(image_format)
# 在图片后插入一个空格,让文字继续
cursor.insertText(" ")
# 标记文档为已修改
if not self.is_modified:
self.is_modified = True
self.update_window_title()
# 显示成功消息
self.status_bar.showMessage(f"图片已插入: {filename}", 3000)
# 添加到临时文件列表以便清理
self.temp_files.append(temp_file)
else:
self.status_bar.showMessage("保存临时图片文件失败", 3000)
except Exception as e:
self.status_bar.showMessage(f"插入图片失败: {str(e)}", 3000)
import traceback
traceback.print_exc()
def closeEvent(self, event):
"""关闭事件处理"""
# 清理临时文件
self.cleanup_temp_files()
if self.is_modified:
reply = QMessageBox.question(
self, "确认退出",
"文档已修改,是否保存更改?",
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
)
if reply == QMessageBox.Save:
self.save_file()
event.accept()
elif reply == QMessageBox.Discard:
event.accept()
else:
event.ignore()
else:
event.accept()
def cleanup_temp_files(self):
"""清理临时文件"""
import os
for temp_file in self.temp_files:
try:
if os.path.exists(temp_file):
os.remove(temp_file)
print(f"已删除临时文件: {temp_file}")
except Exception as e:
print(f"删除临时文件失败 {temp_file}: {str(e)}")
self.temp_files.clear()
def export_as_html(self):
"""导出为HTML"""
file_path, _ = QFileDialog.getSaveFileName(
self, "导出为HTML", "", "HTML文件 (*.html);;所有文件 (*.*)"
)
if file_path:
try:
# 获取当前文本内容
content = self.text_edit.toPlainText()
# 处理图片标签
html_body = ""
lines = content.split('\n')
for line in lines:
if line.strip().startswith('[图片:') and line.strip().endswith(']'):
# 提取图片文件名
img_name = line.strip()[4:-1].strip()
# 查找对应的图片数据
img_data = None
for filename, image_data in self.extracted_images:
if filename == img_name:
img_data = image_data
break
if img_data:
# 创建图片的base64编码
import base64
img_base64 = base64.b64encode(img_data).decode('utf-8')
# 检测图片类型
if img_name.lower().endswith('.png'):
img_type = 'png'
elif img_name.lower().endswith(('.jpg', '.jpeg')):
img_type = 'jpeg'
elif img_name.lower().endswith('.gif'):
img_type = 'gif'
else:
img_type = 'png' # 默认
html_body += f'<div class="image-container">\n'
html_body += f'<img src="data:image/{img_type};base64,{img_base64}" alt="{img_name}" style="max-width: 100%; height: auto; margin: 10px 0;">\n'
html_body += f'<p class="image-caption">{img_name}</p>\n'
html_body += f'</div>\n'
else:
html_body += f'<p class="image-placeholder">[图片: {img_name}]</p>\n'
else:
# 普通文本,使用段落标签
if line.strip():
html_body += f'<p>{line}</p>\n'
else:
html_body += '<br>\n'
# 创建完整的HTML内容
html_content = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MagicWord文档</title>
<style>
body {{
font-family: 'Calibri', 'Microsoft YaHei', '微软雅黑', sans-serif;
font-size: 12pt;
line-height: 1.6;
margin: 40px;
background-color: #ffffff;
color: #000000;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}}
.container {{
padding: 20px;
}}
p {{
margin: 10px 0;
white-space: pre-wrap;
}}
.image-container {{
text-align: center;
margin: 20px 0;
padding: 15px;
border: 1px solid #e0e0e0;
border-radius: 5px;
background-color: #f9f9f9;
}}
.image-container img {{
max-width: 100%;
height: auto;
border-radius: 3px;
}}
.image-caption {{
margin-top: 8px;
font-size: 10pt;
color: #666;
font-style: italic;
}}
.image-placeholder {{
background-color: #f0f0f0;
border: 2px dashed #ccc;
padding: 20px;
text-align: center;
color: #999;
font-style: italic;
margin: 20px 0;
}}
</style>
</head>
<body>
<div class="container">
{html_body}
</div>
</body>
</html>"""
# 写入HTML文件
with open(file_path, 'w', encoding='utf-8') as f:
f.write(html_content)
self.status_bar.showMessage(f"已导出为HTML: {os.path.basename(file_path)}", 3000)
except Exception as e:
QMessageBox.critical(self, "错误", f"导出HTML失败: {str(e)}")
def export_as_pdf(self):
"""导出为PDF"""
file_path, _ = QFileDialog.getSaveFileName(
self, "导出为PDF", "", "PDF文件 (*.pdf);;所有文件 (*.*)"
)
if file_path:
try:
from PyQt5.QtGui import QTextDocument
from PyQt5.QtPrintSupport import QPrinter
# 获取当前文本内容
content = self.text_edit.toPlainText()
# 处理图片标签 - 创建HTML内容以便更好地处理图片
html_content = "<html><body style='font-family: Calibri, Microsoft YaHei, 微软雅黑, sans-serif; font-size: 12pt; line-height: 1.6; margin: 40px;'>"
lines = content.split('\n')
for line in lines:
if line.strip().startswith('[图片:') and line.strip().endswith(']'):
# 提取图片文件名
img_name = line.strip()[4:-1].strip()
# 在PDF中显示图片占位符
html_content += f'<div style="text-align: center; margin: 20px 0; padding: 15px; border: 1px solid #e0e0e0; border-radius: 5px; background-color: #f9f9f9;">'
html_content += f'<div style="background-color: #f0f0f0; border: 2px dashed #ccc; padding: 20px; text-align: center; color: #999; font-style: italic;">'
html_content += f'[图片: {img_name}]'
html_content += f'</div>'
html_content += f'<p style="margin-top: 8px; font-size: 10pt; color: #666; font-style: italic;">{img_name}</p>'
html_content += f'</div>'
else:
# 普通文本,使用段落标签
if line.strip():
html_content += f'<p style="margin: 10px 0; white-space: pre-wrap;">{line}</p>'
else:
html_content += '<br>'
html_content += "</body></html>"
# 创建文本文档
doc = QTextDocument()
doc.setHtml(html_content)
# 设置文档样式
doc.setDefaultFont(self.text_edit.currentFont())
# 创建PDF打印机
printer = QPrinter(QPrinter.HighResolution)
printer.setOutputFormat(QPrinter.PdfFormat)
printer.setOutputFileName(file_path)
printer.setPageSize(QPrinter.A4)
printer.setPageMargins(20, 20, 20, 20, QPrinter.Millimeter)
# 打印文档到PDF
doc.print_(printer)
self.status_bar.showMessage(f"已导出为PDF: {os.path.basename(file_path)}", 3000)
except Exception as e:
QMessageBox.critical(self, "错误", f"导出PDF失败: {str(e)}")
def export_as_txt(self):
"""导出为TXT"""
file_path, _ = QFileDialog.getSaveFileName(
self, "导出为TXT", "", "文本文档 (*.txt);;所有文件 (*.*)"
)
if file_path:
try:
# 获取当前文本内容
content = self.text_edit.toPlainText()
# 处理图片标签 - 在TXT中保留图片标记
processed_content = content
# 写入TXT文件
with open(file_path, 'w', encoding='utf-8') as f:
f.write(processed_content)
self.status_bar.showMessage(f"已导出为TXT: {os.path.basename(file_path)}", 3000)
except Exception as e:
QMessageBox.critical(self, "错误", f"导出TXT失败: {str(e)}")
def export_as_docx(self):
"""导出为DOCX"""
file_path, _ = QFileDialog.getSaveFileName(
self, "导出为DOCX", "", "Word文档 (*.docx);;所有文件 (*.*)"
)
if file_path:
try:
import os
import tempfile
from docx import Document
from docx.shared import Inches
# 创建Word文档
doc = Document()
# 获取当前文本内容
content = self.text_edit.toPlainText()
lines = content.split('\n')
# 逐行处理内容
for line in lines:
if line.strip().startswith('[图片:') and line.strip().endswith(']'):
# 提取图片文件名
img_name = line.strip()[4:-1].strip()
# 查找对应的图片数据
img_data = None
for filename, image_data in self.extracted_images:
if filename == img_name:
img_data = image_data
break
if img_data:
# 检测图片类型
if img_name.lower().endswith('.png'):
img_ext = '.png'
elif img_name.lower().endswith(('.jpg', '.jpeg')):
img_ext = '.jpg'
elif img_name.lower().endswith('.gif'):
img_ext = '.gif'
else:
img_ext = '.png' # 默认
# 创建临时文件
with tempfile.NamedTemporaryFile(mode='wb', suffix=img_ext, delete=False) as temp_file:
temp_file.write(img_data)
temp_img_path = temp_file.name
try:
# 在Word文档中添加图片
doc.add_picture(temp_img_path, width=Inches(4))
# 添加图片说明
doc.add_paragraph(f'图片: {img_name}')
finally:
# 清理临时文件
if os.path.exists(temp_img_path):
os.remove(temp_img_path)
else:
# 图片未找到,添加占位符文本
doc.add_paragraph(f'[图片: {img_name}]')
else:
# 普通文本
if line.strip():
doc.add_paragraph(line)
else:
# 空行,添加空段落
doc.add_paragraph()
# 保存文档
doc.save(file_path)
self.status_bar.showMessage(f"已导出为DOCX: {os.path.basename(file_path)}", 3000)
except ImportError:
QMessageBox.critical(self, "错误", "需要安装python-docx库才能导出DOCX文件")
except Exception as e:
QMessageBox.critical(self, "错误", f"导出DOCX失败: {str(e)}")
def extract_and_display_images(self, file_path=None, images=None):
"""提取并显示Word文档中的图片 - 修复图片位置计算"""
try:
# 如果没有提供图片数据,则从文件中提取
if images is None:
if file_path is None:
return
# 提取图片
images = FileParser.extract_images_from_docx(file_path)
if not images:
return
# 清空之前的图片
self.extracted_images.clear()
self.image_list_widget.clear()
# 保存提取的图片
self.extracted_images.extend(images)
# 创建图片位置信息列表
image_positions = []
# 显示图片列表
self.image_list_widget.setVisible(True)
self.image_list_widget.setMaximumHeight(150)
# 添加图片项到列表
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.setText(f"{filename} ({pixmap.width()}x{pixmap.height()})")
item.setIcon(QIcon(thumbnail))
item.setData(Qt.UserRole, filename) # 保存文件名到数据
self.image_list_widget.addItem(item)
else:
# 如果无法加载图片,显示默认文本
item = QListWidgetItem(f"{filename} (无法预览)")
item.setData(Qt.UserRole, filename)
self.image_list_widget.addItem(item)
# 为每张图片创建位置信息 - 修复位置计算,确保早期显示
content_length = len(self.imported_content)
#if content_length == 0:
#content_length = len(content) if 'content' in locals() else 1000 # 备用长度
# 修复图片位置计算,确保图片能在用户早期打字时显示
if len(images) == 1:
# 只有一张图片放在文档开始位置附近前10%),确保用户能快速看到
start_pos = max(10, content_length // 10)
else:
# 多张图片:前几张放在较前位置,确保用户能看到
if index < 3:
# 前3张图片放在文档前30%
segment = content_length // 3
start_pos = max(10, segment * (index + 1) // 4)
else:
# 其余图片均匀分布
remaining_start = content_length // 2
remaining_index = index - 3
remaining_count = len(images) - 3
if remaining_count > 0:
segment = (content_length - remaining_start) // (remaining_count + 1)
start_pos = remaining_start + segment * (remaining_index + 1)
else:
start_pos = content_length // 2
end_pos = min(start_pos + 50, content_length)
image_positions.append({
'start_pos': start_pos,
'end_pos': end_pos,
'data': image_data,
'filename': filename
})
# 设置图片位置信息到打字逻辑
if self.typing_logic:
self.typing_logic.set_image_positions(image_positions)
# 设置图片数据到打字逻辑
image_data_dict = {}
for filename, image_data in images:
image_data_dict[filename] = image_data
self.typing_logic.set_image_data(image_data_dict)
# 添加调试信息
print(f"已设置 {len(image_positions)} 个图片位置和 {len(image_data_dict)} 个图片数据到打字逻辑")
# 更新状态栏
self.status_bar.showMessage(f"已提取 {len(images)} 张图片,双击查看大图", 5000)
except Exception as e:
self.status_bar.showMessage(f"提取图片失败: {str(e)}", 3000)
def update_format_buttons(self):
"""更新格式按钮的状态,根据当前光标位置的格式"""
try:
# 获取当前光标位置的字符格式
cursor = self.text_edit.textCursor()
char_format = cursor.charFormat()
block_format = cursor.blockFormat()
# 更新粗体按钮状态
if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'bold_btn'):
is_bold = char_format.font().weight() == QFont.Bold
self.ribbon.bold_btn.setChecked(is_bold)
# 更新斜体按钮状态
if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'italic_btn'):
is_italic = char_format.font().italic()
self.ribbon.italic_btn.setChecked(is_italic)
# 更新下划线按钮状态
if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'underline_btn'):
is_underline = char_format.font().underline()
self.ribbon.underline_btn.setChecked(is_underline)
# 更新对齐按钮状态
if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'align_left_btn'):
alignment = block_format.alignment()
self.ribbon.align_left_btn.setChecked(alignment == Qt.AlignLeft)
self.ribbon.align_center_btn.setChecked(alignment == Qt.AlignCenter)
self.ribbon.align_right_btn.setChecked(alignment == Qt.AlignRight)
self.ribbon.align_justify_btn.setChecked(alignment == Qt.AlignJustify)
except Exception as e:
print(f"更新格式按钮状态时出错: {e}")
def resizeEvent(self, event):
"""窗口大小改变事件处理"""
super().resizeEvent(event)
# 如果日历组件可见,调整其大小和位置以适应窗口底部
if hasattr(self, 'calendar_widget') and self.calendar_widget.isVisible():
calendar_height = 350 # 增加高度以确保所有日期都能完整显示
self.calendar_widget.setGeometry(0, self.height() - calendar_height,
self.width(), calendar_height)
def toggle_calendar(self):
"""切换日历组件的显示/隐藏状态"""
if hasattr(self, 'calendar_widget'):
if self.calendar_widget.isVisible():
self.calendar_widget.hide()
else:
# 设置日历组件位置在窗口底部
calendar_height = 350 # 增加高度以确保所有日期都能完整显示
# 将日历组件放置在窗口底部,占据整个宽度
self.calendar_widget.setGeometry(0, self.height() - calendar_height,
self.width(), calendar_height)
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)
# 设置应用程序样式
app.setStyle('Windows')
# 创建并显示主窗口
window = WordStyleMainWindow()
window.show()
sys.exit(app.exec_())