368 lines
13 KiB
Python
368 lines
13 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 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()
|