546 lines
21 KiB
Python
546 lines
21 KiB
Python
"""
|
||
主程序文件
|
||
|
||
重构后的主程序,使用模块化的设计,提供清晰的入口点。
|
||
"""
|
||
|
||
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() |