TxT2Docx/main.py

368 lines
13 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相关导入
import PySimpleGUI as sg
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()
# 设置GUI主题
sg.theme('BlueMono')
# 加载配置
config.load_from_file(CONFIG_FILE_PATH)
def run(self):
"""运行应用程序"""
try:
self._show_main_window()
except Exception as e:
sg.popup_error(f"应用程序运行出错: {str(e)}")
finally:
# 保存配置
config.save_to_file(CONFIG_FILE_PATH)
def _show_main_window(self):
"""显示主界面"""
layout = self._create_main_layout()
window = sg.Window('批量Markdown TXT转DOCX工具', layout, resizable=True)
try:
self._handle_main_window_events(window)
finally:
window.close()
def _create_main_layout(self):
"""创建主界面布局"""
return [
[sg.Text('批量Markdown TXT转DOCX工具', font=('bold', 16))],
[sg.Text('按文件名匹配TXT文件和图片文件夹支持完整Markdown格式', text_color='gray')],
[sg.HSeparator()],
[sg.Text('TXT文件文件夹:', size=(15, 1)),
sg.InputText(key='txt_folder', enable_events=True, default_text=config.last_txt_folder),
sg.FolderBrowse('浏览')],
[sg.Text('图片根文件夹:', size=(15, 1)),
sg.InputText(key='images_root', enable_events=True, default_text=config.last_images_root),
sg.FolderBrowse('浏览')],
[sg.Text('输出根文件夹:', size=(15, 1)),
sg.InputText(key='output_root', enable_events=True, default_text=config.last_output_root),
sg.FolderBrowse('浏览'),
sg.Text('(当选择"输出到指定文件夹"时有效)', text_color='gray')],
[sg.Button('扫描文件', size=(12, 1)),
sg.Button('编辑匹配', size=(12, 1), disabled=True),
sg.Button('转换设置', size=(12, 1)),
sg.Button('帮助', size=(8, 1))],
[sg.HSeparator()],
[sg.Text('匹配结果预览:', font=('bold', 10))],
[sg.Table(
values=[],
headings=['TXT文件名', '相对路径', '匹配的图片文件夹'],
key='-PREVIEW_TABLE-',
auto_size_columns=False,
col_widths=[20, 30, 30],
justification='left',
size=(None, 10)
)],
[sg.ProgressBar(100, orientation='h', size=(80, 20), key='progress_bar', visible=False)],
[sg.Text('状态: 就绪', key='status_text', size=(80, 1))],
[sg.Button('开始批量转换', size=(15, 1), disabled=True), sg.Button('退出')]
]
def _handle_main_window_events(self, window):
"""处理主窗口事件"""
progress_bar = window['progress_bar']
status_text = window['status_text']
preview_table = window['-PREVIEW_TABLE-']
output_root_input = window['output_root']
# 初始化窗口,避免更新元素时的警告
window.read(timeout=1)
# 初始化输出根文件夹输入框状态
self._update_output_root_state(output_root_input)
while True:
event, values = window.read()
if event in (sg.WIN_CLOSED, '退出'):
self._save_current_settings(values)
break
elif event == '转换设置':
self._show_config_window()
self._update_output_root_state(output_root_input)
elif event == '帮助':
self._show_help_window()
elif event == '扫描文件':
self._handle_scan_files(values, window, status_text, preview_table)
elif event == '编辑匹配':
self._handle_edit_matching(values, preview_table)
elif event == '开始批量转换':
self._handle_batch_conversion(values, window, progress_bar, status_text)
elif event in ('txt_folder', 'images_root') and values[event] and not values.get('output_root', ''):
# 自动设置输出路径
default_output = values['txt_folder'] if values['txt_folder'] else values['images_root']
window['output_root'].update(default_output)
def _update_output_root_state(self, output_root_input):
"""根据配置更新输出根文件夹输入框的状态"""
if config.output_location == "custom":
output_root_input.update(disabled=False)
try:
output_root_input.Widget.configure(foreground='black')
except:
pass
else:
output_root_input.update(disabled=True)
try:
output_root_input.Widget.configure(foreground='gray')
except:
pass
def _save_current_settings(self, values):
"""保存当前设置"""
if values:
config.last_txt_folder = values.get('txt_folder', '')
config.last_images_root = values.get('images_root', '')
config.last_output_root = values.get('output_root', '')
config.save_to_file(CONFIG_FILE_PATH)
def _handle_scan_files(self, values, window, status_text, preview_table):
"""处理扫描文件事件"""
txt_folder = values['txt_folder']
images_root = values['images_root']
if not txt_folder:
sg.popup_error('请选择TXT文件所在的文件夹')
return
if not images_root:
sg.popup_error('请选择图片根文件夹')
return
# 保存路径
config.last_txt_folder = txt_folder
config.last_images_root = images_root
if values['output_root']:
config.last_output_root = values['output_root']
config.save_to_file(CONFIG_FILE_PATH)
try:
status_text.update('正在扫描TXT文件...')
window.refresh()
txt_files = self.file_handler.scan_txt_files(txt_folder)
status_text.update('正在匹配图片文件夹...')
window.refresh()
self.matched_pairs = self.file_handler.find_matching_image_folders(txt_files, images_root)
# 更新预览表格
table_data = []
for pair in self.matched_pairs:
img_folder = pair['image_folder']['relative_path'] if pair['image_folder'] else "无匹配"
table_data.append([
pair['txt']['name'],
pair['txt']['relative_path'],
img_folder
])
preview_table.update(values=table_data)
status_text.update(f'扫描完成: 找到 {len(self.matched_pairs)} 个TXT文件')
# 启用相关按钮
window['编辑匹配'].update(disabled=False)
window['开始批量转换'].update(disabled=False)
except Exception as e:
sg.popup_error(f'扫描失败: {str(e)}')
status_text.update('状态: 扫描失败')
def _handle_edit_matching(self, values, preview_table):
"""处理编辑匹配事件"""
images_root = values['images_root']
if not images_root:
sg.popup_error('请选择图片根文件夹')
return
if not self.matched_pairs:
sg.popup_error('请先扫描文件')
return
# 显示匹配编辑窗口
self.matched_pairs = self._show_matching_editor(self.matched_pairs, images_root)
# 更新预览表格
table_data = []
for pair in self.matched_pairs:
img_folder = pair['image_folder']['relative_path'] if pair['image_folder'] else "无匹配"
table_data.append([
pair['txt']['name'],
pair['txt']['relative_path'],
img_folder
])
preview_table.update(values=table_data)
def _handle_batch_conversion(self, values, window, progress_bar, status_text):
"""处理批量转换事件"""
if not self.matched_pairs:
sg.popup_error('请先扫描文件')
return
if config.output_location == "custom" and not values['output_root']:
sg.popup_error('请选择输出根文件夹(在"转换设置"中选择了"输出到指定文件夹"')
return
try:
progress_bar.update(0, visible=True)
status_text.update('开始批量转换...')
window.refresh()
def update_batch_progress(progress, text):
progress_bar.update(progress)
status_text.update(f'状态: {text}')
window.refresh()
results = self.batch_processor.process_batch(
self.matched_pairs,
values['output_root'],
update_batch_progress
)
self._show_results_window(results)
status_text.update('状态: 批量转换完成')
except Exception as e:
sg.popup_error(f'批量处理失败: {str(e)}')
status_text.update('状态: 批量转换失败')
finally:
progress_bar.update(0, visible=False)
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格式的错别字映射文件
- 常见映射事等
"""
sg.popup_scrolled('使用帮助', help_text, size=(70, 25))
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}")
sg.popup_error(f"程序运行出错: {e}")
finally:
print("程序已退出")
if __name__ == '__main__':
main()