TxT2Docx/main.py

546 lines
21 KiB
Python
Raw Normal View History

2025-09-21 19:01:40 +08:00
"""
主程序文件
重构后的主程序使用模块化的设计提供清晰的入口点
"""
import sys
import os
# 添加当前目录到Python路径确保能导入模块
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, current_dir)
try:
# 导入所有必要的模块
from config import config, CONFIG_FILE_PATH
from file_handler import FileHandler
from text_processor import TextProcessor
from markdown_parser import MarkdownParser
from image_processor import ImageProcessor
from error_chars import ErrorCharProcessor
from docx_generator import DocxGenerator
from batch_processor import BatchProcessor
# GUI相关导入
2025-09-21 20:40:36 +08:00
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from tkinter.scrolledtext import ScrolledText
import threading
2025-09-21 19:01:40 +08:00
except ImportError as e:
print(f"导入模块失败: {e}")
print("请确保所有依赖包已正确安装")
sys.exit(1)
class TxtToDocxApp:
"""TXT转DOCX应用程序主类"""
def __init__(self):
"""初始化应用程序"""
self.matched_pairs = []
self.file_handler = FileHandler()
self.batch_processor = BatchProcessor()
2025-09-21 20:40:36 +08:00
# 初始化主窗口
self.root = tk.Tk()
self.root.title('批量Markdown TXT转DOCX工具')
self.root.geometry('900x700')
self.root.minsize(800, 600)
# 设置窗口图标和样式
try:
# 设置主题
self.root.configure(bg='#f0f0f0')
# 设置窗口居中
self.root.geometry('+{}+{}'.format(
self.root.winfo_screenwidth() // 2 - 450,
self.root.winfo_screenheight() // 2 - 350
))
except:
pass
2025-09-21 19:01:40 +08:00
# 加载配置
config.load_from_file(CONFIG_FILE_PATH)
2025-09-21 20:40:36 +08:00
# 界面变量
self.txt_folder_var = tk.StringVar(value=config.last_txt_folder)
self.images_root_var = tk.StringVar(value=config.last_images_root)
self.output_root_var = tk.StringVar(value=config.last_output_root)
# 创建界面
self._create_main_interface()
2025-09-21 19:01:40 +08:00
def run(self):
"""运行应用程序"""
try:
2025-09-21 20:40:36 +08:00
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
self.root.mainloop()
2025-09-21 19:01:40 +08:00
except Exception as e:
2025-09-21 20:40:36 +08:00
messagebox.showerror("错误", f"应用程序运行出错: {str(e)}")
2025-09-21 19:01:40 +08:00
finally:
# 保存配置
config.save_to_file(CONFIG_FILE_PATH)
2025-09-21 20:40:36 +08:00
def _on_closing(self):
"""窗口关闭处理"""
self._save_current_settings()
self.root.destroy()
def _create_main_interface(self):
"""创建主界面"""
# 主容器框架
main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack(fill='both', expand=True)
2025-09-21 19:01:40 +08:00
2025-09-21 20:40:36 +08:00
# 标题
title_frame = ttk.Frame(main_frame)
title_frame.pack(fill='x', pady=(0, 10))
title_label = ttk.Label(title_frame, text='批量Markdown TXT转DOCX工具',
font=('', 16, 'bold'))
title_label.pack()
subtitle_label = ttk.Label(title_frame,
text='按文件名匹配TXT文件和图片文件夹支持完整Markdown格式',
foreground='gray')
subtitle_label.pack()
# 分隔线
ttk.Separator(main_frame, orient='horizontal').pack(fill='x', pady=10)
# 路径选择区域
path_frame = ttk.LabelFrame(main_frame, text='文件路径设置', padding="10")
path_frame.pack(fill='x', pady=(0, 10))
# TXT文件夹
txt_frame = ttk.Frame(path_frame)
txt_frame.pack(fill='x', pady=5)
ttk.Label(txt_frame, text='TXT文件文件夹:', width=15).pack(side='left')
txt_entry = ttk.Entry(txt_frame, textvariable=self.txt_folder_var, width=50)
txt_entry.pack(side='left', fill='x', expand=True, padx=(0, 5))
txt_entry.bind('<KeyRelease>', self._on_path_change)
ttk.Button(txt_frame, text='浏览', command=self._browse_txt_folder, width=8).pack(side='right')
# 图片根文件夹
img_frame = ttk.Frame(path_frame)
img_frame.pack(fill='x', pady=5)
ttk.Label(img_frame, text='图片根文件夹:', width=15).pack(side='left')
img_entry = ttk.Entry(img_frame, textvariable=self.images_root_var, width=50)
img_entry.pack(side='left', fill='x', expand=True, padx=(0, 5))
img_entry.bind('<KeyRelease>', self._on_path_change)
ttk.Button(img_frame, text='浏览', command=self._browse_images_root, width=8).pack(side='right')
# 输出根文件夹
output_frame = ttk.Frame(path_frame)
output_frame.pack(fill='x', pady=5)
ttk.Label(output_frame, text='输出根文件夹:', width=15).pack(side='left')
self.output_entry = ttk.Entry(output_frame, textvariable=self.output_root_var, width=50)
self.output_entry.pack(side='left', fill='x', expand=True, padx=(0, 5))
ttk.Button(output_frame, text='浏览', command=self._browse_output_root, width=8).pack(side='right')
# 提示文本
hint_label = ttk.Label(path_frame, text='提示:输出根文件夹在选择"输出到指定文件夹"时有效',
foreground='gray', font=('', 8))
hint_label.pack(anchor='w', pady=(5, 0))
# 按钮区域
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill='x', pady=10)
self.scan_btn = ttk.Button(button_frame, text='扫描文件', command=self._scan_files, width=12)
self.scan_btn.pack(side='left', padx=(0, 10))
self.edit_btn = ttk.Button(button_frame, text='编辑匹配', command=self._edit_matching,
state='disabled', width=12)
self.edit_btn.pack(side='left', padx=(0, 10))
ttk.Button(button_frame, text='转换设置', command=self._show_config, width=12).pack(side='left', padx=(0, 10))
ttk.Button(button_frame, text='帮助', command=self._show_help, width=8).pack(side='left')
# 预览区域
preview_frame = ttk.LabelFrame(main_frame, text='匹配结果预览', padding="10")
preview_frame.pack(fill='both', expand=True, pady=(0, 10))
# 表格区域
table_frame = ttk.Frame(preview_frame)
table_frame.pack(fill='both', expand=True)
# 创建Treeview表格
columns = ('txt_name', 'relative_path', 'image_folder')
self.tree = ttk.Treeview(table_frame, columns=columns, show='headings', height=12)
self.tree.heading('txt_name', text='TXT文件名')
self.tree.heading('relative_path', text='相对路径')
self.tree.heading('image_folder', text='匹配的图片文件夹')
self.tree.column('txt_name', width=200)
self.tree.column('relative_path', width=300)
self.tree.column('image_folder', width=300)
# 滚动条
v_scrollbar = ttk.Scrollbar(table_frame, orient='vertical', command=self.tree.yview)
h_scrollbar = ttk.Scrollbar(table_frame, orient='horizontal', command=self.tree.xview)
self.tree.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set)
# 布局表格和滚动条
self.tree.grid(row=0, column=0, sticky='nsew')
v_scrollbar.grid(row=0, column=1, sticky='ns')
h_scrollbar.grid(row=1, column=0, sticky='ew')
table_frame.grid_rowconfigure(0, weight=1)
table_frame.grid_columnconfigure(0, weight=1)
# 进度条(初始隐藏)
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var, maximum=100)
# 状态栏和按钮区域
2025-09-22 21:10:29 +08:00
self.bottom_frame = ttk.Frame(main_frame)
self.bottom_frame.pack(fill='x', pady=(0, 0))
2025-09-21 20:40:36 +08:00
# 状态文本
self.status_text = tk.StringVar(value='状态: 就绪')
2025-09-22 21:10:29 +08:00
status_label = ttk.Label(self.bottom_frame, textvariable=self.status_text)
2025-09-21 20:40:36 +08:00
status_label.pack(side='left')
# 右侧按钮
2025-09-22 21:10:29 +08:00
right_buttons = ttk.Frame(self.bottom_frame)
2025-09-21 20:40:36 +08:00
right_buttons.pack(side='right')
self.convert_btn = ttk.Button(right_buttons, text='开始批量转换',
command=self._start_conversion, state='disabled', width=15)
self.convert_btn.pack(side='left', padx=(0, 10))
ttk.Button(right_buttons, text='退出', command=self._on_closing, width=8).pack(side='left')
# 初始化输出路径状态
self._update_output_root_state()
def _browse_txt_folder(self):
"""浏览TXT文件夹"""
folder = filedialog.askdirectory(title='选择TXT文件所在的文件夹')
if folder:
self.txt_folder_var.set(folder)
self._on_path_change()
def _browse_images_root(self):
"""浏览图片根文件夹"""
folder = filedialog.askdirectory(title='选择图片根文件夹')
if folder:
self.images_root_var.set(folder)
self._on_path_change()
def _browse_output_root(self):
"""浏览输出根文件夹"""
folder = filedialog.askdirectory(title='选择输出根文件夹')
if folder:
self.output_root_var.set(folder)
def _on_path_change(self, event=None):
"""路径变化时的处理"""
# 自动设置输出路径
if not self.output_root_var.get():
if self.txt_folder_var.get():
self.output_root_var.set(self.txt_folder_var.get())
elif self.images_root_var.get():
self.output_root_var.set(self.images_root_var.get())
def _update_output_root_state(self):
2025-09-21 19:01:40 +08:00
"""根据配置更新输出根文件夹输入框的状态"""
if config.output_location == "custom":
2025-09-21 20:40:36 +08:00
self.output_entry.configure(state='normal')
2025-09-21 19:01:40 +08:00
else:
2025-09-21 20:40:36 +08:00
self.output_entry.configure(state='disabled')
2025-09-21 19:01:40 +08:00
2025-09-21 20:40:36 +08:00
def _save_current_settings(self):
2025-09-21 19:01:40 +08:00
"""保存当前设置"""
2025-09-21 20:40:36 +08:00
config.last_txt_folder = self.txt_folder_var.get()
config.last_images_root = self.images_root_var.get()
config.last_output_root = self.output_root_var.get()
config.save_to_file(CONFIG_FILE_PATH)
2025-09-21 19:01:40 +08:00
2025-09-21 20:40:36 +08:00
def _scan_files(self):
"""扫描文件"""
txt_folder = self.txt_folder_var.get()
images_root = self.images_root_var.get()
2025-09-21 19:01:40 +08:00
if not txt_folder:
2025-09-21 20:40:36 +08:00
messagebox.showerror('错误', '请选择TXT文件所在的文件夹')
2025-09-21 19:01:40 +08:00
return
if not images_root:
2025-09-21 20:40:36 +08:00
messagebox.showerror('错误', '请选择图片根文件夹')
2025-09-21 19:01:40 +08:00
return
# 保存路径
config.last_txt_folder = txt_folder
config.last_images_root = images_root
2025-09-21 20:40:36 +08:00
if self.output_root_var.get():
config.last_output_root = self.output_root_var.get()
2025-09-21 19:01:40 +08:00
config.save_to_file(CONFIG_FILE_PATH)
try:
2025-09-21 20:40:36 +08:00
self.status_text.set('正在扫描TXT文件...')
self.root.update()
2025-09-21 19:01:40 +08:00
txt_files = self.file_handler.scan_txt_files(txt_folder)
2025-09-21 20:40:36 +08:00
self.status_text.set('正在匹配图片文件夹...')
self.root.update()
2025-09-21 19:01:40 +08:00
self.matched_pairs = self.file_handler.find_matching_image_folders(txt_files, images_root)
# 更新预览表格
2025-09-21 20:40:36 +08:00
for item in self.tree.get_children():
self.tree.delete(item)
2025-09-21 19:01:40 +08:00
for pair in self.matched_pairs:
img_folder = pair['image_folder']['relative_path'] if pair['image_folder'] else "无匹配"
2025-09-21 20:40:36 +08:00
self.tree.insert('', 'end', values=(
2025-09-21 19:01:40 +08:00
pair['txt']['name'],
pair['txt']['relative_path'],
img_folder
2025-09-21 20:40:36 +08:00
))
2025-09-21 19:01:40 +08:00
2025-09-21 20:40:36 +08:00
self.status_text.set(f'扫描完成: 找到 {len(self.matched_pairs)} 个TXT文件')
2025-09-21 19:01:40 +08:00
# 启用相关按钮
2025-09-21 20:40:36 +08:00
self.edit_btn.configure(state='normal')
self.convert_btn.configure(state='normal')
2025-09-21 19:01:40 +08:00
except Exception as e:
2025-09-21 20:40:36 +08:00
messagebox.showerror('错误', f'扫描失败: {str(e)}')
self.status_text.set('状态: 扫描失败')
2025-09-21 19:01:40 +08:00
2025-09-21 20:40:36 +08:00
def _edit_matching(self):
"""编辑匹配"""
images_root = self.images_root_var.get()
2025-09-21 19:01:40 +08:00
if not images_root:
2025-09-21 20:40:36 +08:00
messagebox.showerror('错误', '请选择图片根文件夹')
2025-09-21 19:01:40 +08:00
return
if not self.matched_pairs:
2025-09-21 20:40:36 +08:00
messagebox.showerror('错误', '请先扫描文件')
2025-09-21 19:01:40 +08:00
return
# 显示匹配编辑窗口
self.matched_pairs = self._show_matching_editor(self.matched_pairs, images_root)
# 更新预览表格
2025-09-21 20:40:36 +08:00
for item in self.tree.get_children():
self.tree.delete(item)
2025-09-21 19:01:40 +08:00
for pair in self.matched_pairs:
img_folder = pair['image_folder']['relative_path'] if pair['image_folder'] else "无匹配"
2025-09-21 20:40:36 +08:00
self.tree.insert('', 'end', values=(
2025-09-21 19:01:40 +08:00
pair['txt']['name'],
pair['txt']['relative_path'],
img_folder
2025-09-21 20:40:36 +08:00
))
2025-09-21 19:01:40 +08:00
2025-09-21 20:40:36 +08:00
def _start_conversion(self):
"""开始批量转换"""
2025-09-21 19:01:40 +08:00
if not self.matched_pairs:
2025-09-21 20:40:36 +08:00
messagebox.showerror('错误', '请先扫描文件')
2025-09-21 19:01:40 +08:00
return
2025-09-21 20:40:36 +08:00
if config.output_location == "custom" and not self.output_root_var.get():
messagebox.showerror('错误', '请选择输出根文件夹(在"转换设置"中选择了"输出到指定文件夹"')
2025-09-21 19:01:40 +08:00
return
2025-09-21 20:40:36 +08:00
# 禁用按钮,显示进度条
self.convert_btn.configure(state='disabled')
2025-09-22 21:10:29 +08:00
# 将进度条插入到底部框架之前
self.progress_bar.pack(fill='x', padx=10, pady=5, before=self.bottom_frame)
2025-09-21 20:40:36 +08:00
# 在线程中执行转换,避免界面卡死
def run_conversion():
try:
self.status_text.set('开始批量转换...')
self.root.update()
def update_batch_progress(progress, text):
self.progress_var.set(progress)
self.status_text.set(f'状态: {text}')
self.root.update()
results = self.batch_processor.process_batch(
self.matched_pairs,
self.output_root_var.get(),
update_batch_progress
)
# 在主线程中显示结果
self.root.after(0, lambda: self._show_results_window(results))
self.root.after(0, lambda: self.status_text.set('状态: 批量转换完成'))
except Exception as e:
self.root.after(0, lambda: messagebox.showerror('错误', f'批量处理失败: {str(e)}'))
self.root.after(0, lambda: self.status_text.set('状态: 批量转换失败'))
finally:
self.root.after(0, lambda: self.progress_bar.pack_forget())
self.root.after(0, lambda: self.convert_btn.configure(state='normal'))
# 启动线程
thread = threading.Thread(target=run_conversion, daemon=True)
thread.start()
2025-09-21 19:01:40 +08:00
2025-09-21 20:40:36 +08:00
def _show_config(self):
"""显示配置窗口"""
from gui_config import show_config_window
show_config_window()
self._update_output_root_state()
def _show_help(self):
"""显示帮助窗口"""
help_text = """
批量Markdown TXT转DOCX工具使用说明:
1. 选择包含Markdown内容的TXT文件所在文件夹
2. 选择图片文件夹的根目录程序会自动查找子文件夹
3. 选择输出文件的保存根目录当选择"输出到指定文件夹"时有效
4. 点击"扫描文件"按钮程序会自动匹配TXT文件和图片文件夹
5. 查看匹配结果可点击"编辑匹配"调整匹配关系
6. 点击"开始批量转换"生成DOCX文件
支持的Markdown格式:
- 标题# ## ### #### ##### ######
- 粗体**文字** __文字__
- 斜体*文字* _文字_
- 行内代码`代码`
- 代码块```语言\n代码\n```
- 删除线~~文字~~
- 链接[链接文字](URL)
- 图片![图片描述](图片路径)
- 无序列表- * +
- 有序列表1. 2. 3.
- 引用> 引用内容
- 表格| 列1 | 列2 |
- 水平分隔线--- *** ___
文字处理功能:
- 转换文字顺序将文字内容进行特定转换处理
- 错别字处理可以按设定强度引入常见的错别字用于测试或特殊用途
- 标点符号替换将句号转换为逗号保留文末句号
输出路径选择:
- 输出到TXT文件所在文件夹: 每个DOCX文件会直接保存在对应TXT文件所在的文件夹中
- 输出到指定文件夹: 所有DOCX文件会直接保存在您指定的文件夹中
匹配规则:
- 完全匹配: TXT文件名不含扩展名与TXT文件夹名完全相同
- 前缀匹配: 图片文件夹名以前缀形式包含TXT文件名
- 包含匹配: 图片文件夹名中包含TXT文件名
转换规则:
- 每个小标题的第一段后会插入一张图片
- 先将Markdown格式转换为DOCX格式再处理文字内容
- 支持文字顺序调换错别字处理和标点符号替换功能
错别字处理说明:
- 错误强度控制替换比例0.0表示不替换1.0表示替换所有可能的字
- 错别字库可自定义JSON格式的错别字映射文件
- 常见映射事等
"""
# 创建帮助窗口
help_window = tk.Toplevel(self.root)
help_window.title('使用帮助')
help_window.geometry('600x500')
help_window.transient(self.root)
help_window.grab_set()
# 滚动文本区域
text_widget = ScrolledText(help_window, wrap=tk.WORD, padx=10, pady=10)
text_widget.pack(fill='both', expand=True, padx=10, pady=10)
text_widget.insert('1.0', help_text)
text_widget.configure(state='disabled')
# 关闭按钮
ttk.Button(help_window, text='关闭', command=help_window.destroy).pack(pady=10)
2025-09-21 19:01:40 +08:00
def _show_config_window(self):
"""显示配置窗口"""
from gui_config import show_config_window
show_config_window()
def _show_help_window(self):
"""显示帮助窗口"""
help_text = """
批量Markdown TXT转DOCX工具使用说明:
1. 选择包含Markdown内容的TXT文件所在文件夹
2. 选择图片文件夹的根目录程序会自动查找子文件夹
3. 选择输出文件的保存根目录当选择"输出到指定文件夹"时有效
4. 点击"扫描文件"按钮程序会自动匹配TXT文件和图片文件夹
5. 查看匹配结果可点击"编辑匹配"调整匹配关系
6. 点击"开始批量转换"生成DOCX文件
支持的Markdown格式:
- 标题# ## ### #### ##### ######
- 粗体**文字** __文字__
- 斜体*文字* _文字_
- 行内代码`代码`
- 代码块```语言\\n代码\\n```
- 删除线~~文字~~
- 链接[链接文字](URL)
- 图片![图片描述](图片路径)
- 无序列表- * +
- 有序列表1. 2. 3.
- 引用> 引用内容
- 表格| 列1 | 列2 |
- 水平分隔线--- *** ___
文字处理功能:
- 转换文字顺序将文字内容进行特定转换处理
- 错别字处理可以按设定强度引入常见的错别字用于测试或特殊用途
- 标点符号替换将句号转换为逗号保留文末句号
输出路径选择:
- 输出到TXT文件所在文件夹: 每个DOCX文件会直接保存在对应TXT文件所在的文件夹中
- 输出到指定文件夹: 所有DOCX文件会直接保存在您指定的文件夹中
匹配规则:
- 完全匹配: TXT文件名不含扩展名与图片文件夹名完全相同
- 前缀匹配: 图片文件夹名以前缀形式包含TXT文件名
- 包含匹配: 图片文件夹名中包含TXT文件名
转换规则:
- 每个小标题的第一段后会插入一张图片
- 先将Markdown格式转换为DOCX格式再处理文字内容
- 支持文字顺序调换错别字处理和标点符号替换功能
错别字处理说明:
- 错误强度控制替换比例0.0表示不替换1.0表示替换所有可能的字
- 错别字库可自定义JSON格式的错别字映射文件
- 常见映射事等
"""
2025-09-21 20:40:36 +08:00
2025-09-21 19:01:40 +08:00
def _show_matching_editor(self, matched_pairs, images_root):
"""显示匹配编辑窗口"""
from gui_matching_editor import show_matching_editor
return show_matching_editor(matched_pairs, images_root)
def _show_results_window(self, results):
"""显示结果窗口"""
from gui_results import show_results_window
show_results_window(results)
def main():
"""主函数"""
2025-09-21 20:40:36 +08:00
# print("正在启动批量Markdown TXT转DOCX工具...")
2025-09-21 19:01:40 +08:00
try:
app = TxtToDocxApp()
app.run()
except KeyboardInterrupt:
print("\n用户中断程序运行")
except Exception as e:
print(f"程序运行出错: {e}")
2025-09-21 20:40:36 +08:00
messagebox.showerror("错误", f"程序运行出错: {e}")
2025-09-21 19:01:40 +08:00
finally:
print("程序已退出")
if __name__ == '__main__':
main()