""" 主程序文件 重构后的主程序,使用模块化的设计,提供清晰的入口点。 """ 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('', 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('', 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) # 状态栏和按钮区域 bottom_frame = ttk.Frame(main_frame) bottom_frame.pack(fill='x', pady=(0, 0)) # 状态文本 self.status_text = tk.StringVar(value='状态: 就绪') status_label = ttk.Label(bottom_frame, textvariable=self.status_text) status_label.pack(side='left') # 右侧按钮 right_buttons = ttk.Frame(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.root.children[list(self.root.children.keys())[-2]]) # 在线程中执行转换,避免界面卡死 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()