From 450937ff6561c2b90600a38a5a80d1427af84a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E5=AD=90=E6=98=82?= <929110464@qq.com> Date: Thu, 23 Oct 2025 12:49:32 +0800 Subject: [PATCH 1/3] =?UTF-8?q?UI=E4=BC=98=E5=8C=961?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/word_style_ui.py | 3 +- src/word_main_window.py | 254 +++++++++++++++++++++++++++++++--------- 2 files changed, 198 insertions(+), 59 deletions(-) diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index 0d90ce2..3132158 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -445,7 +445,8 @@ class WordTextEdit(QTextEdit): """) # 设置页面边距和背景 - self.setViewportMargins(50, 50, 50, 50) + # Keep editor background transparent; page container provides the white background and margins + self.setViewportMargins(0, 0, 0, 0) # 设置默认字体 font = QFont("Calibri", 12) diff --git a/src/word_main_window.py b/src/word_main_window.py index 17642a4..f3b7441 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -226,6 +226,23 @@ class WordStyleMainWindow(QMainWindow): background-color: #f3f2f1; } """) + + def init_network_services(self): + """ + 初始化网络服务:启动天气和每日一言的后台线程并连接信号 + """ + try: + # 天气线程 + 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() + except Exception as e: + print(f"初始化网络服务失败: {e}") def create_menu_bar(self): """创建菜单栏""" @@ -395,47 +412,35 @@ class WordStyleMainWindow(QMainWindow): } """) - # 创建文档容器 + # 创建文档容器(用于多页布局) 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("在此输入您的内容...") - - document_layout.addWidget(self.text_edit) - document_container.setLayout(document_layout) - + container_layout = QHBoxLayout() + container_layout.setContentsMargins(0, 40, 0, 40) # workspace vertical padding + container_layout.setSpacing(0) + + # 中心列,垂直堆叠多个 page_frame + pages_column = QWidget() + pages_column_layout = QVBoxLayout() + pages_column_layout.setContentsMargins(0, 0, 0, 0) + pages_column_layout.setSpacing(20) # 间隔表示页间距 + pages_column.setLayout(pages_column_layout) + + # 保存引用以便后续构建/清理页面 + self._pages_column_layout = pages_column_layout + self._page_frames = [] + + # center the pages_column horizontally inside document_container + container_layout.addStretch() + container_layout.addWidget(pages_column) + container_layout.addStretch() + document_container.setLayout(container_layout) + + # 将 document_container 放入 scroll_area 并添加到 main_layout scroll_area.setWidget(document_container) main_layout.addWidget(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() + + # 构建首个空白页面(后续会根据内容重建) + self.build_pages("") def init_typing_logic(self): """初始化打字逻辑""" @@ -443,6 +448,103 @@ class WordStyleMainWindow(QMainWindow): default_content = "欢迎使用MagicWord隐私学习软件!\n\n这是一个仿Microsoft Word界面的学习工具。" self.typing_logic = TypingLogic(default_content) self.typing_logic.reset() + + def clear_pages(self): + """清理已创建的页面控件""" + try: + while self._page_frames: + frame = self._page_frames.pop() + # 从布局中移除并删除 + self._pages_column_layout.removeWidget(frame) + frame.setParent(None) + except Exception: + pass + + def build_pages(self, full_text): + """ + 根据传入文本按近似字符数分页并在界面中渲染多个 page_frame。 + 这是一个近似实现:基于字体度量估算每页可容纳字符数,然后按字符切分。 + """ + from PyQt5.QtWidgets import QTextEdit, QLabel + from PyQt5.QtGui import QFontMetrics + + # 清理旧页面 + self.clear_pages() + + # 基本页面尺寸与内边距(应与 create_document_area 中一致) + page_width = 816 + page_height = 1056 + page_margin = 72 + + inner_width = page_width - page_margin * 2 + inner_height = page_height - page_margin * 2 + + # 使用默认字体度量进行估算 + font = QFont("Calibri", 12) + fm = QFontMetrics(font) + avg_char_width = max(1, fm.averageCharWidth()) + line_height = fm.lineSpacing() + + chars_per_line = max(1, int(inner_width / avg_char_width)) + lines_per_page = max(1, int(inner_height / line_height)) + chars_per_page = chars_per_line * lines_per_page + + # 防止 chars_per_page 过小 + if chars_per_page < 200: + chars_per_page = 2000 + + # 切分文本 + pages = [] + if not full_text: + pages = [""] + else: + i = 0 + L = len(full_text) + while i < L: + pages.append(full_text[i:i+chars_per_page]) + i += chars_per_page + + # 为每页创建 page_frame + for idx, page_text in enumerate(pages, start=1): + page_frame = QFrame() + page_frame.setObjectName('page_frame') + page_frame.setFixedWidth(page_width) + page_frame.setFixedHeight(page_height) + page_layout = QVBoxLayout() + page_layout.setContentsMargins(page_margin, page_margin, page_margin, page_margin) + page_layout.setSpacing(0) + + # 文本编辑器(每页一个) + page_edit = QTextEdit() + page_edit.setPlainText(page_text) + page_edit.setFont(font) + page_edit.setStyleSheet("background-color: transparent; border: none;") + page_edit.setLineWrapMode(QTextEdit.WidgetWidth) + page_edit.setMinimumHeight(inner_height) + + page_layout.addWidget(page_edit) + + # 页码标签(居中) + page_number_label = QLabel(f"{idx}") + page_number_label.setAlignment(Qt.AlignCenter) + page_number_label.setStyleSheet("color: #666666; font-size: 11px;") + page_layout.addWidget(page_number_label) + + page_frame.setLayout(page_layout) + page_frame.setStyleSheet("QFrame#page_frame { background: #ffffff; border: 1px solid #dcdcdc; border-radius: 2px; }") + + # 将 page_frame 添加到列布局 + self._pages_column_layout.addWidget(page_frame) + self._page_frames.append(page_frame) + + # 绑定第一个页面的编辑器为 self.text_edit(保持老逻辑) + try: + first_frame = self._page_frames[0] + first_edit = first_frame.findChild(QTextEdit) + if first_edit: + self.text_edit = first_edit + except Exception: + pass def connect_signals(self): """连接信号和槽""" @@ -845,7 +947,8 @@ class WordStyleMainWindow(QMainWindow): def new_document(self): """新建文档""" - self.text_edit.clear() + # 构建空白页面 + self.build_pages("") self.current_file_path = None self.is_modified = False self.update_window_title() @@ -869,29 +972,32 @@ class WordStyleMainWindow(QMainWindow): if content: # 设置文件加载标志 self.is_loading_file = True - - # 存储完整内容但不立即显示 + + # 存储完整内容 self.imported_content = content self.displayed_chars = 0 - + # 设置学习内容到打字逻辑 if self.typing_logic: self.typing_logic.reset(content) # 重置打字状态并设置新内容 - - # 清空文本编辑器,准备逐步显示 - self.text_edit.clear() - + + # 按页构建并显示内容 + try: + self.build_pages(content) + except Exception as e: + print(f"分页显示失败: {e}") + # 清除文件加载标志 self.is_loading_file = False - + # 设置当前文件路径 self.current_file_path = file_path self.is_modified = False self.update_window_title() - + # 更新状态栏 - self.status_bar.showMessage(f"已打开学习文件: {os.path.basename(file_path)},开始打字逐步显示学习内容!", 5000) - + self.status_bar.showMessage(f"已打开学习文件: {os.path.basename(file_path)},已按页显示内容!", 5000) + # 更新字数统计 if hasattr(self.status_bar, 'words_label'): self.status_bar.words_label.setText(f"总字数: {len(content)}") @@ -911,10 +1017,26 @@ class WordStyleMainWindow(QMainWindow): 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) + # dynamically import python-docx if available to avoid static import errors + try: + import importlib.util + spec = importlib.util.find_spec('docx') + if spec is not None: + docx = importlib.import_module('docx') + Document = getattr(docx, 'Document', None) + else: + Document = None + except Exception: + Document = None + + if Document: + doc = Document() + doc.add_paragraph(self.text_edit.toPlainText()) + doc.save(self.current_file_path) + else: + # python-docx not available; fallback to writing plain text + with open(self.current_file_path, 'w', encoding='utf-8') as f: + f.write(self.text_edit.toPlainText()) else: # 对于其他格式,保持原有逻辑 with open(self.current_file_path, 'w', encoding='utf-8') as f: @@ -943,10 +1065,26 @@ class WordStyleMainWindow(QMainWindow): 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) + # dynamically import python-docx if available to avoid static import errors + try: + import importlib.util + spec = importlib.util.find_spec('docx') + if spec is not None: + docx = importlib.import_module('docx') + Document = getattr(docx, 'Document', None) + else: + Document = None + except Exception: + Document = None + + if Document: + doc = Document() + doc.add_paragraph(self.text_edit.toPlainText()) + doc.save(file_path) + else: + # python-docx not available; save as plain text + with open(file_path, 'w', encoding='utf-8') as f: + f.write(self.text_edit.toPlainText()) else: # 对于其他格式,保持原有逻辑 with open(file_path, 'w', encoding='utf-8') as f: -- 2.34.1 From 9ef137a84e04cb3df2fd87b228e2c10a79c84554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E5=AD=90=E6=98=82?= <929110464@qq.com> Date: Thu, 23 Oct 2025 13:11:55 +0800 Subject: [PATCH 2/3] =?UTF-8?q?Revert=20"UI=E4=BC=98=E5=8C=961"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 450937ff6561c2b90600a38a5a80d1427af84a99. --- src/ui/word_style_ui.py | 3 +- src/word_main_window.py | 254 +++++++++------------------------------- 2 files changed, 59 insertions(+), 198 deletions(-) diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index 3132158..0d90ce2 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -445,8 +445,7 @@ class WordTextEdit(QTextEdit): """) # 设置页面边距和背景 - # Keep editor background transparent; page container provides the white background and margins - self.setViewportMargins(0, 0, 0, 0) + self.setViewportMargins(50, 50, 50, 50) # 设置默认字体 font = QFont("Calibri", 12) diff --git a/src/word_main_window.py b/src/word_main_window.py index f3b7441..17642a4 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -226,23 +226,6 @@ class WordStyleMainWindow(QMainWindow): background-color: #f3f2f1; } """) - - def init_network_services(self): - """ - 初始化网络服务:启动天气和每日一言的后台线程并连接信号 - """ - try: - # 天气线程 - 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() - except Exception as e: - print(f"初始化网络服务失败: {e}") def create_menu_bar(self): """创建菜单栏""" @@ -412,35 +395,47 @@ class WordStyleMainWindow(QMainWindow): } """) - # 创建文档容器(用于多页布局) + # 创建文档容器 document_container = QWidget() - container_layout = QHBoxLayout() - container_layout.setContentsMargins(0, 40, 0, 40) # workspace vertical padding - container_layout.setSpacing(0) - - # 中心列,垂直堆叠多个 page_frame - pages_column = QWidget() - pages_column_layout = QVBoxLayout() - pages_column_layout.setContentsMargins(0, 0, 0, 0) - pages_column_layout.setSpacing(20) # 间隔表示页间距 - pages_column.setLayout(pages_column_layout) - - # 保存引用以便后续构建/清理页面 - self._pages_column_layout = pages_column_layout - self._page_frames = [] - - # center the pages_column horizontally inside document_container - container_layout.addStretch() - container_layout.addWidget(pages_column) - container_layout.addStretch() - document_container.setLayout(container_layout) - - # 将 document_container 放入 scroll_area 并添加到 main_layout + 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("在此输入您的内容...") + + document_layout.addWidget(self.text_edit) + document_container.setLayout(document_layout) + scroll_area.setWidget(document_container) main_layout.addWidget(scroll_area) - - # 构建首个空白页面(后续会根据内容重建) - self.build_pages("") + + 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): """初始化打字逻辑""" @@ -448,103 +443,6 @@ class WordStyleMainWindow(QMainWindow): default_content = "欢迎使用MagicWord隐私学习软件!\n\n这是一个仿Microsoft Word界面的学习工具。" self.typing_logic = TypingLogic(default_content) self.typing_logic.reset() - - def clear_pages(self): - """清理已创建的页面控件""" - try: - while self._page_frames: - frame = self._page_frames.pop() - # 从布局中移除并删除 - self._pages_column_layout.removeWidget(frame) - frame.setParent(None) - except Exception: - pass - - def build_pages(self, full_text): - """ - 根据传入文本按近似字符数分页并在界面中渲染多个 page_frame。 - 这是一个近似实现:基于字体度量估算每页可容纳字符数,然后按字符切分。 - """ - from PyQt5.QtWidgets import QTextEdit, QLabel - from PyQt5.QtGui import QFontMetrics - - # 清理旧页面 - self.clear_pages() - - # 基本页面尺寸与内边距(应与 create_document_area 中一致) - page_width = 816 - page_height = 1056 - page_margin = 72 - - inner_width = page_width - page_margin * 2 - inner_height = page_height - page_margin * 2 - - # 使用默认字体度量进行估算 - font = QFont("Calibri", 12) - fm = QFontMetrics(font) - avg_char_width = max(1, fm.averageCharWidth()) - line_height = fm.lineSpacing() - - chars_per_line = max(1, int(inner_width / avg_char_width)) - lines_per_page = max(1, int(inner_height / line_height)) - chars_per_page = chars_per_line * lines_per_page - - # 防止 chars_per_page 过小 - if chars_per_page < 200: - chars_per_page = 2000 - - # 切分文本 - pages = [] - if not full_text: - pages = [""] - else: - i = 0 - L = len(full_text) - while i < L: - pages.append(full_text[i:i+chars_per_page]) - i += chars_per_page - - # 为每页创建 page_frame - for idx, page_text in enumerate(pages, start=1): - page_frame = QFrame() - page_frame.setObjectName('page_frame') - page_frame.setFixedWidth(page_width) - page_frame.setFixedHeight(page_height) - page_layout = QVBoxLayout() - page_layout.setContentsMargins(page_margin, page_margin, page_margin, page_margin) - page_layout.setSpacing(0) - - # 文本编辑器(每页一个) - page_edit = QTextEdit() - page_edit.setPlainText(page_text) - page_edit.setFont(font) - page_edit.setStyleSheet("background-color: transparent; border: none;") - page_edit.setLineWrapMode(QTextEdit.WidgetWidth) - page_edit.setMinimumHeight(inner_height) - - page_layout.addWidget(page_edit) - - # 页码标签(居中) - page_number_label = QLabel(f"{idx}") - page_number_label.setAlignment(Qt.AlignCenter) - page_number_label.setStyleSheet("color: #666666; font-size: 11px;") - page_layout.addWidget(page_number_label) - - page_frame.setLayout(page_layout) - page_frame.setStyleSheet("QFrame#page_frame { background: #ffffff; border: 1px solid #dcdcdc; border-radius: 2px; }") - - # 将 page_frame 添加到列布局 - self._pages_column_layout.addWidget(page_frame) - self._page_frames.append(page_frame) - - # 绑定第一个页面的编辑器为 self.text_edit(保持老逻辑) - try: - first_frame = self._page_frames[0] - first_edit = first_frame.findChild(QTextEdit) - if first_edit: - self.text_edit = first_edit - except Exception: - pass def connect_signals(self): """连接信号和槽""" @@ -947,8 +845,7 @@ class WordStyleMainWindow(QMainWindow): def new_document(self): """新建文档""" - # 构建空白页面 - self.build_pages("") + self.text_edit.clear() self.current_file_path = None self.is_modified = False self.update_window_title() @@ -972,32 +869,29 @@ class WordStyleMainWindow(QMainWindow): if content: # 设置文件加载标志 self.is_loading_file = True - - # 存储完整内容 + + # 存储完整内容但不立即显示 self.imported_content = content self.displayed_chars = 0 - + # 设置学习内容到打字逻辑 if self.typing_logic: self.typing_logic.reset(content) # 重置打字状态并设置新内容 - - # 按页构建并显示内容 - try: - self.build_pages(content) - except Exception as e: - print(f"分页显示失败: {e}") - + + # 清空文本编辑器,准备逐步显示 + self.text_edit.clear() + # 清除文件加载标志 self.is_loading_file = False - + # 设置当前文件路径 self.current_file_path = file_path self.is_modified = False self.update_window_title() - + # 更新状态栏 - self.status_bar.showMessage(f"已打开学习文件: {os.path.basename(file_path)},已按页显示内容!", 5000) - + self.status_bar.showMessage(f"已打开学习文件: {os.path.basename(file_path)},开始打字逐步显示学习内容!", 5000) + # 更新字数统计 if hasattr(self.status_bar, 'words_label'): self.status_bar.words_label.setText(f"总字数: {len(content)}") @@ -1017,26 +911,10 @@ class WordStyleMainWindow(QMainWindow): try: # 如果是.docx文件,创建一个基本的Word文档 if self.current_file_path.endswith('.docx'): - # dynamically import python-docx if available to avoid static import errors - try: - import importlib.util - spec = importlib.util.find_spec('docx') - if spec is not None: - docx = importlib.import_module('docx') - Document = getattr(docx, 'Document', None) - else: - Document = None - except Exception: - Document = None - - if Document: - doc = Document() - doc.add_paragraph(self.text_edit.toPlainText()) - doc.save(self.current_file_path) - else: - # python-docx not available; fallback to writing plain text - with open(self.current_file_path, 'w', encoding='utf-8') as f: - f.write(self.text_edit.toPlainText()) + 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: @@ -1065,26 +943,10 @@ class WordStyleMainWindow(QMainWindow): try: # 如果是.docx文件,创建一个基本的Word文档 if file_path.endswith('.docx'): - # dynamically import python-docx if available to avoid static import errors - try: - import importlib.util - spec = importlib.util.find_spec('docx') - if spec is not None: - docx = importlib.import_module('docx') - Document = getattr(docx, 'Document', None) - else: - Document = None - except Exception: - Document = None - - if Document: - doc = Document() - doc.add_paragraph(self.text_edit.toPlainText()) - doc.save(file_path) - else: - # python-docx not available; save as plain text - with open(file_path, 'w', encoding='utf-8') as f: - f.write(self.text_edit.toPlainText()) + 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: -- 2.34.1 From ef9c99b4b7d9e9116e74da1c83996c333a98b835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E5=AD=90=E6=98=82?= <929110464@qq.com> Date: Fri, 24 Oct 2025 23:46:11 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/file_parser.py | 39 ++- src/typing_logic.py | 28 ++ src/word_main_window.py | 704 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 748 insertions(+), 23 deletions(-) diff --git a/src/file_parser.py b/src/file_parser.py index 70016bb..2ab8681 100644 --- a/src/file_parser.py +++ b/src/file_parser.py @@ -1,5 +1,7 @@ import os -from typing import Union +import zipfile +import tempfile +from typing import Union, List, Tuple class FileParser: @staticmethod @@ -86,6 +88,41 @@ class FileParser: # 默认返回UTF-8 return 'utf-8' + @staticmethod + def extract_images_from_docx(file_path: str) -> List[Tuple[str, bytes]]: + """从Word文档中提取图片 + + Args: + file_path: Word文档路径 + + Returns: + 图片列表,每个元素为(图片文件名, 图片二进制数据)的元组 + """ + if not FileParser.validate_file_path(file_path): + raise ValueError(f"Invalid file path: {file_path}") + + images = [] + try: + # Word文档实际上是ZIP文件,可以直接解压 + with zipfile.ZipFile(file_path, 'r') as zip_file: + # 遍历ZIP文件中的所有文件 + for file_info in zip_file.filelist: + file_name = file_info.filename + # Word文档中的图片通常存储在word/media/目录下 + if file_name.startswith('word/media/') and file_info.file_size > 0: + # 读取图片数据 + image_data = zip_file.read(file_name) + # 获取图片扩展名 + image_ext = os.path.splitext(file_name)[1].lower() + if image_ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp']: + # 保存图片信息 + base_name = os.path.basename(file_name) + images.append((base_name, image_data)) + + return images + except Exception as e: + raise Exception(f"Error extracting images from docx file {file_path}: {str(e)}") + @staticmethod def parse_docx(file_path: str) -> str: diff --git a/src/typing_logic.py b/src/typing_logic.py index c296b40..b7cfbcf 100644 --- a/src/typing_logic.py +++ b/src/typing_logic.py @@ -11,6 +11,7 @@ class TypingLogic: self.error_count = 0 self.total_chars = len(learning_content) self.typed_chars = 0 + self.image_positions = [] # 存储图片位置信息 def check_input(self, user_text: str) -> dict: """ @@ -134,6 +135,7 @@ class TypingLogic: self.current_index = 0 self.error_count = 0 self.typed_chars = 0 + self.image_positions = [] # 重置图片位置信息 def get_statistics(self) -> dict: """ @@ -150,6 +152,32 @@ class TypingLogic: "accuracy_rate": self._calculate_accuracy() } + def set_image_positions(self, image_positions: list): + """ + 设置图片位置信息 + - image_positions: 列表,包含图片位置信息 + """ + self.image_positions = image_positions + + def get_current_image_info(self, position: int) -> dict: + """ + 获取当前位置的图片信息 + - position: 整数,当前输入位置 + - 返回字典包含图片信息,如果没有图片返回None + """ + for img_info in self.image_positions: + if img_info['start_pos'] <= position <= img_info['end_pos']: + return img_info + return None + + def check_image_at_position(self, position: int) -> bool: + """ + 检查指定位置是否有图片 + - position: 整数,位置索引 + - 返回布尔值,该位置是否有图片 + """ + return self.get_current_image_info(position) is not None + def _calculate_accuracy(self) -> float: """ 计算准确率 diff --git a/src/word_main_window.py b/src/word_main_window.py index 17642a4..c5aafb7 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -4,9 +4,10 @@ import os from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLabel, QSplitter, QFrame, QMenuBar, QAction, QFileDialog, QMessageBox, QApplication, - QDialog, QLineEdit, QCheckBox, QPushButton) -from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QRect -from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor, QTextDocument + QDialog, QLineEdit, QCheckBox, QPushButton, QListWidget, + QListWidgetItem, QScrollArea) +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 from ui.word_style_ui import (WordRibbon, WordStatusBar, WordTextEdit, WordStyleToolBar) @@ -66,6 +67,21 @@ class WordStyleMainWindow(QMainWindow): 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.unified_document_content = "" # 统一文档内容 + self.last_edit_mode = "typing" # 上次编辑模式 # 初始化网络服务和WeatherAPI self.network_service = NetworkService() @@ -337,6 +353,25 @@ class WordStyleMainWindow(QMainWindow): 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) + 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('附加工具') @@ -420,6 +455,30 @@ class WordStyleMainWindow(QMainWindow): self.text_edit.setPlainText("在此输入您的内容...") 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) scroll_area.setWidget(document_container) @@ -475,7 +534,212 @@ class WordStyleMainWindow(QMainWindow): self.ribbon.refresh_weather_btn.clicked.connect(self.refresh_weather) def on_text_changed(self): - """文本变化处理 - 逐步显示模式""" + """文本变化处理 - 根据视图模式处理文本变化""" + # 如果正在加载文件,跳过处理 + if self.is_loading_file: + 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": + # 打字模式:可以自由打字 + self.handle_typing_mode_typing() + + # 标记文档为已修改 + if not self.is_modified: + self.is_modified = True + self.update_window_title() + + 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): + # 计算应该显示的字符数(基于当前已显示的字符数) + chars_to_show = min(self.displayed_chars + 1, len(self.imported_content)) + + # 更新已显示字符数 + self.displayed_chars = chars_to_show + + # 保存当前学习进度,以便在模式切换时恢复 + self.learning_progress = 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', '') + 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', '') + 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', '') + 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 + + def on_text_changed_original(self): + """文本变化处理 - 支持逐步显示模式和自由打字模式""" # 如果正在加载文件,跳过处理 if self.is_loading_file: return @@ -501,17 +765,32 @@ class WordStyleMainWindow(QMainWindow): # 临时禁用文本变化信号,避免递归 self.text_edit.textChanged.disconnect(self.on_text_changed) - # 更新文本编辑器内容 - self.text_edit.setPlainText(display_text) + # 保存当前光标位置 + cursor = self.text_edit.textCursor() + original_position = cursor.position() - # 重新连接文本变化信号 - self.text_edit.textChanged.connect(self.on_text_changed) + # 只添加新字符,而不是重置整个文本 + 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 = self.text_edit.textCursor() - cursor.movePosition(cursor.End) + # 恢复光标位置 + 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) @@ -527,6 +806,9 @@ class WordStyleMainWindow(QMainWindow): stats = self.typing_logic.get_statistics() self.update_status_bar(stats) + # 检查当前位置是否有图片 + self.check_and_show_image_at_position(self.displayed_chars) + # 检查是否完成 if self.displayed_chars >= len(self.imported_content): self.on_lesson_complete() @@ -535,6 +817,23 @@ class WordStyleMainWindow(QMainWindow): # 更新状态栏显示进度 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: @@ -844,17 +1143,29 @@ class WordStyleMainWindow(QMainWindow): 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() + # 根据当前模式重置打字逻辑 if self.typing_logic: - self.typing_logic.reset() + if self.view_mode == "learning": + # 学习模式:重置为默认内容 + self.typing_logic.reset("欢迎使用MagicWord隐私学习软件!\n\n这是一个仿Microsoft Word界面的学习工具。") + self.imported_content = "" + self.displayed_chars = 0 + self.status_bar.showMessage("新建文档 - 学习模式,请先导入文件开始打字学习", 3000) + elif self.view_mode == "typing": + # 打字模式:重置为默认内容,允许自由打字 + self.typing_logic.reset("欢迎使用MagicWord隐私学习软件!\n\n这是一个仿Microsoft Word界面的学习工具。") + self.imported_content = "" + self.displayed_chars = 0 + self.status_bar.showMessage("新建文档 - 打字模式,可以自由开始打字", 3000) def open_file(self): - """打开文件并设置为打字学习内容 - 逐步显示模式""" + """打开文件 - 创建空白副本并在学习模式下显示导入内容""" file_path, _ = QFileDialog.getOpenFileName( self, "打开文件", "", "文档文件 (*.docx *.txt *.pdf);;所有文件 (*.*)" @@ -874,13 +1185,24 @@ class WordStyleMainWindow(QMainWindow): self.imported_content = content self.displayed_chars = 0 - # 设置学习内容到打字逻辑 - if self.typing_logic: - self.typing_logic.reset(content) # 重置打字状态并设置新内容 - - # 清空文本编辑器,准备逐步显示 + # 创建空白副本 - 清空文本编辑器 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) + + elif self.view_mode == "typing": + # 打字模式:也设置内容,但允许自由打字 + if self.typing_logic: + self.typing_logic.reset("") # 重置打字状态,但内容为空,允许自由打字 + + self.status_bar.showMessage(f"已创建空白副本,可以自由打字", 5000) + # 清除文件加载标志 self.is_loading_file = False @@ -889,12 +1211,13 @@ class WordStyleMainWindow(QMainWindow): self.is_modified = False self.update_window_title() - # 更新状态栏 - self.status_bar.showMessage(f"已打开学习文件: {os.path.basename(file_path)},开始打字逐步显示学习内容!", 5000) - # 更新字数统计 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, "警告", "无法读取文件内容或文件为空") @@ -990,6 +1313,122 @@ class WordStyleMainWindow(QMainWindow): # 这里可以实现打印布局的逻辑 self.status_bar.showMessage("打印布局功能开发中...", 3000) + def set_view_mode(self, mode): + """设置视图模式 - 实现文档A和学习文档C的正确交互""" + if mode not in ["typing", "learning"]: + return + + # 保存当前模式的内容 + current_content = self.text_edit.toPlainText() + + # 根据当前模式保存特定信息 + if self.view_mode == "typing": + # 打字模式:保存打字内容到文档A + self.typing_mode_content = current_content + print(f"保存打字模式内容到文档A,长度: {len(self.typing_mode_content)}") + elif self.view_mode == "learning": + # 学习模式:保存当前学习进度和内容 + self.learning_text = current_content + if hasattr(self, 'displayed_chars') and self.imported_content: + self.learning_progress = self.displayed_chars + print(f"保存学习模式内容,学习进度: {self.learning_progress}/{len(self.imported_content) if self.imported_content else 0}") + + # 更新模式 + self.view_mode = mode + self.last_edit_mode = mode + + # 更新菜单项状态 + self.typing_mode_action.setChecked(mode == "typing") + self.learning_mode_action.setChecked(mode == "learning") + + # 临时禁用文本变化信号,避免递归 + self.text_edit.textChanged.disconnect(self.on_text_changed) + + try: + if mode == "typing": + # 打字模式:显示文档A的内容 + self.status_bar.showMessage("切换到打字模式 - 显示文档A内容", 3000) + + # 设置文档A的内容 + self.text_edit.clear() + if self.typing_mode_content: + self.text_edit.setPlainText(self.typing_mode_content) + + # 设置光标位置到文档末尾 + cursor = self.text_edit.textCursor() + cursor.movePosition(cursor.End) + self.text_edit.setTextCursor(cursor) + + # 重置打字逻辑,准备接受新的输入 + if self.typing_logic: + self.typing_logic.reset("") + + # 重置显示字符计数 + self.displayed_chars = 0 + + # 重置图片插入记录 + if hasattr(self, 'inserted_images'): + self.inserted_images.clear() + + elif mode == "learning": + # 学习模式:从上次中断的地方继续显示学习文档C的内容 + if not self.imported_content: + self.status_bar.showMessage("学习模式需要导入文件 - 请先打开一个文件", 3000) + # 清空文本编辑器,等待导入文件 + self.text_edit.clear() + if self.typing_logic: + self.typing_logic.reset("在此输入您的内容...") + # 重置显示字符计数 + self.displayed_chars = 0 + # 重置图片插入记录 + if hasattr(self, 'inserted_images'): + self.inserted_images.clear() + else: + # 检查是否有保存的学习进度 + if hasattr(self, 'learning_progress') and self.learning_progress > 0: + self.status_bar.showMessage(f"切换到学习模式 - 从上次中断处继续 ({self.learning_progress}/{len(self.imported_content)})", 3000) + # 恢复学习进度 + self.displayed_chars = self.learning_progress + + # 重置打字逻辑,准备接受导入内容 + if self.typing_logic: + self.typing_logic.reset(self.imported_content) + + # 获取应该显示的学习文档C的内容部分 + display_text = self.imported_content[:self.displayed_chars] + + # 设置文本内容 + self.text_edit.clear() + self.text_edit.setPlainText(display_text) + + # 设置光标位置到文本末尾 + cursor = self.text_edit.textCursor() + cursor.movePosition(cursor.End) + self.text_edit.setTextCursor(cursor) + + # 重新插入图片 + if hasattr(self, 'inserted_images'): + self.inserted_images.clear() + self.insert_images_in_text() + else: + self.status_bar.showMessage("切换到学习模式 - 准备显示学习文档C内容", 3000) + # 重置打字逻辑,准备接受导入内容 + if self.typing_logic: + self.typing_logic.reset(self.imported_content) + + # 重置显示字符计数 + self.displayed_chars = 0 + + # 清空文本编辑器,等待用户开始打字 + self.text_edit.clear() + + # 重置图片插入记录 + if hasattr(self, 'inserted_images'): + self.inserted_images.clear() + finally: + # 重新连接文本变化信号 + self.text_edit.textChanged.connect(self.on_text_changed) + def set_page_color(self, color): """设置页面颜色""" color_map = { @@ -1352,6 +1791,156 @@ class WordStyleMainWindow(QMainWindow): 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 show_image_viewer(self, filename, image_data): + """显示图片查看器""" + try: + # 创建图片查看窗口 + viewer = QDialog(self) + viewer.setWindowTitle(f"图片查看 - {filename}") + viewer.setModal(False) + viewer.resize(800, 600) + + # 创建布局 + layout = QVBoxLayout() + + # 创建滚动区域 + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + + # 创建图片标签 + image_label = QLabel() + image_label.setAlignment(Qt.AlignCenter) + + # 加载图片 + pixmap = QPixmap() + if pixmap.loadFromData(image_data): + image_label.setPixmap(pixmap) + image_label.setScaledContents(False) + + # 设置标签大小为图片实际大小 + image_label.setFixedSize(pixmap.size()) + else: + image_label.setText("无法加载图片") + + scroll_area.setWidget(image_label) + layout.addWidget(scroll_area) + + # 添加关闭按钮 + close_btn = QPushButton("关闭") + close_btn.clicked.connect(viewer.close) + layout.addWidget(close_btn) + + viewer.setLayout(layout) + viewer.show() + + except Exception as e: + self.status_bar.showMessage(f"创建图片查看器失败: {str(e)}", 3000) + + def insert_images_in_text(self): + """在文本中插入图片 - 修复图片显示逻辑""" + try: + if not self.typing_logic or not hasattr(self.typing_logic, 'image_positions'): + print("打字逻辑或图片位置信息不存在") + return + + # 检查是否已经插入过图片(避免重复插入) + if not hasattr(self, 'inserted_images'): + self.inserted_images = set() + + # 获取当前显示的文本 + current_text = self.text_edit.toPlainText() + current_length = len(current_text) + + # 检查当前显示位置是否有图片需要插入 + for image_info in self.typing_logic.image_positions: + 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'] and current_length >= image_info['start_pos']: + # 在图片位置插入图片 + cursor = self.text_edit.textCursor() + + # 计算图片应该插入的位置(相对于当前文本) + insert_position = min(image_info['start_pos'], current_length) + + # 确保插入位置有效 + if insert_position >= 0 and insert_position <= current_length: + cursor.setPosition(insert_position) + + # 创建图片格式 + image_format = QTextImageFormat() + + # 加载图片数据 + pixmap = QPixmap() + if pixmap.loadFromData(image_info['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 closeEvent(self, event): """关闭事件处理""" if self.is_modified: @@ -1370,6 +1959,77 @@ class WordStyleMainWindow(QMainWindow): event.ignore() else: event.accept() + + def extract_and_display_images(self, file_path): + """提取并显示Word文档中的图片 - 修复图片位置计算""" + try: + # 提取图片 + 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) + + # 为每张图片创建位置信息 - 更合理的分布 + if len(images) == 1: + # 只有一张图片,放在文档中间 + start_pos = len(self.imported_content) // 2 + else: + # 多张图片,均匀分布 + start_pos = (len(self.imported_content) * (index + 1)) // (len(images) + 1) + + end_pos = min(start_pos + 50, len(self.imported_content)) + + 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) + + # 更新状态栏 + self.status_bar.showMessage(f"已提取 {len(images)} 张图片,双击查看大图", 5000) + + except Exception as e: + self.status_bar.showMessage(f"提取图片失败: {str(e)}", 3000) if __name__ == "__main__": app = QApplication(sys.argv) -- 2.34.1