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/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 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/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 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"), 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.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") # 设置窗口图标(如果存在) 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/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 ed1cf8f..aa4e950 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() @@ -155,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} 的天气数据") @@ -202,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) @@ -282,10 +278,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 +583,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 +743,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 +796,7 @@ class WordStyleMainWindow(QMainWindow): self.text_edit.setTextCursor(cursor) # 在文本中插入图片(如果有的话) + # 注意:必须在更新文本后调用,且要处理图片插入对文本长度的影响 self.insert_images_in_text() # 重新连接文本变化信号 @@ -814,6 +820,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() @@ -913,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): """手动刷新天气信息""" @@ -948,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: @@ -974,6 +992,7 @@ class WordStyleMainWindow(QMainWindow): return weather_data = self.current_weather_data + print(f"详细天气对话框 - 天气数据: {weather_data}") # 创建对话框 dialog = QDialog(self) @@ -990,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()) @@ -1003,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() @@ -1154,59 +1189,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.status_bar.showMessage(f"已创建空白副本,可以自由打字", 5000) + # 存储完整内容但不立即显示 + self.imported_content = content + self.displayed_chars = 0 + + # 创建空白副本 - 清空文本编辑器 + self.text_edit.clear() + + # 根据当前模式进行处理 + if self.view_mode == "learning": + # 学习模式:设置学习内容到打字逻辑 + if self.typing_logic: + self.typing_logic.reset(content) # 重置打字状态并设置新内容 + + self.status_bar.showMessage(f"已打开学习文件: {os.path.basename(file_path)},开始打字逐步显示学习内容!", 5000) + else: + # 打字模式:直接显示完整内容 + self.text_edit.setPlainText(content) + self.status_bar.showMessage(f"已打开: {os.path.basename(file_path)}", 5000) + + # 清除文件加载标志 + self.is_loading_file = False + + # 设置当前文件路径 + self.current_file_path = file_path + self.is_modified = False + self.update_window_title() + + # 更新字数统计 + if hasattr(self.status_bar, 'words_label'): + self.status_bar.words_label.setText(f"总字数: {len(content)}") + + except Exception as fallback_e: + # 确保在异常情况下也清除标志 + self.is_loading_file = False + QMessageBox.critical(self, "错误", f"无法打开文件:\n{str(e)}\n\n回退方法也失败:\n{str(fallback_e)}") # 清除文件加载标志 self.is_loading_file = False @@ -1351,13 +1508,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 +1528,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 +2028,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 +2041,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 +2140,9 @@ class WordStyleMainWindow(QMainWindow): def closeEvent(self, event): """关闭事件处理""" + # 清理临时文件 + self.cleanup_temp_files() + if self.is_modified: reply = QMessageBox.question( self, "确认退出", @@ -1965,6 +2160,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 +2216,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 +2254,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)