0.3 #56

Merged
p9o3yklam merged 7 commits from main into maziang 4 months ago

1
.gitignore vendored

@ -199,6 +199,7 @@ temp/
# Project specific
dist_package/
dist_package_v0.3/
*.zip
*.pyc
*.pyo

@ -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版本

@ -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()

@ -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<ProgressData> getProgressByUser(int userID)
List<ProgressData> 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的数据隐私保护规范

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before

Width:  |  Height:  |  Size: 372 B

After

Width:  |  Height:  |  Size: 372 B

Before

Width:  |  Height:  |  Size: 763 B

After

Width:  |  Height:  |  Size: 763 B

@ -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"),

@ -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

@ -85,7 +85,7 @@ def main():
# 设置应用程序属性
app.setApplicationName("MagicWord")
app.setApplicationVersion("2.0")
app.setApplicationVersion("0.2.2")
app.setOrganizationName("MagicWord")
# 设置窗口图标(如果存在)

@ -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
)

@ -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:
"""
检查指定位置是否有图片

@ -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}")

@ -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("<b>当前天气:</b>"))
# 获取温度信息,支持嵌套结构
current_data = weather_data.get('current', {})
temp = current_data.get('temp', 'N/A')
if temp != 'N/A' and isinstance(temp, str):
temp = float(temp) if temp.replace('.', '').isdigit() else temp
# 从预报数据中获取最高和最低气温
temp_max = 'N/A'
temp_min = 'N/A'
if 'forecast' in weather_data and weather_data['forecast']:
forecast_data = weather_data['forecast'][0] # 今天的预报
if isinstance(forecast_data, dict):
temp_max = forecast_data.get('temp_max', 'N/A')
temp_min = forecast_data.get('temp_min', 'N/A')
current_info = f"""
温度: {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("<b>天气预报:</b>"))
# 生活提示信息(替换原来的天气预报)
life_tips = weather_data.get('life_tips', [])
print(f"详细天气对话框 - 生活提示: {life_tips}")
print(f"详细天气对话框 - 完整天气数据: {weather_data}")
if life_tips:
tips_layout = QVBoxLayout()
tips_layout.addWidget(QLabel("<b>生活提示:</b>"))
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)

Loading…
Cancel
Save