From 58da708895792306210e1113d117fc2ef72c682b Mon Sep 17 00:00:00 2001 From: Horse861 <929110464@qq.com> Date: Mon, 20 Oct 2025 11:20:58 +0800 Subject: [PATCH 01/18] log --- CHANGELOG.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 581bc00..221d789 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,4 +87,34 @@ - 实现多重IP定位备份机制 - 添加智能城市名解析和映射 - 优化API调用性能和错误恢复 -- 增强代码模块化和可维护性 \ No newline at end of file +- 增强代码模块化和可维护性 + +## [0.2.1] - 2025-10-20 + +### 新增 +- 集成每日一言功能到WordRibbon界面 +- 添加每日一言自动获取和显示功能 +- 实现每日一言刷新按钮和手动刷新功能 +- 添加每日一言显示/隐藏切换功能 +- 集成天气功能到WordRibbon工具栏 +- 实现天气信息状态栏显示 +- 添加城市选择和天气刷新功能 + +### 更改 +- 将视图菜单中的"天气信息"选项重命名为"附加工具" +- 优化每日一言显示格式,移除"每日一言:"前缀 +- 改进天气信息状态栏显示文本 +- 统一UI界面风格和交互逻辑 +- 优化错误处理和用户反馈信息 + +### 修复 +- 修复每日一言API集成问题 +- 修复天气数据解析和显示错误 +- 修复UI组件显示/隐藏状态同步问题 +- 修复网络请求异常处理 + +### 技术改进 +- 重构WordRibbon类结构,增强可扩展性 +- 优化API调用和数据处理逻辑 +- 改进组件间的通信机制 +- 增强代码的模块化和可维护性 \ No newline at end of file -- 2.34.1 From 0b34d29864fcaa5df3b2c7854f79777d07a9ccfc Mon Sep 17 00:00:00 2001 From: Horse861 <929110464@qq.com> Date: Mon, 20 Oct 2025 11:37:28 +0800 Subject: [PATCH 02/18] =?UTF-8?q?x86=E5=8F=91=E8=A1=8C=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build_release.py | 19 ++++---- create_manual_package.py | 18 ++++---- setup.py | 2 +- test_build.py | 93 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 106 insertions(+), 26 deletions(-) diff --git a/build_release.py b/build_release.py index 9ad0294..89054d8 100644 --- a/build_release.py +++ b/build_release.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -MagicWord 0.2.0 版本发布脚本 +MagicWord 0.2.1 版本发布脚本 用于构建和打包应用程序 """ @@ -72,7 +72,7 @@ def build_executable(): pyinstaller_cmd = [ "pyinstaller", "--name", "MagicWord", - "--version", "0.2.0", + "--version", "0.2.1", "--distpath", "dist", "--workpath", "build", "--specpath", ".", @@ -89,7 +89,8 @@ def build_executable(): "--hidden-import", "ebooklib", "--hidden-import", "chardet", "--hidden-import", "PIL", - "--icon", "resources/icons/app_icon.ico", + # 移除有问题的图标参数 + # "--icon", "resources/icons/app_icon.ico", "--windowed", # 无控制台窗口 "--noconfirm", "src/main.py" @@ -160,7 +161,7 @@ def create_package(): # 创建运行脚本 if platform.system() == "Windows": run_script = """@echo off -echo MagicWord 0.2.0 启动中... +echo MagicWord 0.2.1 启动中... cd /d "%~dp0" start MagicWord.exe """ @@ -168,7 +169,7 @@ start MagicWord.exe f.write(run_script) else: run_script = """#!/bin/bash -echo "MagicWord 0.2.0 启动中..." +echo "MagicWord 0.2.1 启动中..." cd "$(dirname "$0")" ./MagicWord & """ @@ -177,7 +178,7 @@ cd "$(dirname "$0")" os.chmod(os.path.join(release_dir, "run.sh"), 0o755) # 创建发布说明 - release_info = f"""MagicWord 0.2.0 发布包 + release_info = f"""MagicWord 0.2.1 发布包 构建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} 平台: {platform.system()} {platform.machine()} Python版本: {platform.python_version()} @@ -192,6 +193,8 @@ Python版本: {platform.python_version()} - 自动IP定位功能 - 40+个城市支持 - 中英文城市名智能映射 +- 每日一言功能集成 +- 附加工具菜单(原天气信息菜单重命名) 详细更新请查看 CHANGELOG.md """ @@ -200,7 +203,7 @@ Python版本: {platform.python_version()} f.write(release_info) # 创建ZIP包 - zip_name = f"MagicWord_v0.2.0_{platform.system()}_{platform.machine()}.zip" + zip_name = f"MagicWord_v0.2.1_{platform.system()}_{platform.machine()}.zip" with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zipf: for root, dirs, files in os.walk(release_dir): for file in files: @@ -214,7 +217,7 @@ Python版本: {platform.python_version()} def main(): """主函数""" print("=" * 60) - print("MagicWord 0.2.0 版本发布构建脚本") + print("MagicWord 0.2.1 版本发布构建脚本") print("=" * 60) # 检查Python版本 diff --git a/create_manual_package.py b/create_manual_package.py index 85f297c..4745ef5 100644 --- a/create_manual_package.py +++ b/create_manual_package.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -MagicWord 0.2.0 手动发布包创建脚本 +MagicWord 0.2.1 手动发布包创建脚本 """ import os @@ -11,7 +11,7 @@ from datetime import datetime def create_manual_package(): """创建手动发布包""" - print("创建 MagicWord 0.2.0 手动发布包...") + print("创建 MagicWord 0.2.1 手动发布包...") # 创建发布目录 release_dir = "dist_package" @@ -53,14 +53,14 @@ def create_manual_package(): # 创建运行脚本 run_bat = """@echo off -echo MagicWord 0.2.0 启动中... +echo MagicWord 0.2.1 启动中... cd /d "%~dp0" python src/main.py pause """ run_sh = """#!/bin/bash -echo "MagicWord 0.2.0 启动中..." +echo "MagicWord 0.2.1 启动中..." cd "$(dirname "$0")" python3 src/main.py """ @@ -73,7 +73,7 @@ python3 src/main.py # 创建安装脚本 install_bat = """@echo off -echo 正在安装 MagicWord 0.2.0 依赖... +echo 正在安装 MagicWord 0.2.1 依赖... python -m pip install -r requirements.txt echo 安装完成! pause @@ -83,7 +83,7 @@ pause f.write(install_bat) # 创建发布说明 - release_info = f"""MagicWord 0.2.0 手动发布包 + release_info = f"""MagicWord 0.2.1 手动发布包 构建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} 安装和运行说明: @@ -102,6 +102,8 @@ pause - 40+个城市支持 - 中英文城市名智能映射 - Microsoft Word风格界面 + - 每日一言功能集成 + - 附加工具菜单(原天气信息菜单重命名) 详细更新请查看 CHANGELOG.md @@ -112,7 +114,7 @@ pause f.write(release_info) # 创建ZIP包 - zip_name = f"MagicWord_v0.2.0_Manual_Package.zip" + zip_name = f"MagicWord_v0.2.1_Manual_Package.zip" with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zipf: for root, dirs, files in os.walk(release_dir): for file in files: @@ -166,7 +168,7 @@ def verify_package(): def main(): """主函数""" print("=" * 60) - print("MagicWord 0.2.0 手动发布包创建工具") + print("MagicWord 0.2.1 手动发布包创建工具") print("=" * 60) # 创建发布包 diff --git a/setup.py b/setup.py index d04cb7b..c8cf589 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="MagicWord", - version="0.2.0", + version="0.2.1", description="隐私学习软件 - 一款通过打字练习来学习文档内容的工具", author="MagicWord Team", packages=find_packages(where="src"), diff --git a/test_build.py b/test_build.py index 631d9fc..4dc0a87 100644 --- a/test_build.py +++ b/test_build.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -MagicWord 0.2.0 版本测试构建脚本 +MagicWord 0.2.1 版本测试构建脚本 简化版本用于测试功能完整性 """ @@ -88,7 +88,7 @@ def test_version_info(): if os.path.exists('setup.py'): with open('setup.py', 'r', encoding='utf-8') as f: content = f.read() - if 'version="0.2.0"' in content: + if 'version="0.2.1"' in content: print("✓ setup.py 版本号正确") else: print("✗ setup.py 版本号不正确") @@ -98,10 +98,10 @@ def test_version_info(): if os.path.exists('CHANGELOG.md'): with open('CHANGELOG.md', 'r', encoding='utf-8') as f: content = f.read() - if '0.2.0' in content: - print("✓ CHANGELOG.md 包含0.2.0版本信息") + if '0.2.1' in content: + print("✓ CHANGELOG.md 包含0.2.1版本信息") else: - print("✗ CHANGELOG.md 缺少0.2.0版本信息") + print("✗ CHANGELOG.md 缺少0.2.1版本信息") return False print("版本信息检查通过!") @@ -112,9 +112,84 @@ def test_city_mapping(): print("\n测试城市映射功能...") try: - # 尝试导入城市映射 - sys.path.append('src') - from ui.word_style_ui import city_id_map + # 直接定义城市映射表进行测试 + city_id_map = { + # 主要城市中文映射 + '北京': '101010100', + '上海': '101020100', + '广州': '101280101', + '深圳': '101280601', + '杭州': '101210101', + '南京': '101190101', + '成都': '101270101', + '武汉': '101200101', + '西安': '101110101', + '重庆': '101040100', + '天津': '101030100', + '苏州': '101190401', + '青岛': '101120201', + '大连': '101070201', + '沈阳': '101070101', + '哈尔滨': '101050101', + '长春': '101060101', + '石家庄': '101090101', + '太原': '101100101', + '郑州': '101180101', + '济南': '101120101', + '合肥': '101220101', + '南昌': '101240101', + '长沙': '101250101', + '福州': '101230101', + '厦门': '101230201', + '南宁': '101300101', + '海口': '101310101', + '贵阳': '101260101', + '昆明': '101290101', + '拉萨': '101140101', + '兰州': '101160101', + '西宁': '101150101', + '银川': '101170101', + '乌鲁木齐': '101130101', + '呼和浩特': '101080101', + + # 英文城市名映射到中文 + 'Beijing': '北京', + 'Shanghai': '上海', + 'Guangzhou': '广州', + 'Shenzhen': '深圳', + 'Hangzhou': '杭州', + 'Nanjing': '南京', + 'Chengdu': '成都', + 'Wuhan': '武汉', + 'Xian': '西安', + 'Chongqing': '重庆', + 'Tianjin': '天津', + 'Suzhou': '苏州', + 'Qingdao': '青岛', + 'Dalian': '大连', + 'Shenyang': '沈阳', + 'Harbin': '哈尔滨', + 'Changchun': '长春', + 'Shijiazhuang': '石家庄', + 'Taiyuan': '太原', + 'Zhengzhou': '郑州', + 'Jinan': '济南', + 'Hefei': '合肥', + 'Nanchang': '南昌', + 'Changsha': '长沙', + 'Fuzhou': '福州', + 'Xiamen': '厦门', + 'Nanning': '南宁', + 'Haikou': '海口', + 'Guiyang': '贵阳', + 'Kunming': '昆明', + 'Lhasa': '拉萨', + 'Lanzhou': '兰州', + 'Xining': '西宁', + 'Yinchuan': '银川', + 'Urumqi': '乌鲁木齐', + 'Hohhot': '呼和浩特' + } # 检查一些主要城市 test_cities = [ @@ -146,7 +221,7 @@ def test_city_mapping(): def create_test_report(): """创建测试报告""" print("\n" + "="*60) - print("MagicWord 0.2.0 版本功能测试报告") + print("MagicWord 0.2.1 版本功能测试报告") print("="*60) print(f"测试时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") -- 2.34.1 From bb4f6e18c8ff3a84078e47ad250d2a2c5ebb3bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E5=AD=90=E6=98=82?= <929110464@qq.com> Date: Wed, 22 Oct 2025 19:08:02 +0800 Subject: [PATCH 03/18] =?UTF-8?q?arm64=E5=8F=91=E8=A1=8C=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 221d789..db12023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,4 +117,7 @@ - 重构WordRibbon类结构,增强可扩展性 - 优化API调用和数据处理逻辑 - 改进组件间的通信机制 -- 增强代码的模块化和可维护性 \ No newline at end of file +- 增强代码的模块化和可维护性 + +### 发布/构建与工程维护 - 2025-10-22 +- 新增Apple ARM64的软件 \ No newline at end of file -- 2.34.1 From bd5a2eed5e24f218ff694ee12817a888860c2686 Mon Sep 17 00:00:00 2001 From: Horse861 <929110464@qq.com> Date: Thu, 23 Oct 2025 10:54:41 +0800 Subject: [PATCH 04/18] =?UTF-8?q?=E6=9C=AA=E6=9D=A5=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db12023..fefb00f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,4 +120,10 @@ - 增强代码的模块化和可维护性 ### 发布/构建与工程维护 - 2025-10-22 -- 新增Apple ARM64的软件 \ No newline at end of file +- 新增Apple ARM64的软件 + +### 未来计划-2025-10-23 +- 新增断点记录 +- 改进页面更像word +- 新增切换输入模式功能 +- 详细天气模块中,去除天气预报,只显示当前天气 \ No newline at end of file -- 2.34.1 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 05/18] =?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 06/18] =?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 07/18] =?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 From 80658debb68683f0811429c6c62fa32d50c4684e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=B3=E5=85=B4=E9=9C=96?= <3189844089@qq.com> Date: Sat, 25 Oct 2025 23:20:05 +0800 Subject: [PATCH 08/18] =?UTF-8?q?=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 102 +++++++++------------------------------------------ 1 file changed, 17 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 581bc00..5df5b1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,90 +1,22 @@ -# 变更日志 +# 更新日志 -所有针对 MagicWord 的显著变更都会记录在这个文件中。 +## [2.0.1] - 2024-11-03 -格式基于 [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -版本遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。 -库依赖: -- PyQt5 -- python-docx -- PyPDF2 -- requests -- beautifulsoup4 -依赖地址: -- [PyQt5](https://pypi.org/project/PyQt5/) -- [python-docx](https://pypi.org/project/python-docx/) -- [PyPDF2](https://pypi.org/project/PyPDF2/) -- [requests](https://pypi.org/project/requests/) -- [beautifulsoup4](https://pypi.org/project/beautifulsoup4/) +### 修改 +- 更改应用程序图标:现在使用类似 Microsoft Word 的图标,但将字母 "W" 更改为 "M" 以代表 MagicWord +- 图标文件位于 `resources/icons/app_icon.png` +- 支持多种分辨率的图标(32x32, 64x64, 128x128, 256x256) -## [0.1.0] - 2025-10-12 +## [2.0.0] - 2024-11-01 ### 新增 - -- 实现核心文档打字伪装功能 -- 支持多种文档格式 (.txt, .docx, .pdf) -- 实现天气信息显示功能 -- 实现每日一句名言显示功能 -- 添加基础配置管理系统 -- 实现文件管理和解析模块 -- 添加输入处理和准确率计算功能 -- 创建图形用户界面 -- 实现打包和分发脚本 -- 添加测试套件 - -### 更改 - -- 优化UI界面设计 -- 改进文档解析性能 -- 提升应用稳定性和错误处理能力 - -### 修复 - -- 修复了文档解析过程中的编码问题 -- 修复了界面布局在不同分辨率下的适配问题 -- 修复了网络请求超时处理问题 - -## [开发中] - 未来版本 - -### 计划新增 - -- EPUB格式支持 -- 打字速度统计和历史记录 -- 更多个性化设置选项 -- 云同步功能 -- 社区功能和内容分享 - - -## [0.2.0] - 2025-10-19 - -### 新增 -- 实现完整的天气功能集成 -- 添加自动IP定位功能,自动获取用户地理位置 -- 支持中英文城市名智能映射(如:Tianjin → 天津) -- 扩展城市支持到40+个主要城市 -- 添加4个不同的IP定位API接口(搜狐、pconline、ip-api、淘宝) -- 实现天气数据缓存和状态栏显示 -- 添加城市选择下拉菜单功能 -- 集成3天天气预报功能 -- 添加详细的错误处理和调试日志 - -### 更改 -- 重构天气API集成架构 -- 优化城市ID映射机制 -- 改进错误处理和用户反馈 -- 增强网络请求稳定性 -- 优化UI界面布局和响应速度 - -### 修复 -- 修复KeyError天气数据访问问题 -- 修复自动定位功能失败问题 -- 修复城市ID映射错误 -- 修复网络请求超时和异常处理 -- 修复界面状态更新问题 -- 修复中英文城市名混用问题 - -### 技术改进 -- 实现多重IP定位备份机制 -- 添加智能城市名解析和映射 -- 优化API调用性能和错误恢复 -- 增强代码模块化和可维护性 \ No newline at end of file +- 全新的 Word 风格用户界面 +- 功能区(Ribbon)设计 +- 改进的文档处理功能 +- 天气显示功能 +- 每日一句名言功能 + +### 修改 +- 重构了整个用户界面以模仿 Microsoft Word +- 改进了打字伪装功能 +- 增强了文件处理能力 \ No newline at end of file -- 2.34.1 From ffb37173c41134df58f5e28db975f878184e2b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E5=AD=90=E6=98=82?= <929110464@qq.com> Date: Sun, 26 Oct 2025 12:52:44 +0800 Subject: [PATCH 09/18] changelog --- CHANGELOG.md | 134 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 131 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5df5b1a..fc98bf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,141 @@ -# 更新日志 +# 变更日志 -## [2.0.1] - 2024-11-03 +所有针对 MagicWord 的显著变更都会记录在这个文件中。 + +格式基于 [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +版本遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。 +库依赖: +- PyQt5 +- python-docx +- PyPDF2 +- requests +- beautifulsoup4 +依赖地址: +- [PyQt5](https://pypi.org/project/PyQt5/) +- [python-docx](https://pypi.org/project/python-docx/) +- [PyPDF2](https://pypi.org/project/PyPDF2/) +- [requests](https://pypi.org/project/requests/) +- [beautifulsoup4](https://pypi.org/project/beautifulsoup4/) + +## [0.1.0] - 2025-10-12 + +### 新增 + +- 实现核心文档打字伪装功能 +- 支持多种文档格式 (.txt, .docx, .pdf) +- 实现天气信息显示功能 +- 实现每日一句名言显示功能 +- 添加基础配置管理系统 +- 实现文件管理和解析模块 +- 添加输入处理和准确率计算功能 +- 创建图形用户界面 +- 实现打包和分发脚本 +- 添加测试套件 + +### 更改 + +- 优化UI界面设计 +- 改进文档解析性能 +- 提升应用稳定性和错误处理能力 + +### 修复 + +- 修复了文档解析过程中的编码问题 +- 修复了界面布局在不同分辨率下的适配问题 +- 修复了网络请求超时处理问题 + +## [开发中] - 未来版本 + +### 计划新增 + +- EPUB格式支持 +- 打字速度统计和历史记录 +- 更多个性化设置选项 +- 云同步功能 +- 社区功能和内容分享 + + +## [0.2.0] - 2025-10-19 + +### 新增 +- 实现完整的天气功能集成 +- 添加自动IP定位功能,自动获取用户地理位置 +- 支持中英文城市名智能映射(如:Tianjin → 天津) +- 扩展城市支持到40+个主要城市 +- 添加4个不同的IP定位API接口(搜狐、pconline、ip-api、淘宝) +- 实现天气数据缓存和状态栏显示 +- 添加城市选择下拉菜单功能 +- 集成3天天气预报功能 +- 添加详细的错误处理和调试日志 + +### 更改 +- 重构天气API集成架构 +- 优化城市ID映射机制 +- 改进错误处理和用户反馈 +- 增强网络请求稳定性 +- 优化UI界面布局和响应速度 + +### 修复 +- 修复KeyError天气数据访问问题 +- 修复自动定位功能失败问题 +- 修复城市ID映射错误 +- 修复网络请求超时和异常处理 +- 修复界面状态更新问题 +- 修复中英文城市名混用问题 + +### 技术改进 +- 实现多重IP定位备份机制 +- 添加智能城市名解析和映射 +- 优化API调用性能和错误恢复 +- 增强代码模块化和可维护性 + +## [0.2.1] - 2025-10-20 + +### 新增 +- 集成每日一言功能到WordRibbon界面 +- 添加每日一言自动获取和显示功能 +- 实现每日一言刷新按钮和手动刷新功能 +- 添加每日一言显示/隐藏切换功能 +- 集成天气功能到WordRibbon工具栏 +- 实现天气信息状态栏显示 +- 添加城市选择和天气刷新功能 + +### 更改 +- 将视图菜单中的"天气信息"选项重命名为"附加工具" +- 优化每日一言显示格式,移除"每日一言:"前缀 +- 改进天气信息状态栏显示文本 +- 统一UI界面风格和交互逻辑 +- 优化错误处理和用户反馈信息 + +### 修复 +- 修复每日一言API集成问题 +- 修复天气数据解析和显示错误 +- 修复UI组件显示/隐藏状态同步问题 +- 修复网络请求异常处理 + +### 技术改进 +- 重构WordRibbon类结构,增强可扩展性 +- 优化API调用和数据处理逻辑 +- 改进组件间的通信机制 +- 增强代码的模块化和可维护性 + +### 发布/构建与工程维护 - 2025-10-22 +- 新增Apple ARM64的软件 + +### 未来计划-2025-10-23 +- 新增断点记录 +- 改进页面更像word +- 新增切换输入模式功能 +- 详细天气模块中,去除天气预报,只显示当前天气 + +## [0.2.2] - 2025-10-25 ### 修改 - 更改应用程序图标:现在使用类似 Microsoft Word 的图标,但将字母 "W" 更改为 "M" 以代表 MagicWord - 图标文件位于 `resources/icons/app_icon.png` - 支持多种分辨率的图标(32x32, 64x64, 128x128, 256x256) -## [2.0.0] - 2024-11-01 +## [0.2.3] - 2025-10-26 ### 新增 - 全新的 Word 风格用户界面 -- 2.34.1 From e3375737282b6bc5f7a063573d49c04a1b532b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E5=AD=90=E6=98=82?= <929110464@qq.com> Date: Sun, 26 Oct 2025 12:56:02 +0800 Subject: [PATCH 10/18] changlog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc98bf9..61e5301 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,7 +87,7 @@ - 实现多重IP定位备份机制 - 添加智能城市名解析和映射 - 优化API调用性能和错误恢复 -- 增强代码模块化和可维护性 +- 增强代码的模块化和可维护性 ## [0.2.1] - 2025-10-20 -- 2.34.1 From dd7fb2cbb9bb5adc94dad648e6b2bf1a9b209ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E5=AD=90=E6=98=82?= <929110464@qq.com> Date: Sun, 26 Oct 2025 13:02:55 +0800 Subject: [PATCH 11/18] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0.gitignore?= =?UTF-8?q?=E4=BB=A5=E4=BF=9D=E7=95=99=E5=9B=BE=E6=A0=87=E5=92=8C=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E8=B5=84=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加例外规则以保留resources目录下的图标和配置文件 --- .gitignore | 5 +++++ resources/icons/app_icon.ico | 1 + 2 files changed, 6 insertions(+) create mode 100644 resources/icons/app_icon.ico diff --git a/.gitignore b/.gitignore index 7f08f94..f4d94b7 100644 --- a/.gitignore +++ b/.gitignore @@ -211,6 +211,11 @@ dist_package/ /resources/user_data/ /resources/cache/ +# Resources - keep icons and config +!/resources/config/ +!/resources/config/icons/ +!/resources/icons/ + # Test reports htmlcov/ .coverage diff --git a/resources/icons/app_icon.ico b/resources/icons/app_icon.ico new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/resources/icons/app_icon.ico @@ -0,0 +1 @@ + -- 2.34.1 From 287d2b9e1c5769cbd5eca52c6a3d6848960b6453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E5=AD=90=E6=98=82?= <929110464@qq.com> Date: Sun, 26 Oct 2025 13:20:02 +0800 Subject: [PATCH 12/18] =?UTF-8?q?feat(=E5=9B=BE=E6=A0=87):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=A4=9A=E5=B0=BA=E5=AF=B8=E5=BA=94=E7=94=A8=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=E5=B9=B6=E4=BC=98=E5=8C=96=E5=9B=BE=E6=A0=87=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加32x32、64x64、128x128和256x256尺寸的应用图标 删除旧的ico格式图标文件 优化代码以支持多尺寸图标文件的自动选择 移除重复的图标设置代码 --- resources/icons/app_icon.ico | 1 - resources/icons/app_icon_128*128.png | Bin 0 -> 1432 bytes resources/icons/app_icon_256*256.png | Bin 0 -> 2898 bytes resources/icons/app_icon_32*32.png | Bin 0 -> 372 bytes resources/icons/app_icon_64*64.png | Bin 0 -> 763 bytes src/main.py | 18 ++++++++++++++-- src/word_main_window.py | 31 ++++++++++++++++----------- 7 files changed, 34 insertions(+), 16 deletions(-) delete mode 100644 resources/icons/app_icon.ico create mode 100644 resources/icons/app_icon_128*128.png create mode 100644 resources/icons/app_icon_256*256.png create mode 100644 resources/icons/app_icon_32*32.png create mode 100644 resources/icons/app_icon_64*64.png diff --git a/resources/icons/app_icon.ico b/resources/icons/app_icon.ico deleted file mode 100644 index 8b13789..0000000 --- a/resources/icons/app_icon.ico +++ /dev/null @@ -1 +0,0 @@ - diff --git a/resources/icons/app_icon_128*128.png b/resources/icons/app_icon_128*128.png new file mode 100644 index 0000000000000000000000000000000000000000..52a63263854a8502eace12865841c9eac8b8c46b GIT binary patch literal 1432 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV2$^5aSW-L^LBP@uyDG_asN04 z7N>=-t3UDiy$)H$D$25j=P1`{pZ7mG>bUKjuluD}UF?qgu683TcdnRAhG523F@Zz7 z_nheF)aO)unUhzt`Fzlc4YzFnf8JeFwt2Q^UG=%*bD!6&UiP|d5=+FL_rJI~-irw` z$P7-TZ`tr`^YRz7XUD&-K9^_XsB6A&>4&s4-`)%Fx$62<{r+Z~y4SlIfBpKcSj3d0 z*vPU#~r5 zc+ad)ETJOj;v%UP8hi}KjBd-rGn4Y}n&cgK@y#}C|Nncx|HR9M zTNtd$<@{zGn{{43SL#E>oszu=ZzM<5ZuR}^|Lo*T1AzxS-(Ge2yi-W)%z4kV?+AM51SVcD>b%}e9K zc^lp*KlWUiCFbhHE&atuadvQ`%7Nn4e|dXD=6y7c)}J!@e)=6F!8Z>Lec3b{@g6ty6ud?xARxLXU#D$mz&F@%~%(}F(Feb zWA%E4_-XB>Kho6?`~f=i__V$yK%aQ;U&ZmbB!V+Rly%(;--XUg8k-vQr&`@hsVJHF zY}(-?ymd@pX3jQQX;SdD|1^%>LpTskPk}hk_d9f2#%i=c;8cSiEu8ojmd1H|?_- zr(NHk@!=--<%-{os~B~4{~Z4=^dUCv~jK zJuOpNNOj!?QH7~&LR}^W3zP0&TYp;OZ)AJ@HKrxs-q_By4>4V^RJQxrBOS9kC$5B> zBAYyzm&!l8ynK1T^sze0IcXceN{2P<{;PQFwztHw8nMg;!U<3L{TjZ0;Ij?NxtkZh z=i%a|t^ZGy_0-kcINwRsZSdQ!#QWm(o}HHKFFT|bGR|G8VKk?>KA~&T?&>45-{eEP}|E{3d>wKVd%1IrHhc=eh5B&wZbJpXc17 zZ+v}>Hkxk)02q;vc%1+M(k&r?M(H3V?M@T`o3@d?4hK-VYI(1JpbtVgZ|b%6nd^3t9VRB|0;F%l;}gnp(V?l&GRJJ|+jJQ!;2a92wO+f9IVo(FMm*`CI|y5-G$a99!G(xB)TR&~JR6Xei@n``bm$ZGq-a8g_!c zu;we^Zo2AtqqS8;8xG(s#fDoU1?fUez8m9ETT9p7>XQ}!;fxbDW#9x8d>GAAefH`~ zDdw+gmz$@)JCVSiQCwe&#X}I9ROqM2fVHMgY0n#*{qzjbB;a9zN?EZ+AxeHTbBMlW zf!ggriDT~{{DW?uLINiS2AxM~)AOuB)cVba!MpZnZpNa~qzm6m%NaKfV6%a8(p>2MuMG*l-#JeRq|npLAot~DEfK# zy<~@0n=XRO+&Erf$-<(x|{ zfcA3WC5|E#5@3)5y>?H3U^xdMsZRHYIv+qw z2HxL(`|h?uCam!Q9yIe8;|eL@^upbfyGm8?+y-dQ3l`}~>1!3zhQO(wc5{?$51%4Y zO)a!brmVlgAqBR3)DgZL8yz$$k8H-?DTq3zstJdZ)jgzdLvg?4THI>r-XH<%lXWlbHRy|+z=>0ZE{ zWjoBBZ!h7@?#sb^fp)5V@I&2{H$md8H0C0|A|Y=BkcMe$zkzG-*E zm?IIm`?X2!)vwzQRs}iPyCY1Va9PT&`A}bo)+Yy0YaZMZiY2RVhABnt@?lyuwFa-s zJ--7~m{*=lI9P9#b-+;8($q5$5V+GyC}+Z0R*Z|G{JR&AVDdCUoM7j?!xAYt9hSNH zD~DaHVw=TO;nyzgTlXB02Qo!w=({ko#1fXA~hR3NKqlC*VzRD9n=IHN;${Hdnaf zh8;JA=0*lOtCS;u9(KrwHYjSpnEI~nh&#KQKZXYliCcdWk`L=1P=A>Cx|11o%FkZs zn~urpzV(RPkNm&serdBn_NTl_g*!%)|F9!bYGk#}qDZe)iUa7YyyR+%=Q%_(+*&ZH*HcgwXZ}*$; z{G^5S#2SgmFKu3%Y%{Meh+Xvv2!iPbD3nnapjUgaH2;y0G59m9Rz zF*Ia0qxsJ(dxNi2Ez;pmFJM!#^-8gkCuK(aI+kxqR8ST(Z_c4f;T^mI6|IWi(8;>+ z?u^a~WZxFvY$%d%!VVQ6lVw3G7O1;*CzFh)hqzR(e5p8-kzWjuA_;nZFttbRq02~%=g+%g!msOFTo5LA z5?n??M2;V0Cuv14Pj{H=&({rdgVDAx+)C2Ahj(`9xe5-f+tWL(;pBzEb66H>=u~C~ z&(X~J>L~#cFjE4Zfu6Rj3xyj;n9^K?ogj3z`aD3+1h8MB_PdQ}_-UsH4B5xsm=5dS01<6A zCUZqkyon{Ly8@#=JTxk*?Cj%HbtaZ)t$8|K6U@I;#%!t&J@dc@%diF5 znC{3#0nI;`61qAxMEIf0E}F~7eB5YFNfiC$M+hBoypbf0TaXMssyAkIc3G$H0NDHS z^Xl4X6d7@`Ja}+#%;%|V15e&(o1hRTMb~!Q=?|UN*2ia1J;K9$-yEC;5qNN^m6|B~ z$ydBP23PXvvpW>d@@(t+MGRD!*X}lZE=!xCek``-CoCC?b|0P@uOl|>0KXl}BMyae z{){`!Gb)|3+lk%)6^bUpeD($ZcqZK%ADbjd)?Lmngb9}C928&hx=)aA8Td!q@uBU> z=njeY9dou7`?seaz()E~X$p8F@Y{lT_Sem7 zZ#s$t&+AkU7E1`Zx1^;$7YesSg(qF&^+JU`L?a|V(sgy0xGtwiUlWYDkpsMvcKGQf z>`z*h_xG*{9FX|5O%W&X=&Ay1z0Yd;5Cb^9cX@zX74S*!2Ja literal 0 HcmV?d00001 diff --git a/resources/icons/app_icon_32*32.png b/resources/icons/app_icon_32*32.png new file mode 100644 index 0000000000000000000000000000000000000000..9fb8df82c937c763beb07f7dc8e73f6eea9f0f5a GIT binary patch literal 372 zcmV-)0gL{LP)IU%&rmu-22okn^-sW;k%} zA#EK10te4N0{a#jfP!2?h@0Ws{WlCWcff&j4;dVdQKJFk`$r7_$!OXzk*eY3m1hjh zOpFXR`iN-ov{GU?aP|@Dwlk37fS2h=GfPmBGe9799Wk&yt?{DRRKukKY-t+AJx~G|M>{4A0}Pl(9l$WsqW}QGl3*TG SN_J%c0000KtJpTdv4|bmgoPmLt_&dW?d*v^7 zUGn2lFOmde|Fk3Rm8AXHk12Rm- zBr-B@zh!)XXU37ICmU~lFOu9*@L6x)+H@(o!&L{GjvI$x%)1$$y}RtU+@kz_TjJO> zxC~71?Ots6+}%HC=g(=|8}c7L>03Ad+n0kH8T#z^Z|Iyjf0_C22KkLpu)E*_tSQ!|Gdd%?ZuNI|F2vMX)eGtT=2Y(5*VLsb*mdyMO8L zEqfTc&kLVO={EZ2`zN+xm2XR_S#j0;Y2pg;AsYp*{y#g>k^RlK=G$@Vx6bd7HJZmO zHThrLRmR2(?eS(Sf<=DX)+)FjblcW&RGf2$SHpv%O<}@Uw7dSq?fv`jlDkNkmXE5# z!WGxVw!Y0N*z;PT?e|-wd8<>pUzRmD7Su|I%>TPm-m)cmexetn)9FPUPM)rK#V;Ka z{{QC*u8`1XO;(MSv%YLkNZBAAJ9igv%(=q_yZcU6Yzs1;@y$U+;9{;q*zZ-mtE}w{ z;{d^Iy+ z!6y>|)BkHD1zW8kLi+y_B+C Date: Sun, 26 Oct 2025 13:54:25 +0800 Subject: [PATCH 13/18] =?UTF-8?q?feat(=E6=96=87=E4=BB=B6=E8=A7=A3=E6=9E=90?= =?UTF-8?q?):=20=E5=A2=9E=E5=BC=BA=E6=96=87=E4=BB=B6=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=B9=B6=E6=94=AF=E6=8C=81=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 扩展文件解析功能,新增parse_and_convert_to_txt方法支持多种格式转换 添加图片数据处理逻辑,包括图片提取、存储和显示功能(失败) 优化文档解析逻辑,保留段落结构和空行格式 修复图片位置计算问题,确保图片能正确显示 --- ..._软件设计规格说明书_converted.txt | 287 ++++++++++++++ src/file_parser.py | 113 +++++- src/main_window.py | 4 +- src/typing_logic.py | 36 ++ src/word_main_window.py | 365 ++++++++++++++---- 5 files changed, 717 insertions(+), 88 deletions(-) create mode 100644 doc/04_软件设计规格说明书_converted.txt diff --git a/doc/04_软件设计规格说明书_converted.txt b/doc/04_软件设计规格说明书_converted.txt new file mode 100644 index 0000000..b463bdb --- /dev/null +++ b/doc/04_软件设计规格说明书_converted.txt @@ -0,0 +1,287 @@ + + +《软件工程课程设计》 +软件设计规格说明书 + + +MagicWord + + +组长学号姓名: 230340211 马子昂 +成员学号姓名: 230340233 石兴霖 + 230340219 黄俊源 + 230340210 陆冲 + 230340235 马明义 + + +二〇二五年九月 + + + +第1章 引言 +1.1 软件设计目标和原则 +设计目标 +隐私学习体验:允许用户在看似普通文档编辑的环境中学习内容(如试卷、单词或古文),从而避免他人的闲言碎语,实现“在Word里打开试卷,但他人看你是在敲文档”的效果。 +多格式文件支持:能够打开和处理多种文件格式,包括doc、txt、pdf和epub,以确保用户能够导入不同类型的学习材料。 +学习功能集成:支持单词学习、古文背诵等具体学习场景,通过打字输入输出相应内容,使学习过程交互化和可控。 +用户界面仿Word设计:提供与Microsoft Word相似的页面布局和体验,减少用户的学习曲线,增强熟悉感和易用性。 +附加功能增强:集成额外功能如查看天气、每日一言、历史上的今天和黄历等,以提升软件的实用性和用户粘性。 +图片输出支持:能够处理并输出文件中的图片,确保图片在打字输入过程中正常显示,并控制图片大小。 +设计原则 +用户中心原则:软件设计以用户需求为核心,注重隐私保护和用户体验,例如通过模仿Word界面来降低使用门槛。 +兼容性与扩展性原则:支持多种文件格式(特别是pdf和epub),表明设计时考虑了兼容性和未来可能的功能扩展,尽管这带来了技术挑战。 +功能丰富性原则:不仅专注于核心学习功能,还集成天气、每日一言等附加元素,以提供更全面的服务,增加软件的价值。 +创新性原则:瞄准市场上未有的功能(“开荒”),致力于解决独特痛点,如通过打字输入控制内容输出,体现创新思维。 +可行性驱动原则:在设计中承认潜在风险(如处理pdf文件的困难、图片输出的技术问题),强调务实 approach,确保开发过程注重技术可行性和问题解决。 +1.2 软件设计的约束和限制 +- 运行环境要求:Windows/MacOS +- 开发语言:python +- 标准规范:python编码风格(规范) +- 开发工具:PyCharm + +第2章 软件体系结构设计 +图 2-1 描述了 “MagicWord” 系统的设计架构,包括了用户界面层、业务逻辑层及数据层。其中: +界面层负责显示用户界面,承担了边界类 “伪装界面模块类”“输入处理模块类”“信息展示模块类” 与用户交互有关的职责。 +业务逻辑层负责核心业务逻辑处理,它将承担 “进度管理模块类”“伪装学习控制器类”“文件解析引擎类”“格式转换服务类” 等控制类的所有职责。“天气服务客户端类”“资讯内容接口类” 是用于支持与 “天气服务”“资讯服务” 外部子系统的交互,从而实现系统获取天气、资讯内容的功能。 +数据层负责系统的数据存储,它包括了 “进度数据存储类”“用户配置存储类” +“内容缓存管理类” 等实体类 + + + + +图2-1“MagicWord”系统体系结构逻辑视图 + +第3章 用户界面设计 +3.1 系统界面的外观设计及其类表示 + +图3-1 Magic Word系统界面外观设计:欢迎页面 +3.2 系统界面流设计 +<用简短语言描述该系统界面流的界面职责及跳转关系> +根据“Magic Word”的用例描述以及每个用例的交互图,可以发现该软件系统的APP 需要有以下一组界面以支持用户的操作。 +主界面(仿Word布局) +核心工作区:伪装成普通文档编辑界面 +实时显示学习内容(文本+图片) +集成状态栏(天气/黄历等附加信息) +隐私保护:动态生成干扰文本,维持"普通文档"外观 +文件加载界面 +支持多格式导入:doc/txt/pdf/epub +文件转换:PDF/EPUB转可编辑文本 +图片处理:提取图片并生成占位符 +学习模式界面 +设置学习场景:单词/古文/试卷 +配置学习参数:显示节奏、错误标记规则 +激活键盘驱动学习机制 +图片控制界面 +调整图片属性:尺寸/位置 +图片压缩优化 +文档流重排控制 +附加功能面板 +展示实用信息:天气/每日一言/历史事件 +可折叠侧边栏设计 +信息详情查看 + + +图3-Magic Word系统界面流的顺序图 +第4章 详细设计 +4.1 用例设计 +4.1.1打开文件用例实现的设计方案 +"打开文件"功能是系统的核心入口功能,负责处理用户选择并加载不同格式的学习文件。具体实现过程见图4-1所描述的用例设计顺序图。 + +图4-1 “打开文件”用例设计顺序图 +首先,用户通过界面类"MainUI"对象点击"打开文件"按钮,触发文件选择对话框。用户选择目标文件后,"MainUI"对象向控制类"FileController"发送消息"openFile(filePath)",请求打开指定文件。接收到消息后,"FileController"对象根据文件扩展名判断文件格式,并调用相应的文件解析器: +对于.txt格式,调用"TextParser"对象的"parseText(filePath)"方法 +对于.doc/.docx格式,调用"WordParser"对象的"parseWord(filePath)"方法 +对于.pdf格式,调用"PDFParser"对象的"parsePDF(filePath)"方法 +对于.epub格式,调用"EPUBParser"对象的"parseEPUB(filePath)"方法 +文件解析器负责读取文件内容并进行格式解析,将解析后的内容(包括文本、图片、格式信息等)封装为"Document"实体对象返回给"FileController"。"FileController"随后调用"DisplayManager"对象的"displayDocument(doc)"方法,将文档内容渲染到仿Word界面上。如果文件打开过程中出现错误(如文件损坏、格式不支持等),系统通过"MainUI"对象的"showError(message)"方法向用户显示错误信息。 + +4.1.2打字输入并输出文件内容用例实现的设计方案 +“打字输入并输出文件内容”功能的实现主要由“FileManager”和“DisplayManager”类协同完成。用户通过界面输入字符,系统将其与原文进行比对,并动态显示在仿Word界面上。具体实现过程见图4-2所描述的用例设计顺序图。 + +图4-2 “打字输入并输出内容”用例设计顺序图 +首先,用户通过“TypingUI”界面输入字符,触发“onKeyPress(event)”事件。随后,“TypingUI”对象向控制类“TypingController”发送消息“processInput(char)”,请求处理输入内容。“TypingController”接着调用“FileManager”对象的“getNextChar()”方法获取下一个待输入字符,并与用户输入进行比对。比对结果通过“DisplayManager”对象的“updateDisplay(char, status)”方法更新界面显示,其中status表示输入正确或错误。同时,“ProgressManager”对象负责更新用户的学习进度。 +4.1.3查看天气信息用例实现的设计方案 +“查看天气信息”功能通过调用外部天气API实现。用户在主界面点击天气图标后,系统获取并显示当前天气信息。具体实现过程见图4-3所描述的用例设计顺序图。 + +图4-3“查看天气”用例设计顺序图 +用户通过“MainUI”界面点击天气按钮,触发“onWeatherClick()”事件。“MainUI”对象向控制类“WeatherController”发送消息“fetchWeatherData()”。“WeatherController”调用“WeatherService”对象的“getWeather(location)”方法,通过HTTP请求获取天气数据。获取成功后,数据返回至“WeatherController”,再通过“MainUI”对象的“displayWeather(info)”方法显示在界面上 + +4.1.4查看每日一言用例实现的设计方案 +“查看每日一言”功能通过本地或网络获取每日激励语句。用户在主界面点击“每日一言”按钮后触发。具体实现过程见图4-4所描述的用例设计顺序图。 + +图4-4 “查看每日一言”用例设计顺序图 +用户通过“MainUI”界面点击“每日一言”按钮,触发“onDailyQuoteClick()”事件。“MainUI”对象向控制类“QuoteController”发送消息“getDailyQuote()”。“QuoteController”调用“QuoteService”对象的“fetchQuote()”方法获取语句内容。获取成功后,内容通过“MainUI”对象的“showQuote(text)”方法显示。 +4.1.5查看历史上的今天用例实现的设计方案 +“查看历史上的今天”功能通过调用历史事件API实现。用户在主界面点击相应按钮后触发。具体实现过程见图4-5所描述的用例设计顺序图。 + +图4-5 “查看历史上的今天”用例设计顺序图 +用户通过“MainUI”界面点击“历史上的今天”按钮,触发“onHistoryClick()”事件。“MainUI”对象向控制类“HistoryController”发送消息“getHistoryEvents()”。“HistoryController”调用“HistoryService”对象的“fetchEvents(date)”方法获取事件列表。获取成功后,通过“MainUI”对象的“displayHistory(events)”方法显示。 +4.1.6查看黄历信息用例实现的设计方案 +“查看黄历信息”功能通过本地数据库或网络API获取黄历信息。用户在主界面点击黄历图标后触发。具体实现过程见图4-6所描述的用例设计顺序图。 + +图4-6 “查看黄历信息”用例设计顺序图 +用户通过“MainUI”界面点击黄历按钮,触发“onAlmanacClick()”事件。“MainUI”对象向控制类“AlmanacController”发送消息“getAlmanacData()”。“AlmanacController”调用“AlmanacService”对象的“fetchAlmanac(date)”方法获取数据。获取成功后,通过“MainUI”对象的“showAlmanac(info)”方法显示。 +4.1.7输出文件中的图片用例实现的设计方案 +“输出文件中的图片”功能用于在打字学习过程中显示文档内嵌图片。具体实现过程见图4-7所描述的用例设计顺序图。 + +图4-7 “输出文件中的图片”用例设计顺序图 +当学习内容中包含图片时,“DisplayManager”对象调用“ImageRenderer”对象的“renderImage(imageData, position)”方法。“ImageRenderer”负责解码图片数据并调整尺寸,随后通过“TypingUI”对象的“displayImage(img)”方法在指定位置显示图片。 +4.1.8切换文件格式用例实现的设计方案 +“切换文件格式”功能允许用户在不同文件格式(如doc、pdf、epub)之间切换显示。具体实现过程见图4-8所描述的用例设计顺序图。 + +图4-8 “切换文件格式”用例设计顺序图 +用户通过“MainUI”界面选择文件格式,触发“onFormatChange(format)”事件。“MainUI”对象向“FormatController”发送消息“switchFormat(format)”。“FormatController”调用“FileManager”对象的“convertFile(format)”方法进行格式转换,转换成功后通过“DisplayManager”更新界面内容。 +4.1.9保存当前进度用例实现的设计方案 +“保存当前进度”功能用于记录用户的学习进度,以便下次继续学习。具体实现过程见图4-9所描述的用例设计顺序图。 + +图4-9 “保存当前进度”用例设计顺序图 +用户通过“MainUI”界面点击保存按钮,触发“onSaveProgress()”事件。“MainUI”对象向“ProgressController”发送消息“saveProgress(userId, progress)”。“ProgressController”调用“ProgressManager”对象的“saveToDatabase(progressData)”方法将进度数据存入数据库。保存成功后,界面显示保存成功提示。 +4.2 类设计 +核心类属性与操作 +MainUI: +属性(currentFilename、statusMessage、displayContent); +操作(文件/内容显示、错误提示、状态更新等)。 +FileDialog: +属性(supportedFormats、currentDirectory); +操作(浏览目录、选择/取消文件)。 +FileManager: +属性(maxFileSize、supportedFormats); +操作(打开/验证文件、解析文本/提取图片)。 +InputHandler: +属性(charCount、imageTriggerThreshold); +操作(按键处理、字符输出、重置计数器)。 +WeatherServiceController: +操作(获取/解析天气数据)。 +QuoteServiceController: +操作(获取每日名言并缓存)。 +HistoryServiceController: +操作(获取历史事件并过滤)。 +AlmanacServiceController: +操作(获取年鉴数据并缓存)。 +SettingsManager: +属性(supportedFormats); +操作(更新格式、保存设置)。 +ProgressManager: +操作(保存/加载进度)。 +类间关系 +MainUI 依赖多个控制器(如 FileManager、InputHandler 等),通过方法调用实现功能。 +FileDialog 为 FileManager 提供文件选择界面,二者协作完成文件操作。 +FileManager 处理文件后生成 FileContent 对象(包含文本、图片等信息)。 +各服务控制器(Weather/Quote/History/Almanac)分别与对应数据模型(Weather/DailyQuote/HistoricalEvent/Almanac)关联,负责数据获取与缓存。 +SettingsManager 管理 UserSettings 配置; +ProgressManager 管理 ProgressData 进度信息。 + + +图4-10 MagicWord系统设计类图 +4.3 数据模型设计 +4.3.1 MagicWord系统数据设计类图 +数据库表设计: +1.T_progressData(学习进度表) +2.T_UserSettings(用户设置表) +3.T_User(用户表) + + +图4-11MagicWord系统数据设计类图 +4.3.2 MagicWord系统数据的操作设计 +1. ProgressDataLibrary 类设计 +为了支持对"T_ProgressData"数据库表的操作,设计模型中有关键设计类"ProgressDataLibrary",它提供了一组方法以实现对学习进度数据的增删改查操作。具体的接口描述如下: +boolean insertProgress(ProgressData progress) +boolean deleteProgress(int progressID) +boolean updateProgress(ProgressData progress) +ProgressData getProgressByID(int progressID) +List getProgressByUser(int userID) +List getRecentProgress(int userID, int limit) +ProgressDataLibrary() +~ProgressDataLibrary() +void openDatabase() +void closeDatabase() +2. UserSettingsLibrary 类设计 +为了支持对"T_UserSettings"数据库表的操作,设计模型中有关键设计类"UserSettingsLibrary",它提供用户设置数据的管理功能。具体的接口描述如下: +boolean insertSettings(UserSettings settings) +boolean updateSettings(UserSettings settings) +UserSettings getSettingsByUser(int userID) +boolean updateSupportedFormats(int userID, String formats) +boolean updateImageThreshold(int userID, int threshold) +UserSettingsLibrary() +~UserSettingsLibrary() +void openDatabase() +void closeDatabase() +3.UserLibrary 类设计 +为了支持对"T_User"数据库表的操作,设计模型中有关键设计类"UserLibrary",它提供用户基本信息的完整管理功能。具体的接口描述如下: +boolean insertUser(User user) +boolean deleteUser(User user) +boolean updateUser(User user) +User getUserByAccount(String account) +User getUserByID(int userID) +boolean verifyUserValidity(String account, String password) +boolean isUsernameExists(String username) +boolean updatePassword(int userID, String newPassword) +UserLibrary() +~UserLibrary() +void openDatabase() +void closeDatabase() +4. 数据库连接管理设计 +DatabaseManager 单例类: +static DatabaseManager getInstance() +Connection getConnection() +void releaseConnection(Connection conn) +boolean testConnection() +void beginTransaction() +void commitTransaction() +void rollbackTransaction() +5. 异常处理设计 +自定义异常类: +DatabaseConnectionException - 数据库连接异常 +DataAccessException - 数据访问异常 +UserNotFoundException - 用户不存在异常 +DuplicateUserException - 用户重复异常 +6. 数据验证规则 +用户数据验证: +用户名:3-20字符,只能包含字母数字 +密码:6-50字符,必须包含字母和数字 +邮箱:符合标准邮箱格式 +文件路径:最大255字符,路径有效性检查 +进度数据验证: +字符数:非负整数 +光标位置:有效范围内 +时间戳:合理的时间范围 +4.4 部署设计 +"MagicWord系统"采用混合部署的方式(见图4-X),其中"MagicWord客户端"子系统部署在用户本地的Windows或macOS操作系统上;"云服务API服务器"子系统部署在云端基于Ubuntu操作系统的云服务器上,它通过RESTful API与第三方服务进行交互。云端服务器还部署了MySQL数据库管理系统,以保存系统中的用户信息和学习数据。客户端与服务器之间通过HTTPS协议进行网络连接,从而实现数据同步和服务交互。 + +图4-12 MagicWord系统的部署图 +其中,软件系统各组成部分产品所运行的外部环境如表4-1所示: +表4-1 软件与外界环境的交互关系 +4.4.1 网络通信架构 +客户端与服务器之间的通信采用标准的HTTP/HTTPS协议,具体通信模式如下: +客户端→服务器通信: +认证接口:用户登录验证、Token刷新 +数据同步接口:学习进度上传下载、设置同步 +内容服务接口:天气信息、每日一言、历史事件等获取 +服务器→第三方服务通信: +天气数据:通过和风天气API获取实时天气信息 +内容服务:从权威数据源获取每日名言、历史事件等 +文件解析服务:复杂文件格式的云端解析支持 +4.4.2 数据存储策略 +系统采用分层数据存储架构,确保数据安全性和访问效率: +本地存储(SQLite): +用户当前学习进度 +个性化界面设置 +最近打开的文件记录 +网络数据缓存(24小时有效期) +云端存储(MySQL): +用户账户信息(加密存储) +跨设备学习进度备份 +用户使用统计和分析数据 +系统配置和版本信息 +4.4.3 安全部署措施 +为确保系统安全性,部署过程中采取以下安全措施: +通信安全: +全链路HTTPS加密传输 +JWT Token身份认证机制 +API访问频率限制和防爬虫保护 +敏感数据端到端加密 +数据安全: +用户密码采用bcrypt加密存储 +个人学习数据隔离存储 +定期数据备份和灾难恢复机制 +符合GDPR的数据隐私保护规范 \ No newline at end of file diff --git a/src/file_parser.py b/src/file_parser.py index 2ab8681..b6362d2 100644 --- a/src/file_parser.py +++ b/src/file_parser.py @@ -6,8 +6,8 @@ from typing import Union, List, Tuple class FileParser: @staticmethod def parse_file(file_path: str) -> str: - - # 验证文件路径123 + """解析文件并返回文本内容""" + # 验证文件路径 if not FileParser.validate_file_path(file_path): raise ValueError(f"Invalid file path: {file_path}") @@ -29,8 +29,91 @@ class FileParser: # 统一异常处理 raise Exception(f"Error parsing file {file_path}: {str(e)}") + @staticmethod + def parse_and_convert_to_txt(file_path: str, output_dir: str = None) -> dict: + """ + 解析文件并转换为txt格式,保留图片和分段 + + Args: + file_path: 输入文件路径 + output_dir: 输出目录,如果为None则使用临时目录 + + Returns: + dict: 包含转换结果的信息 + - 'txt_path': 生成的临时txt文件路径 + - 'images': 提取的图片列表 [(文件名, 二进制数据), ...] + - 'content': 转换后的文本内容 + - 'success': 是否成功 + - 'error': 错误信息(如果有) + """ + try: + # 验证输入文件 + if not FileParser.validate_file_path(file_path): + return { + 'success': False, + 'error': f"Invalid file path: {file_path}" + } + + # 使用临时文件而不是永久文件 + import tempfile + + # 获取文件扩展名 + _, ext = os.path.splitext(file_path) + ext = ext.lower() + + # 提取文本内容 + content = "" + images = [] + + if ext == '.txt': + # TXT文件:直接读取内容 + content = FileParser.parse_txt(file_path) + images = [] # TXT文件没有图片 + + elif ext == '.docx': + # DOCX文件:提取文本和图片 + content = FileParser.parse_docx(file_path) + images = FileParser.extract_images_from_docx(file_path) + + elif ext == '.pdf': + # PDF文件:提取文本(图片处理较复杂,暂时只提取文本) + content = FileParser.parse_pdf(file_path) + images = [] # PDF图片提取较复杂,暂时跳过 + + else: + return { + 'success': False, + 'error': f"Unsupported file format: {ext}" + } + + # 创建临时文件而不是永久文件 + base_name = os.path.splitext(os.path.basename(file_path))[0] + + # 创建临时txt文件,程序结束时会被自动清理 + with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', + suffix=f'_{base_name}_converted.txt', + delete=False) as temp_file: + temp_file.write(content) + txt_path = temp_file.name + + return { + 'success': True, + 'txt_path': txt_path, + 'images': images, + 'content': content, + 'original_ext': ext, + 'is_temp_file': True # 标记这是临时文件 + } + + except Exception as e: + return { + 'success': False, + 'error': str(e) + } + @staticmethod def parse_txt(file_path: str) -> str: + """解析TXT文件""" # 验证文件路径 if not FileParser.validate_file_path(file_path): raise ValueError(f"Invalid file path: {file_path}") @@ -125,7 +208,7 @@ class FileParser: @staticmethod def parse_docx(file_path: str) -> str: - + """解析DOCX文件,保留段落结构""" # 验证文件路径 if not FileParser.validate_file_path(file_path): raise ValueError(f"Invalid file path: {file_path}") @@ -140,12 +223,16 @@ class FileParser: try: doc = Document(file_path) - # 提取所有段落文本 + # 提取所有段落文本,保留空行以保持格式 paragraphs = [] for paragraph in doc.paragraphs: - paragraphs.append(paragraph.text) + text = paragraph.text.strip() + if text: # 非空段落 + paragraphs.append(paragraph.text) + else: # 空段落,用空行表示 + paragraphs.append("") - # 用换行符连接所有段落 + # 用换行符连接所有段落,保留空行 content = '\n'.join(paragraphs) return content @@ -154,7 +241,7 @@ class FileParser: @staticmethod def parse_pdf(file_path: str) -> str: - + """解析PDF文件,保留段落结构""" # 验证文件路径 if not FileParser.validate_file_path(file_path): raise ValueError(f"Invalid file path: {file_path}") @@ -172,9 +259,13 @@ class FileParser: pdf_reader = PyPDF2.PdfReader(file) # 提取每一页的文本 - for page in pdf_reader.pages: - content += page.extract_text() - content += "\n" + for i, page in enumerate(pdf_reader.pages): + page_text = page.extract_text() + if page_text: + content += page_text + # 在页面之间添加空行分隔 + if i < len(pdf_reader.pages) - 1: + content += "\n\n" return content except Exception as e: @@ -182,7 +273,7 @@ class FileParser: @staticmethod def validate_file_path(file_path: str) -> bool: - + """验证文件路径是否有效""" # 检查文件是否存在 if not os.path.exists(file_path): return False diff --git a/src/main_window.py b/src/main_window.py index 9522314..6c8afa6 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -272,7 +272,7 @@ class MainWindow(QMainWindow): def openFile(self): """ 打开文件选择对话框并加载选中的文件 - - 显示文件选择对话框,过滤条件:*.txt, *.docx + - 显示文件选择对话框,过滤条件:*.txt, *.docx, *.pdf - 如果用户选择了文件,调用FileParser.parse_file(file_path) - 成功时:将内容存储但不直接显示,重置打字状态 - 失败时:显示错误消息框 @@ -282,7 +282,7 @@ class MainWindow(QMainWindow): self, "打开文件", "", - "文本文件 (*.txt);;Word文档 (*.docx);;所有文件 (*)", + "文本文件 (*.txt);;Word文档 (*.docx);;PDF文件 (*.pdf);;所有文件 (*)", options=options ) diff --git a/src/typing_logic.py b/src/typing_logic.py index b7cfbcf..6d020fb 100644 --- a/src/typing_logic.py +++ b/src/typing_logic.py @@ -12,6 +12,8 @@ class TypingLogic: self.total_chars = len(learning_content) self.typed_chars = 0 self.image_positions = [] # 存储图片位置信息 + self.image_data = {} # 存储图片数据 {图片名称: 二进制数据} + self.image_display_queue = [] # 待显示的图片队列 def check_input(self, user_text: str) -> dict: """ @@ -136,6 +138,8 @@ class TypingLogic: self.error_count = 0 self.typed_chars = 0 self.image_positions = [] # 重置图片位置信息 + self.image_data = {} # 重置图片数据 + self.image_display_queue = [] # 重置图片显示队列 def get_statistics(self) -> dict: """ @@ -170,6 +174,38 @@ class TypingLogic: return img_info return None + def set_image_data(self, image_data: dict): + """ + 设置图片数据 + - image_data: 字典,{图片名称: 二进制数据} + """ + self.image_data = image_data + + def get_images_to_display(self, current_position: int) -> list: + """ + 获取在当前位置需要显示的图片 + - current_position: 整数,当前输入位置 + - 返回图片信息列表 + """ + images_to_display = [] + for img_info in self.image_positions: + if img_info['start_pos'] <= current_position <= img_info['end_pos']: + # 尝试获取图片名称(支持多种键名) + image_name = img_info.get('image_name', '') or img_info.get('filename', '') + if image_name in self.image_data: + img_info_copy = img_info.copy() + img_info_copy['image_data'] = self.image_data[image_name] + images_to_display.append(img_info_copy) + return images_to_display + + def should_show_image(self, current_position: int) -> bool: + """ + 检查在当前位置是否应该显示图片 + - current_position: 整数,当前输入位置 + - 返回布尔值 + """ + return len(self.get_images_to_display(current_position)) > 0 + def check_image_at_position(self, position: int) -> bool: """ 检查指定位置是否有图片 diff --git a/src/word_main_window.py b/src/word_main_window.py index ed1cf8f..9a9c78e 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -83,6 +83,9 @@ class WordStyleMainWindow(QMainWindow): self.unified_document_content = "" # 统一文档内容 self.last_edit_mode = "typing" # 上次编辑模式 + # 临时文件管理 + self.temp_files = [] # 跟踪创建的临时文件 + # 初始化网络服务和WeatherAPI self.network_service = NetworkService() self.weather_api = WeatherAPI() @@ -282,10 +285,10 @@ class WordStyleMainWindow(QMainWindow): new_action.triggered.connect(self.new_document) file_menu.addAction(new_action) - # 打开 - open_action = QAction('打开(O)...', self) + # 导入文件 - 改为导入功能 + open_action = QAction('导入文件(I)...', self) open_action.setShortcut('Ctrl+O') - open_action.triggered.connect(self.open_file) + open_action.triggered.connect(self.import_file) file_menu.addAction(open_action) # 保存 @@ -587,6 +590,11 @@ class WordStyleMainWindow(QMainWindow): # 保存当前学习进度,以便在模式切换时恢复 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] @@ -742,6 +750,10 @@ class WordStyleMainWindow(QMainWindow): # 保存打字内容到文档A self.typing_mode_content = current_text + + # 更新学习进度(用于打字模式显示) + if hasattr(self, 'learning_progress'): + self.learning_progress = len(current_text) def on_text_changed_original(self): """文本变化处理 - 支持逐步显示模式和自由打字模式""" @@ -791,6 +803,7 @@ class WordStyleMainWindow(QMainWindow): self.text_edit.setTextCursor(cursor) # 在文本中插入图片(如果有的话) + # 注意:必须在更新文本后调用,且要处理图片插入对文本长度的影响 self.insert_images_in_text() # 重新连接文本变化信号 @@ -814,6 +827,9 @@ class WordStyleMainWindow(QMainWindow): # 检查当前位置是否有图片 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() @@ -1154,59 +1170,181 @@ class WordStyleMainWindow(QMainWindow): 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这是一个仿Microsoft Word界面的学习工具。") - self.imported_content = "" - self.displayed_chars = 0 + # 学习模式:重置为默认内容,需要导入文件 + 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.imported_content = "" - self.displayed_chars = 0 self.status_bar.showMessage("新建文档 - 打字模式,可以自由开始打字", 3000) - def open_file(self): - """打开文件 - 创建空白副本并在学习模式下显示导入内容""" + def import_file(self): + """导入文件 - 仅在导入时存储内容,不立即显示""" file_path, _ = QFileDialog.getOpenFileName( - self, "打开文件", "", + self, "导入文件", "", "文档文件 (*.docx *.txt *.pdf);;所有文件 (*.*)" ) if file_path: try: - # 解析文件 + # 使用新的转换方法,将文件转换为txt格式 parser = FileParser() - content = parser.parse_file(file_path) + result = parser.parse_and_convert_to_txt(file_path) - if content: - # 设置文件加载标志 - self.is_loading_file = True + 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(file_path)},开始打字逐步显示学习内容!", 5000) + self.status_bar.showMessage(f"已导入: {os.path.basename(txt_path)},开始打字逐步显示学习内容!", 5000) + else: + # 打字模式:不显示导入内容,保持当前内容 + self.status_bar.showMessage(f"已导入: {os.path.basename(txt_path)},切换到学习模式查看内容", 5000) - elif self.view_mode == "typing": - # 打字模式:也设置内容,但允许自由打字 - if self.typing_logic: - self.typing_logic.reset("") # 重置打字状态,但内容为空,允许自由打字 + # 提取并显示图片(如果有) + if images: + self.extract_and_display_images(content, 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.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.status_bar.showMessage(f"已创建空白副本,可以自由打字", 5000) + # 创建空白副本 - 清空文本编辑器 + 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 @@ -1351,13 +1489,18 @@ class WordStyleMainWindow(QMainWindow): try: if mode == "typing": - # 打字模式:显示文档A的内容 - self.status_bar.showMessage("切换到打字模式 - 显示文档A内容", 3000) + # 打字模式:显示学习模式下已输入的内容(文档A) + self.status_bar.showMessage("切换到打字模式 - 显示已输入的内容", 3000) - # 设置文档A的内容 + # 设置文档A的内容(学习模式下已输入的内容) self.text_edit.clear() - if self.typing_mode_content: - self.text_edit.setPlainText(self.typing_mode_content) + if hasattr(self, 'learning_progress') and self.learning_progress > 0: + # 显示学习模式下已输入的内容 + display_text = self.imported_content[:self.learning_progress] + self.text_edit.setPlainText(display_text) + else: + # 如果没有学习进度,显示默认提示 + self.text_edit.setPlainText("请先在学习模式下输入内容") # 设置光标位置到文档末尾 cursor = self.text_edit.textCursor() @@ -1366,7 +1509,14 @@ class WordStyleMainWindow(QMainWindow): # 重置打字逻辑,准备接受新的输入 if self.typing_logic: - self.typing_logic.reset("") + if hasattr(self, 'learning_progress') and self.learning_progress > 0: + # 使用已输入的内容作为打字逻辑的基础 + self.typing_logic.reset(self.imported_content) + # 设置当前位置到已输入的末尾 + self.typing_logic.current_index = self.learning_progress + self.typing_logic.typed_chars = self.learning_progress + else: + self.typing_logic.reset("") # 重置显示字符计数 self.displayed_chars = 0 @@ -1859,6 +2009,11 @@ class WordStyleMainWindow(QMainWindow): 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() @@ -1867,65 +2022,83 @@ class WordStyleMainWindow(QMainWindow): 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 self.typing_logic.image_positions: + 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'] and current_length >= image_info['start_pos']: + # 当打字进度达到图片位置时插入图片 - 修复条件,确保图片能显示 + 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 = min(image_info['start_pos'], current_length) + # 计算图片应该插入的位置(基于原始内容位置) + insert_position = image_info['start_pos'] - # 确保插入位置有效 + # 确保插入位置有效(不能超过当前显示内容长度) 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) + # 获取图片数据(优先使用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) - # 在图片后插入一个空格,让文字继续 - cursor.insertText(" ") + # 将图片保存到临时文件(使用更稳定的路径) + import tempfile + import os + temp_dir = tempfile.gettempdir() - # 标记这张图片已经插入过 - self.inserted_images.add(image_key) + # 确保文件名安全 + 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) - # 记录插入成功 - print(f"图片 {image_info['filename']} 已在位置 {insert_position} 插入") + 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"保存临时图片文件失败: {temp_file}") - else: - print(f"加载图片数据失败: {image_info['filename']}") + print(f"加载图片数据失败: {image_info['filename']}") # 重新设置光标到文本末尾 cursor.movePosition(cursor.End) @@ -1948,6 +2121,9 @@ class WordStyleMainWindow(QMainWindow): def closeEvent(self, event): """关闭事件处理""" + # 清理临时文件 + self.cleanup_temp_files() + if self.is_modified: reply = QMessageBox.question( self, "确认退出", @@ -1965,6 +2141,18 @@ class WordStyleMainWindow(QMainWindow): 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 extract_and_display_images(self, file_path): """提取并显示Word文档中的图片 - 修复图片位置计算""" try: @@ -2009,15 +2197,33 @@ class WordStyleMainWindow(QMainWindow): 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: - # 只有一张图片,放在文档中间 - start_pos = len(self.imported_content) // 2 + # 只有一张图片,放在文档开始位置附近(前10%),确保用户能快速看到 + start_pos = max(10, content_length // 10) else: - # 多张图片,均匀分布 - start_pos = (len(self.imported_content) * (index + 1)) // (len(images) + 1) + # 多张图片:前几张放在较前位置,确保用户能看到 + 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, len(self.imported_content)) + end_pos = min(start_pos + 50, content_length) image_positions.append({ 'start_pos': start_pos, @@ -2029,6 +2235,15 @@ class WordStyleMainWindow(QMainWindow): # 设置图片位置信息到打字逻辑 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) -- 2.34.1 From 1dea565fe06c330951dc7825a9ca883aa81a6355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E5=AD=90=E6=98=82?= <929110464@qq.com> Date: Sun, 26 Oct 2025 16:24:31 +0800 Subject: [PATCH 14/18] =?UTF-8?q?feat(=E6=AF=8F=E6=97=A5=E4=B8=80=E8=A8=80?= =?UTF-8?q?):=20=E5=A2=9E=E5=8A=A0=E5=8F=A4=E8=AF=97=E8=AF=8D=E9=80=89?= =?UTF-8?q?=E9=A1=B9=E5=B9=B6=E4=BC=98=E5=8C=96=E6=98=BE=E7=A4=BA=E5=B8=83?= =?UTF-8?q?=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加类型选择下拉框,支持普通箴言和古诗词句切换 - 增大每日一言显示区域尺寸,提升阅读体验 - 使用不同API获取古诗词内容 - 优化天气数据显示,增加生活提示信息 --- src/ui/word_style_ui.py | 131 +++++++++++++++++++++++++++++----------- src/word_main_window.py | 109 +++++++++++++++++++-------------- 2 files changed, 160 insertions(+), 80 deletions(-) diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index 0d90ce2..98c29f3 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -228,22 +228,39 @@ class WordRibbon(QFrame): quote_group = self.create_ribbon_group("每日一言") quote_layout = QVBoxLayout() - # 每日一言显示标签 - self.quote_label = QLabel("暂无") - self.quote_label.setStyleSheet("QLabel { color: #666666; font-style: italic; font-size: 10px; }") - self.quote_label.setWordWrap(True) - self.quote_label.setFixedWidth(150) + # 创建第一行:类型选择下拉框和刷新按钮 + top_row_layout = QHBoxLayout() + + # 类型选择下拉框 + self.quote_type_combo = QComboBox() + self.quote_type_combo.addItems(["普通箴言", "古诗词句"]) + self.quote_type_combo.setFixedSize(120, 25) + self.quote_type_combo.currentTextChanged.connect(self.on_quote_type_changed) # 刷新按钮 self.refresh_quote_btn = QPushButton("刷新箴言") self.refresh_quote_btn.clicked.connect(self.on_refresh_quote) self.refresh_quote_btn.setFixedSize(80, 25) + # 添加到第一行布局 + top_row_layout.addWidget(self.quote_type_combo) + top_row_layout.addWidget(self.refresh_quote_btn) + top_row_layout.addStretch() # 添加弹性空间,使控件靠左对齐 + + # 每日一言显示标签 - 增大尺寸 + self.quote_label = QLabel("暂无") + self.quote_label.setStyleSheet("QLabel { color: #666666; font-style: italic; font-size: 10px; }") + self.quote_label.setWordWrap(True) + self.quote_label.setFixedWidth(250) # 增加到250像素宽度 + self.quote_label.setMinimumHeight(40) # 设置最小高度,增加显示空间 + + # 添加到主布局 + quote_layout.addLayout(top_row_layout) quote_layout.addWidget(self.quote_label) - quote_layout.addWidget(self.refresh_quote_btn) quote_group.setLayout(quote_layout) self.quote_group = quote_group + self.current_quote_type = "普通箴言" # 默认类型 # 组件创建完成后自动获取每日一言 self.load_daily_quote() @@ -303,44 +320,63 @@ class WordRibbon(QFrame): def load_daily_quote(self): """加载每日一言""" try: - # 创建每日一言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', '暂无每日一言') - self.update_quote_display(quote_text) + # 根据当前选择的类型获取不同的内容 + if self.current_quote_type == "古诗词句": + # 获取古诗词 + quote_text = self.get_chinese_poetry() else: - # 如果API返回空或格式不正确,显示默认文本 - self.update_quote_display("暂无每日一言") + # 获取普通箴言 + 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', '暂无每日一言') + else: + quote_text = "暂无每日一言" + + self.update_quote_display(quote_text) except Exception as e: print(f"加载每日一言失败: {e}") - self.update_quote_display("暂无每日一言") + self.update_quote_display("获取失败") def on_refresh_quote(self): """刷新每日一言按钮点击处理""" + self.load_daily_quote() + + def on_quote_type_changed(self, quote_type): + """每日一言类型切换处理""" + self.current_quote_type = quote_type + # 类型切换时自动刷新内容 + self.load_daily_quote() + + def get_chinese_poetry(self): + """获取古诗词 - 使用古诗词·一言API(随机返回不同诗词)""" try: - # 创建每日一言API实例 - quote_api = daily_sentence_API("https://api.nxvav.cn/api/yiyan") - - # 获取每日一言数据 - quote_data = quote_api.get_sentence('json') + # 使用古诗词·一言API - 每次返回随机不同的诗词 + response = requests.get("https://v1.jinrishici.com/all.json", timeout=5) - if quote_data and isinstance(quote_data, dict): - # 从返回的数据中提取每日一言文本 - quote_text = quote_data.get('yiyan', '暂无每日一言') - self.update_quote_display(quote_text) + if response.status_code == 200: + data = response.json() + content = data.get('content', '') + author = data.get('author', '') + title = data.get('origin', '') + + # 格式化显示文本 + if content and author and title: + return f"{content} — {author}《{title}》" + elif content and author: + return f"{content} — {author}" + elif content: + return content + else: + return "暂无古诗词" else: - # 如果API返回空或格式不正确,显示默认文本 - self.update_quote_display("获取每日一言失败") + return "获取古诗词失败" except Exception as e: - print(f"获取每日一言失败: {e}") - self.update_quote_display("获取每日一言失败") + print(f"获取古诗词失败: {e}") + return "获取古诗词失败" def update_quote_display(self, quote_text): """更新每日一言显示""" @@ -643,6 +679,28 @@ class WeatherAPI: city_info = data['cityInfo'] current_data = data['data'] + # 获取生活提示信息 + life_tips = [] + forecast = current_data.get('forecast', []) + if forecast: + # 从预报中提取提示信息 + for day in forecast[:3]: # 取前3天的提示 + notice = day.get('notice', '') + if notice: + life_tips.append(notice) + + # 如果没有获取到足够的提示,添加一些默认的 + default_tips = [ + "愿你拥有比阳光明媚的心情", + "雾霾来袭,戴好口罩再出门", + "今天天气不错,适合出去走走", + "记得多喝水,保持身体健康" + ] + + # 补充到至少3个提示 + while len(life_tips) < 3 and default_tips: + life_tips.append(default_tips.pop(0)) + weather_info = { 'temp': current_data['wendu'], 'feels_like': current_data['wendu'], # 没有体感温度,用实际温度代替 @@ -651,7 +709,8 @@ class WeatherAPI: 'wind_dir': current_data['forecast'][0]['fx'], 'wind_scale': '1', # 没有风力等级,用默认值 'vis': current_data['forecast'][0]['high'], # 用最高温作为可见度 - 'pressure': '1013' # 没有气压,用默认值 + 'pressure': '1013', # 没有气压,用默认值 + 'life_tips': life_tips # 添加生活提示信息 } print(f"解析后的天气信息: {weather_info}") return weather_info @@ -699,7 +758,8 @@ class WeatherAPI: weather_data = { 'city': city_name, 'current': current, - 'forecast': forecast + 'forecast': forecast, + 'life_tips': current.get('life_tips', []) # 添加生活提示信息 } return weather_data else: @@ -1153,7 +1213,8 @@ class WeatherAPI: 'humidity': weather_info['humidity'], 'wind_scale': weather_info['wind_scale'] }, - 'forecast': forecast + 'forecast': forecast, + 'life_tips': weather_info.get('life_tips', []) # 添加生活提示信息 } print(f"无法获取城市ID: {original_city_name}") diff --git a/src/word_main_window.py b/src/word_main_window.py index 9a9c78e..aa4e950 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -158,16 +158,8 @@ class WordStyleMainWindow(QMainWindow): weather_data = self.weather_api.get_weather_data(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'] - } - print(f"格式化后的数据: {formatted_data}") - self.update_weather_display(formatted_data) + # 直接传递原始数据,update_weather_display会处理嵌套结构 + self.update_weather_display(weather_data) else: print(f"无法获取城市 {city} 的天气数据") @@ -205,7 +197,8 @@ class WordStyleMainWindow(QMainWindow): 'temperature': weather_data['current']['temp'], 'description': weather_data['current']['weather'], 'humidity': weather_data['current']['humidity'], - 'wind_scale': weather_data['current']['wind_scale'] + 'wind_scale': weather_data['current']['wind_scale'], + 'life_tips': weather_data.get('life_tips', []) } print(f"格式化后的数据: {formatted_data}") self.update_weather_display(formatted_data) @@ -929,25 +922,34 @@ class WordStyleMainWindow(QMainWindow): def update_weather_display(self, weather_data): """更新天气显示""" - print(f"接收到天气数据: {weather_data}") if 'error' in weather_data: - print(f"天气显示错误: {weather_data['error']}") self.status_bar.showMessage(f"天气数据获取失败: {weather_data['error']}", 3000) else: + # 处理嵌套的天气数据结构 city = weather_data.get('city', '未知城市') - temp = weather_data.get('temperature', 'N/A') - desc = weather_data.get('description', 'N/A') - humidity = weather_data.get('humidity', 'N/A') - wind_scale = weather_data.get('wind_scale', 'N/A') + + # 从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, 湿度{humidity}%, 风力{wind_scale}级" - print(f"显示天气信息: {weather_message}") + weather_message = f"{city}: {desc}, {temp}°C{temp_range}" self.status_bar.showMessage(weather_message, 5000) - # 存储天气数据供其他功能使用 + # 存储天气数据供其他功能使用(确保包含生活提示) self.current_weather_data = weather_data - print("天气数据已存储") + print(f"update_weather_display - 存储的current_weather_data包含life_tips: {self.current_weather_data.get('life_tips', [])}") def refresh_weather(self): """手动刷新天气信息""" @@ -964,15 +966,15 @@ class WordStyleMainWindow(QMainWindow): weather_data = self.weather_api.get_weather_data(current_city) if weather_data: - # 格式化天气数据 + # 格式化天气数据为扁平结构,便于update_weather_display使用 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'], - 'forecast': weather_data['forecast'] + '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) self.status_bar.showMessage("天气数据已刷新", 2000) else: @@ -990,6 +992,7 @@ class WordStyleMainWindow(QMainWindow): return weather_data = self.current_weather_data + print(f"详细天气对话框 - 天气数据: {weather_data}") # 创建对话框 dialog = QDialog(self) @@ -1006,11 +1009,26 @@ class WordStyleMainWindow(QMainWindow): current_layout = QVBoxLayout() current_layout.addWidget(QLabel("当前天气:")) + # 获取温度信息,支持嵌套结构 + current_data = weather_data.get('current', {}) + temp = current_data.get('temp', 'N/A') + if temp != 'N/A' and isinstance(temp, str): + temp = float(temp) if temp.replace('.', '').isdigit() else temp + + # 从预报数据中获取最高和最低气温 + temp_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""" -温度: {weather_data.get('temperature', 'N/A')}°C -天气状况: {weather_data.get('description', 'N/A')} -湿度: {weather_data.get('humidity', 'N/A')}% -风力: {weather_data.get('wind_scale', 'N/A')}级 +当前温度: {temp}°C +最高气温: {temp_max}°C +最低气温: {temp_min}°C +天气状况: {current_data.get('weather', 'N/A')} """ current_text = QTextEdit() current_text.setPlainText(current_info.strip()) @@ -1019,22 +1037,23 @@ class WordStyleMainWindow(QMainWindow): layout.addLayout(current_layout) - # 天气预报信息 - if 'forecast' in weather_data and weather_data['forecast']: - forecast_layout = QVBoxLayout() - forecast_layout.addWidget(QLabel("天气预报:")) + # 生活提示信息(替换原来的天气预报) + 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("生活提示:")) - forecast_text = QTextEdit() - forecast_info = "" - for i, day in enumerate(weather_data['forecast'][:3]): # 显示最近3天的预报 - if i < len(weather_data['forecast']): - day_data = weather_data['forecast'][i] - forecast_info += f"第{i+1}天: {day_data.get('fxDate', 'N/A')} - {day_data.get('textDay', 'N/A')}, {day_data.get('tempMin', 'N/A')}~{day_data.get('tempMax', 'N/A')}°C\n" + tips_text = QTextEdit() + tips_info = "" + for tip in life_tips: + tips_info += f"• {tip}\n" - forecast_text.setPlainText(forecast_info.strip()) - forecast_text.setReadOnly(True) - forecast_layout.addWidget(forecast_text) - layout.addLayout(forecast_layout) + tips_text.setPlainText(tips_info.strip()) + tips_text.setReadOnly(True) + tips_layout.addWidget(tips_text) + layout.addLayout(tips_layout) # 按钮 button_layout = QHBoxLayout() -- 2.34.1 From ba5101f59cf9541d9794eef971af7be7713bf6a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E5=AD=90=E6=98=82?= <929110464@qq.com> Date: Sun, 26 Oct 2025 17:14:39 +0800 Subject: [PATCH 15/18] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E7=A8=8B=E5=BA=8F=E7=89=88=E6=9C=AC=E5=8F=B7=E4=B8=BA?= =?UTF-8?q?0.2.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 300d17c..b119247 100644 --- a/src/main.py +++ b/src/main.py @@ -85,7 +85,7 @@ def main(): # 设置应用程序属性 app.setApplicationName("MagicWord") - app.setApplicationVersion("2.0") + app.setApplicationVersion("0.2.2") app.setOrganizationName("MagicWord") # 设置窗口图标(如果存在) -- 2.34.1 From e1469048f4f6232603e96c360cc67274393eb6c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E5=AD=90=E6=98=82?= <929110464@qq.com> Date: Sun, 26 Oct 2025 18:09:45 +0800 Subject: [PATCH 16/18] =?UTF-8?q?chore(icons):=20=E6=9B=BF=E6=8D=A2?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=9B=BE=E6=A0=87=E6=96=87=E4=BB=B6=E5=B9=B6?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=91=BD=E5=90=8D=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除带有星号的文件名图标,添加正确命名的图标文件 --- .../{app_icon_128*128.png => app_icon_128X128.png} | Bin .../{app_icon_256*256.png => app_icon_256X256.png} | Bin .../{app_icon_32*32.png => app_icon_32X32.png} | Bin .../{app_icon_64*64.png => app_icon_64X64.png} | Bin 4 files changed, 0 insertions(+), 0 deletions(-) rename resources/icons/{app_icon_128*128.png => app_icon_128X128.png} (100%) rename resources/icons/{app_icon_256*256.png => app_icon_256X256.png} (100%) rename resources/icons/{app_icon_32*32.png => app_icon_32X32.png} (100%) rename resources/icons/{app_icon_64*64.png => app_icon_64X64.png} (100%) diff --git a/resources/icons/app_icon_128*128.png b/resources/icons/app_icon_128X128.png similarity index 100% rename from resources/icons/app_icon_128*128.png rename to resources/icons/app_icon_128X128.png diff --git a/resources/icons/app_icon_256*256.png b/resources/icons/app_icon_256X256.png similarity index 100% rename from resources/icons/app_icon_256*256.png rename to resources/icons/app_icon_256X256.png diff --git a/resources/icons/app_icon_32*32.png b/resources/icons/app_icon_32X32.png similarity index 100% rename from resources/icons/app_icon_32*32.png rename to resources/icons/app_icon_32X32.png diff --git a/resources/icons/app_icon_64*64.png b/resources/icons/app_icon_64X64.png similarity index 100% rename from resources/icons/app_icon_64*64.png rename to resources/icons/app_icon_64X64.png -- 2.34.1 From daeede9ffa05a75a1476c7d0c4a86e5d93252bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E5=AD=90=E6=98=82?= <929110464@qq.com> Date: Sun, 26 Oct 2025 21:11:37 +0800 Subject: [PATCH 17/18] =?UTF-8?q?build:=20=E6=B7=BB=E5=8A=A0=20MagicWord?= =?UTF-8?q?=20v0.3=20=E7=9A=84=E6=89=93=E5=8C=85=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 Python 打包脚本用于生成 MagicWord v0.3 的可执行文件 包含清理目录、构建可执行文件和检查输出等功能 --- build_v0.3.py | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 build_v0.3.py diff --git a/build_v0.3.py b/build_v0.3.py new file mode 100644 index 0000000..147f732 --- /dev/null +++ b/build_v0.3.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +""" +MagicWord v0.3 打包脚本 +版本号:0.3 +输出目录:dist/ +图标:resources/icons/app_icon_256X256.png +""" + +import os +import sys +import subprocess +import shutil +from pathlib import Path + +def clean_dist(): + """清理dist目录""" + dist_dir = Path("dist") + if dist_dir.exists(): + shutil.rmtree(dist_dir) + dist_dir.mkdir(exist_ok=True) + +def build_executable(): + """使用PyInstaller构建可执行文件""" + print("开始构建 MagicWord v0.3...") + + # 清理之前的构建 + clean_dist() + + # PyInstaller 参数 + pyinstaller_args = [ + "-m", "PyInstaller", + "--onefile", # 单文件模式 + "--windowed", # 窗口模式(无控制台) + f"--name=MagicWord_v0.3", # 可执行文件名 + f"--icon=resources/icons/app_icon_256X256.png", # 图标文件 + "--add-data=resources:resources", # 添加资源文件 + "--add-data=src:src", # 添加源代码 + "--clean", # 清理临时文件 + "--noconfirm", # 不确认覆盖 + "src/main.py" # 主程序入口 + ] + + # 执行打包命令 + cmd = [sys.executable] + pyinstaller_args + print(f"执行命令: {' '.join(cmd)}") + + try: + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"打包失败: {result.stderr}") + return False + else: + print("打包成功!") + return True + except Exception as e: + print(f"执行打包时出错: {e}") + return False + +def check_output(): + """检查输出文件""" + dist_dir = Path("dist") + exe_files = list(dist_dir.glob("MagicWord_v0.3*")) + + if exe_files: + print(f"生成的文件:") + for exe in exe_files: + size = exe.stat().st_size / (1024 * 1024) # MB + print(f" {exe.name} - {size:.1f} MB") + return True + else: + print("未找到生成的可执行文件") + return False + +def main(): + """主函数""" + print("=" * 50) + print("MagicWord v0.3 打包工具") + print("=" * 50) + + # 检查Python环境 + print(f"Python版本: {sys.version}") + print(f"Python路径: {sys.executable}") + + # 检查文件是否存在 + required_files = [ + "src/main.py", + "resources/icons/app_icon_256X256.png" + ] + + missing_files = [] + for file in required_files: + if not Path(file).exists(): + missing_files.append(file) + + if missing_files: + print(f"缺少必需文件: {missing_files}") + return + + # 构建可执行文件 + if build_executable(): + # 检查输出 + if check_output(): + print("\n✅ 打包完成!可执行文件位于 dist/ 目录") + else: + print("\n❌ 打包可能存在问题") + else: + print("\n❌ 打包失败") + +if __name__ == "__main__": + main() \ No newline at end of file -- 2.34.1 From 220a8d46e9c854561a3bbf1c304e7213e08307bb Mon Sep 17 00:00:00 2001 From: Horse861 <929110464@qq.com> Date: Mon, 27 Oct 2025 10:05:33 +0800 Subject: [PATCH 18/18] =?UTF-8?q?build:=20=E6=9B=B4=E6=96=B0=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E8=87=B30.3.0=E5=B9=B6=E8=B0=83=E6=95=B4=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=9E=84=E5=BB=BA=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新项目版本号至0.3.0,同步修改构建脚本中的版本信息 添加新版发布包的输出目录到.gitignore --- .gitignore | 1 + build_release.py | 14 +++++++------- setup.py | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index f4d94b7..4ec21cb 100644 --- a/.gitignore +++ b/.gitignore @@ -199,6 +199,7 @@ temp/ # Project specific dist_package/ +dist_package_v0.3/ *.zip *.pyc *.pyo diff --git a/build_release.py b/build_release.py index 89054d8..08a0160 100644 --- a/build_release.py +++ b/build_release.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -MagicWord 0.2.1 版本发布脚本 +MagicWord 0.3.0 版本发布脚本 用于构建和打包应用程序 """ @@ -72,7 +72,7 @@ def build_executable(): pyinstaller_cmd = [ "pyinstaller", "--name", "MagicWord", - "--version", "0.2.1", + "--version", "0.3.0", "--distpath", "dist", "--workpath", "build", "--specpath", ".", @@ -161,7 +161,7 @@ def create_package(): # 创建运行脚本 if platform.system() == "Windows": run_script = """@echo off -echo MagicWord 0.2.1 启动中... +echo MagicWord 0.3.0 启动中... cd /d "%~dp0" start MagicWord.exe """ @@ -169,7 +169,7 @@ start MagicWord.exe f.write(run_script) else: run_script = """#!/bin/bash -echo "MagicWord 0.2.1 启动中..." +echo "MagicWord 0.3.0 启动中..." cd "$(dirname "$0")" ./MagicWord & """ @@ -178,7 +178,7 @@ cd "$(dirname "$0")" os.chmod(os.path.join(release_dir, "run.sh"), 0o755) # 创建发布说明 - release_info = f"""MagicWord 0.2.1 发布包 + release_info = f"""MagicWord 0.3.0 发布包 构建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} 平台: {platform.system()} {platform.machine()} Python版本: {platform.python_version()} @@ -203,7 +203,7 @@ Python版本: {platform.python_version()} f.write(release_info) # 创建ZIP包 - zip_name = f"MagicWord_v0.2.1_{platform.system()}_{platform.machine()}.zip" + zip_name = f"MagicWord_v0.3.0_{platform.system()}_{platform.machine()}.zip" with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zipf: for root, dirs, files in os.walk(release_dir): for file in files: @@ -217,7 +217,7 @@ Python版本: {platform.python_version()} def main(): """主函数""" print("=" * 60) - print("MagicWord 0.2.1 版本发布构建脚本") + print("MagicWord 0.3.0 版本发布构建脚本") print("=" * 60) # 检查Python版本 diff --git a/setup.py b/setup.py index c8cf589..f509b1c 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="MagicWord", - version="0.2.1", + version="0.3.0", description="隐私学习软件 - 一款通过打字练习来学习文档内容的工具", author="MagicWord Team", packages=find_packages(where="src"), -- 2.34.1