TxT2Docx/main.py

546 lines
21 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
主程序文件
重构后的主程序,使用模块化的设计,提供清晰的入口点。
"""
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相关导入
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from tkinter.scrolledtext import ScrolledText
import threading
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()
# 初始化主窗口
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
# 加载配置
config.load_from_file(CONFIG_FILE_PATH)
# 界面变量
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()
def run(self):
"""运行应用程序"""
try:
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
self.root.mainloop()
except Exception as e:
messagebox.showerror("错误", f"应用程序运行出错: {str(e)}")
finally:
# 保存配置
config.save_to_file(CONFIG_FILE_PATH)
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)
# 标题
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)
# 状态栏和按钮区域
self.bottom_frame = ttk.Frame(main_frame)
self.bottom_frame.pack(fill='x', pady=(0, 0))
# 状态文本
self.status_text = tk.StringVar(value='状态: 就绪')
status_label = ttk.Label(self.bottom_frame, textvariable=self.status_text)
status_label.pack(side='left')
# 右侧按钮
right_buttons = ttk.Frame(self.bottom_frame)
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):
"""根据配置更新输出根文件夹输入框的状态"""
if config.output_location == "custom":
self.output_entry.configure(state='normal')
else:
self.output_entry.configure(state='disabled')
def _save_current_settings(self):
"""保存当前设置"""
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)
def _scan_files(self):
"""扫描文件"""
txt_folder = self.txt_folder_var.get()
images_root = self.images_root_var.get()
if not txt_folder:
messagebox.showerror('错误', '请选择TXT文件所在的文件夹')
return
if not images_root:
messagebox.showerror('错误', '请选择图片根文件夹')
return
# 保存路径
config.last_txt_folder = txt_folder
config.last_images_root = images_root
if self.output_root_var.get():
config.last_output_root = self.output_root_var.get()
config.save_to_file(CONFIG_FILE_PATH)
try:
self.status_text.set('正在扫描TXT文件...')
self.root.update()
txt_files = self.file_handler.scan_txt_files(txt_folder)
self.status_text.set('正在匹配图片文件夹...')
self.root.update()
self.matched_pairs = self.file_handler.find_matching_image_folders(txt_files, images_root)
# 更新预览表格
for item in self.tree.get_children():
self.tree.delete(item)
for pair in self.matched_pairs:
img_folder = pair['image_folder']['relative_path'] if pair['image_folder'] else "无匹配"
self.tree.insert('', 'end', values=(
pair['txt']['name'],
pair['txt']['relative_path'],
img_folder
))
self.status_text.set(f'扫描完成: 找到 {len(self.matched_pairs)} 个TXT文件')
# 启用相关按钮
self.edit_btn.configure(state='normal')
self.convert_btn.configure(state='normal')
except Exception as e:
messagebox.showerror('错误', f'扫描失败: {str(e)}')
self.status_text.set('状态: 扫描失败')
def _edit_matching(self):
"""编辑匹配"""
images_root = self.images_root_var.get()
if not images_root:
messagebox.showerror('错误', '请选择图片根文件夹')
return
if not self.matched_pairs:
messagebox.showerror('错误', '请先扫描文件')
return
# 显示匹配编辑窗口
self.matched_pairs = self._show_matching_editor(self.matched_pairs, images_root)
# 更新预览表格
for item in self.tree.get_children():
self.tree.delete(item)
for pair in self.matched_pairs:
img_folder = pair['image_folder']['relative_path'] if pair['image_folder'] else "无匹配"
self.tree.insert('', 'end', values=(
pair['txt']['name'],
pair['txt']['relative_path'],
img_folder
))
def _start_conversion(self):
"""开始批量转换"""
if not self.matched_pairs:
messagebox.showerror('错误', '请先扫描文件')
return
if config.output_location == "custom" and not self.output_root_var.get():
messagebox.showerror('错误', '请选择输出根文件夹(在"转换设置"中选择了"输出到指定文件夹"')
return
# 禁用按钮,显示进度条
self.convert_btn.configure(state='disabled')
# 将进度条插入到底部框架之前
self.progress_bar.pack(fill='x', padx=10, pady=5, before=self.bottom_frame)
# 在线程中执行转换,避免界面卡死
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()
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)
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格式的错别字映射文件
- 常见映射:的↔地↔得、在↔再、是↔事等
"""
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():
"""主函数"""
# print("正在启动批量Markdown TXT转DOCX工具...")
try:
app = TxtToDocxApp()
app.run()
except KeyboardInterrupt:
print("\n用户中断程序运行")
except Exception as e:
print(f"程序运行出错: {e}")
messagebox.showerror("错误", f"程序运行出错: {e}")
finally:
print("程序已退出")
if __name__ == '__main__':
main()