更换GUI框架

This commit is contained in:
taiyi 2025-09-21 20:40:36 +08:00
parent 45dab3d892
commit 93781d3ebf
6 changed files with 905 additions and 401 deletions

View File

@ -37,7 +37,7 @@ class ErrorCharProcessor:
if dir_name and not os.path.exists(dir_name):
os.makedirs(dir_name)
print(f"加载错别字库文件: {self.db_path}")
# print(f"加载错别字库文件: {self.db_path}")
# 检查文件是否存在,不存在则创建默认库
if not os.path.exists(self.db_path):

View File

@ -4,180 +4,328 @@ GUI配置窗口模块
提供配置设置的图形界面
"""
import PySimpleGUI as sg
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from config import config
def show_config_window():
"""显示配置窗口"""
# 创建标签页布局
tab_file_layout = [
[sg.Text('文件处理设置', font=('bold', 12))],
[sg.HSeparator()],
[sg.Text('TXT编码:', size=(12, 1)),
sg.Combo(['utf-8', 'gbk', 'utf-16'], default_value=config.txt_encoding, key='txt_encoding', size=(15, 1))],
[sg.Text('匹配模式:', size=(12, 1))],
[sg.Radio('完全匹配(文件名与文件夹名相同)', 'match', default=config.match_pattern == "exact",
key='match_exact')],
[sg.Radio('前缀匹配', 'match', default=config.match_pattern == "prefix", key='match_prefix')],
[sg.Radio('包含匹配', 'match', default=config.match_pattern == "contains", key='match_contains')],
[sg.HSeparator()],
[sg.Text('输出位置:', size=(12, 1))],
[sg.Radio('输出到TXT文件所在文件夹', 'output_loc', default=config.output_location == "txt_folder",
key='output_txt_folder')],
[sg.Radio('输出到指定文件夹', 'output_loc', default=config.output_location == "custom", key='output_custom')]
]
# 创建配置窗口
config_window = tk.Toplevel()
config_window.title('转换设置')
config_window.geometry('600x700')
config_window.transient()
config_window.grab_set()
# 创建笔记本组件
notebook = ttk.Notebook(config_window)
notebook.pack(fill='both', expand=True, padx=10, pady=10)
# 文件处理选项卡
file_frame = ttk.Frame(notebook)
notebook.add(file_frame, text='文件处理')
_create_file_tab(file_frame)
# 文字处理选项卡
text_frame = ttk.Frame(notebook)
notebook.add(text_frame, text='文字处理')
char_vars = _create_text_tab(text_frame)
# 图片处理选项卡
image_frame = ttk.Frame(notebook)
notebook.add(image_frame, text='图片处理')
_create_image_tab(image_frame)
# 文档格式选项卅
format_frame = ttk.Frame(notebook)
notebook.add(format_frame, text='文档格式')
_create_format_tab(format_frame)
# 底部按钮
button_frame = ttk.Frame(config_window)
button_frame.pack(fill='x', padx=10, pady=10)
ttk.Button(button_frame, text='确定', command=lambda: _save_config(config_window, char_vars)).pack(side='left', padx=5)
ttk.Button(button_frame, text='取消', command=config_window.destroy).pack(side='left', padx=5)
ttk.Button(button_frame, text='重置为默认', command=lambda: _reset_to_default(char_vars)).pack(side='left', padx=5)
return char_vars
tab_text_layout = [
[sg.Text('文字处理设置', font=('bold', 12))],
[sg.HSeparator()],
[sg.Checkbox('转换文字顺序', key='-REVERSE_TEXT-', default=config.reverse_text_order)],
[sg.Checkbox('替换标点符号(句号转逗号,保留结尾句号)', key='-REPLACE_PUNCTUATION-',
default=config.replace_punctuation)],
[sg.HSeparator()],
[sg.Text('错别字处理', font=('bold', 11), text_color='darkblue')],
[sg.Checkbox('启用错别字处理', key='-ENABLE_CHAR_ERRORS-', default=config.enable_char_errors,
enable_events=True)],
[sg.Text('错误强度:', size=(10, 1)),
sg.Slider(range=(0.0, 1.0), default_value=config.char_error_intensity, resolution=0.1,
orientation='h', size=(20, 15), key='char_error_intensity', disabled=not config.enable_char_errors)],
[sg.Text('错别字库路径:', size=(12, 1)),
sg.InputText(config.char_error_db_path, key='char_error_db_path', size=(30, 1),
disabled=not config.enable_char_errors),
sg.FileBrowse('浏览', file_types=(("JSON Files", "*.json"),), disabled=not config.enable_char_errors)],
[sg.HSeparator()],
[sg.Checkbox('添加免责声明', key='-ADD_DISCLAIMER-', default=config.add_disclaimer)]
]
tab_image_layout = [
[sg.Text('图片处理设置', font=('bold', 12))],
[sg.HSeparator()],
[sg.Text('图片排序方式:', size=(12, 1))],
[sg.Radio('按名称', 'sort', default=config.image_sort_by == "name", key='sort_name'),
sg.Radio('按修改时间', 'sort', default=config.image_sort_by == "time", key='sort_time')],
[sg.HSeparator()],
[sg.Text('图片尺寸调整:', size=(12, 1))],
[sg.Radio('不调整', 'resize', default=config.image_resize == "none", key='resize_none')],
[sg.Radio('按宽度:', 'resize', default=config.image_resize == "width", key='resize_width'),
sg.InputText(str(config.image_width), size=(8, 1), key='image_width'),
sg.Text('英寸')],
[sg.HSeparator()],
[sg.Text('图片对齐方式:', size=(12, 1))],
[sg.Radio('左对齐', 'align', default=config.image_alignment == "left", key='align_left'),
sg.Radio('居中', 'align', default=config.image_alignment == "center", key='align_center'),
sg.Radio('右对齐', 'align', default=config.image_alignment == "right", key='align_right')],
[sg.HSeparator()],
[sg.Text('图片不足时策略:', size=(12, 1))],
[sg.Radio('循环使用', 'strategy', default=config.image_strategy == "cycle", key='strategy_cycle')],
[sg.Radio('忽略多余标题', 'strategy', default=config.image_strategy == "truncate", key='strategy_truncate')],
[sg.Radio('重复最后一张', 'strategy', default=config.image_strategy == "repeat_last", key='strategy_repeat')]
]
def _create_file_tab(parent):
"""创建文件处理选项卡"""
# 标题
ttk.Label(parent, text='文件处理设置', font=('', 12, 'bold')).pack(anchor='w', padx=10, pady=(10, 5))
ttk.Separator(parent, orient='horizontal').pack(fill='x', padx=10, pady=5)
# TXT编码
encoding_frame = ttk.Frame(parent)
encoding_frame.pack(fill='x', padx=10, pady=5)
ttk.Label(encoding_frame, text='TXT编码:', width=12).pack(side='left')
encoding_var = tk.StringVar(value=config.txt_encoding)
encoding_combo = ttk.Combobox(encoding_frame, textvariable=encoding_var,
values=['utf-8', 'gbk', 'utf-16'], state='readonly', width=15)
encoding_combo.pack(side='left', padx=(0, 10))
encoding_combo.bind('<<ComboboxSelected>>', lambda e: setattr(config, 'txt_encoding', encoding_var.get()))
# 匹配模式
ttk.Label(parent, text='匹配模式:', font=('', 10, 'bold')).pack(anchor='w', padx=10, pady=(15, 5))
match_var = tk.StringVar(value=config.match_pattern)
ttk.Radiobutton(parent, text='完全匹配(文件名与文件夹名相同)',
variable=match_var, value='exact').pack(anchor='w', padx=20, pady=2)
ttk.Radiobutton(parent, text='前缀匹配',
variable=match_var, value='prefix').pack(anchor='w', padx=20, pady=2)
ttk.Radiobutton(parent, text='包含匹配',
variable=match_var, value='contains').pack(anchor='w', padx=20, pady=2)
match_var.trace('w', lambda *args: setattr(config, 'match_pattern', match_var.get()))
ttk.Separator(parent, orient='horizontal').pack(fill='x', padx=10, pady=15)
# 输出位置
ttk.Label(parent, text='输出位置:', font=('', 10, 'bold')).pack(anchor='w', padx=10, pady=(0, 5))
output_var = tk.StringVar(value=config.output_location)
ttk.Radiobutton(parent, text='输出到TXT文件所在文件夹',
variable=output_var, value='txt_folder').pack(anchor='w', padx=20, pady=2)
ttk.Radiobutton(parent, text='输出到指定文件夹',
variable=output_var, value='custom').pack(anchor='w', padx=20, pady=2)
output_var.trace('w', lambda *args: setattr(config, 'output_location', output_var.get()))
tab_format_layout = [
[sg.Text('文档格式设置', font=('bold', 12))],
[sg.HSeparator()],
[sg.Text('行间距:', size=(12, 1)),
sg.InputText(str(config.line_spacing), size=(8, 1), key='line_spacing')],
[sg.Text('最大标题层级:', size=(12, 1)),
sg.Combo([1, 2, 3, 4, 5, 6], default_value=config.title_levels, key='title_levels', size=(8, 1))]
]
layout = [
[sg.TabGroup([
[sg.Tab('文件处理', tab_file_layout, key='tab_file')],
[sg.Tab('文字处理', tab_text_layout, key='tab_text')],
[sg.Tab('图片处理', tab_image_layout, key='tab_image')],
[sg.Tab('文档格式', tab_format_layout, key='tab_format')]
])],
[sg.HSeparator()],
[sg.Button('确定', size=(10, 1)), sg.Button('取消', size=(10, 1)), sg.Button('重置为默认', size=(12, 1))]
]
def _create_text_tab(parent):
"""创建文字处理选项卡"""
# 标题
ttk.Label(parent, text='文字处理设置', font=('', 12, 'bold')).pack(anchor='w', padx=10, pady=(10, 5))
ttk.Separator(parent, orient='horizontal').pack(fill='x', padx=10, pady=5)
# 文字处理选项
reverse_var = tk.BooleanVar(value=config.reverse_text_order)
ttk.Checkbutton(parent, text='转换文字顺序', variable=reverse_var).pack(anchor='w', padx=10, pady=5)
reverse_var.trace('w', lambda *args: setattr(config, 'reverse_text_order', reverse_var.get()))
punctuation_var = tk.BooleanVar(value=config.replace_punctuation)
ttk.Checkbutton(parent, text='替换标点符号(句号转逗号,保留结尾句号)',
variable=punctuation_var).pack(anchor='w', padx=10, pady=5)
punctuation_var.trace('w', lambda *args: setattr(config, 'replace_punctuation', punctuation_var.get()))
ttk.Separator(parent, orient='horizontal').pack(fill='x', padx=10, pady=15)
# 错别字处理
ttk.Label(parent, text='错别字处理', font=('', 11, 'bold'), foreground='darkblue').pack(anchor='w', padx=10, pady=(0, 5))
enable_errors_var = tk.BooleanVar(value=config.enable_char_errors)
enable_checkbox = ttk.Checkbutton(parent, text='启用错别字处理',
variable=enable_errors_var)
enable_checkbox.pack(anchor='w', padx=10, pady=5)
# 错误强度
intensity_frame = ttk.Frame(parent)
intensity_frame.pack(fill='x', padx=10, pady=5)
ttk.Label(intensity_frame, text='错误强度:', width=10).pack(side='left')
intensity_var = tk.DoubleVar(value=config.char_error_intensity)
intensity_scale = ttk.Scale(intensity_frame, from_=0.0, to=1.0, variable=intensity_var,
orient='horizontal', length=200)
intensity_scale.pack(side='left', padx=(0, 10))
intensity_label = ttk.Label(intensity_frame, text=f'{config.char_error_intensity:.1f}')
intensity_label.pack(side='left')
def update_intensity_label(*args):
intensity_label.config(text=f'{intensity_var.get():.1f}')
config.char_error_intensity = intensity_var.get()
intensity_var.trace('w', update_intensity_label)
# 错别字库路径
db_frame = ttk.Frame(parent)
db_frame.pack(fill='x', padx=10, pady=5)
ttk.Label(db_frame, text='错别字库路径:', width=12).pack(side='left')
db_var = tk.StringVar(value=config.char_error_db_path)
db_entry = ttk.Entry(db_frame, textvariable=db_var, width=30)
db_entry.pack(side='left', fill='x', expand=True, padx=(0, 5))
def browse_db_file():
filename = filedialog.askopenfilename(title='选择错别字库文件',
filetypes=[('JSON Files', '*.json')])
if filename:
db_var.set(filename)
ttk.Button(db_frame, text='浏览', command=browse_db_file).pack(side='right')
db_var.trace('w', lambda *args: setattr(config, 'char_error_db_path', db_var.get()))
ttk.Separator(parent, orient='horizontal').pack(fill='x', padx=10, pady=15)
# 免责声明
disclaimer_var = tk.BooleanVar(value=config.add_disclaimer)
ttk.Checkbutton(parent, text='添加免责声明', variable=disclaimer_var).pack(anchor='w', padx=10, pady=5)
disclaimer_var.trace('w', lambda *args: setattr(config, 'add_disclaimer', disclaimer_var.get()))
# 启用/禁用错别字相关控件
def toggle_error_controls(*args):
state = 'normal' if enable_errors_var.get() else 'disabled'
intensity_scale.configure(state=state)
db_entry.configure(state=state)
config.enable_char_errors = enable_errors_var.get()
enable_errors_var.trace('w', toggle_error_controls)
toggle_error_controls() # 初始化状态
return {
'enable_errors': enable_errors_var,
'intensity': intensity_var,
'db_path': db_var,
'reverse_text': reverse_var,
'punctuation': punctuation_var,
'disclaimer': disclaimer_var
}
window = sg.Window('转换设置', layout, modal=True, resizable=True, size=(500, 450))
while True:
event, values = window.read()
def _create_image_tab(parent):
"""创建图片处理选项卡"""
# 标题
ttk.Label(parent, text='图片处理设置', font=('', 12, 'bold')).pack(anchor='w', padx=10, pady=(10, 5))
ttk.Separator(parent, orient='horizontal').pack(fill='x', padx=10, pady=5)
# 图片排序方式
ttk.Label(parent, text='图片排序方式:', font=('', 10, 'bold')).pack(anchor='w', padx=10, pady=(0, 5))
sort_var = tk.StringVar(value=config.image_sort_by)
ttk.Radiobutton(parent, text='按名称', variable=sort_var, value='name').pack(anchor='w', padx=20, pady=2)
ttk.Radiobutton(parent, text='按修改时间', variable=sort_var, value='time').pack(anchor='w', padx=20, pady=2)
sort_var.trace('w', lambda *args: setattr(config, 'image_sort_by', sort_var.get()))
ttk.Separator(parent, orient='horizontal').pack(fill='x', padx=10, pady=15)
# 图片尺寸调整
ttk.Label(parent, text='图片尺寸调整:', font=('', 10, 'bold')).pack(anchor='w', padx=10, pady=(0, 5))
resize_var = tk.StringVar(value=config.image_resize)
ttk.Radiobutton(parent, text='不调整', variable=resize_var, value='none').pack(anchor='w', padx=20, pady=2)
width_frame = ttk.Frame(parent)
width_frame.pack(anchor='w', padx=20, pady=2)
width_radio = ttk.Radiobutton(width_frame, text='按宽度:', variable=resize_var, value='width')
width_radio.pack(side='left')
width_var = tk.StringVar(value=str(config.image_width))
width_entry = ttk.Entry(width_frame, textvariable=width_var, width=8)
width_entry.pack(side='left', padx=(5, 5))
ttk.Label(width_frame, text='英寸').pack(side='left')
resize_var.trace('w', lambda *args: setattr(config, 'image_resize', resize_var.get()))
width_var.trace('w', lambda *args: _update_image_width(width_var.get()))
ttk.Separator(parent, orient='horizontal').pack(fill='x', padx=10, pady=15)
# 图片对齐方式
ttk.Label(parent, text='图片对齐方式:', font=('', 10, 'bold')).pack(anchor='w', padx=10, pady=(0, 5))
align_var = tk.StringVar(value=config.image_alignment)
ttk.Radiobutton(parent, text='左对齐', variable=align_var, value='left').pack(anchor='w', padx=20, pady=2)
ttk.Radiobutton(parent, text='居中', variable=align_var, value='center').pack(anchor='w', padx=20, pady=2)
ttk.Radiobutton(parent, text='右对齐', variable=align_var, value='right').pack(anchor='w', padx=20, pady=2)
align_var.trace('w', lambda *args: setattr(config, 'image_alignment', align_var.get()))
ttk.Separator(parent, orient='horizontal').pack(fill='x', padx=10, pady=15)
# 图片不足时策略
ttk.Label(parent, text='图片不足时策略:', font=('', 10, 'bold')).pack(anchor='w', padx=10, pady=(0, 5))
strategy_var = tk.StringVar(value=config.image_strategy)
ttk.Radiobutton(parent, text='循环使用', variable=strategy_var, value='cycle').pack(anchor='w', padx=20, pady=2)
ttk.Radiobutton(parent, text='忽略多余标题', variable=strategy_var, value='truncate').pack(anchor='w', padx=20, pady=2)
ttk.Radiobutton(parent, text='重复最后一张', variable=strategy_var, value='repeat_last').pack(anchor='w', padx=20, pady=2)
strategy_var.trace('w', lambda *args: setattr(config, 'image_strategy', strategy_var.get()))
if event in (sg.WIN_CLOSED, '取消'):
break
# 处理错别字启用/禁用事件
if event == '-ENABLE_CHAR_ERRORS-':
enabled = values['-ENABLE_CHAR_ERRORS-']
window['char_error_intensity'].update(disabled=not enabled)
window['char_error_db_path'].update(disabled=not enabled)
def _create_format_tab(parent):
"""创建文档格式选项卡"""
# 标题
ttk.Label(parent, text='文档格式设置', font=('', 12, 'bold')).pack(anchor='w', padx=10, pady=(10, 5))
ttk.Separator(parent, orient='horizontal').pack(fill='x', padx=10, pady=5)
# 行间距
spacing_frame = ttk.Frame(parent)
spacing_frame.pack(fill='x', padx=10, pady=5)
ttk.Label(spacing_frame, text='行间距:', width=12).pack(side='left')
spacing_var = tk.StringVar(value=str(config.line_spacing))
spacing_entry = ttk.Entry(spacing_frame, textvariable=spacing_var, width=8)
spacing_entry.pack(side='left')
spacing_var.trace('w', lambda *args: _update_line_spacing(spacing_var.get()))
# 最大标题层级
title_frame = ttk.Frame(parent)
title_frame.pack(fill='x', padx=10, pady=5)
ttk.Label(title_frame, text='最大标题层级:', width=12).pack(side='left')
title_var = tk.StringVar(value=str(config.title_levels))
title_combo = ttk.Combobox(title_frame, textvariable=title_var, values=['1', '2', '3', '4', '5', '6'],
state='readonly', width=8)
title_combo.pack(side='left')
title_var.trace('w', lambda *args: _update_title_levels(title_var.get()))
if event == '重置为默认':
# 重置为默认值
from config import Config
default_config = Config()
window['txt_encoding'].update(default_config.txt_encoding)
window['match_exact'].update(True)
window['output_txt_folder'].update(True)
window['-REVERSE_TEXT-'].update(default_config.reverse_text_order)
window['-REPLACE_PUNCTUATION-'].update(default_config.replace_punctuation)
window['-ENABLE_CHAR_ERRORS-'].update(default_config.enable_char_errors)
window['char_error_intensity'].update(default_config.char_error_intensity)
window['char_error_db_path'].update(default_config.char_error_db_path)
window['-ADD_DISCLAIMER-'].update(default_config.add_disclaimer)
window['sort_name'].update(True)
window['resize_none'].update(True)
window['image_width'].update(str(default_config.image_width))
window['align_center'].update(True)
window['strategy_cycle'].update(True)
window['line_spacing'].update(str(default_config.line_spacing))
window['title_levels'].update(default_config.title_levels)
if event == '确定':
# 保存配置
config.txt_encoding = values['txt_encoding']
def _update_image_width(value):
"""更新图片宽度"""
try:
config.image_width = float(value)
except:
pass
if values['match_exact']:
config.match_pattern = "exact"
elif values['match_prefix']:
config.match_pattern = "prefix"
else:
config.match_pattern = "contains"
config.output_location = "txt_folder" if values['output_txt_folder'] else "custom"
config.image_sort_by = "name" if values['sort_name'] else "time"
config.image_resize = "none" if values['resize_none'] else "width"
config.reverse_text_order = values['-REVERSE_TEXT-']
config.replace_punctuation = values['-REPLACE_PUNCTUATION-']
config.add_disclaimer = values['-ADD_DISCLAIMER-']
def _update_line_spacing(value):
"""更新行间距"""
try:
config.line_spacing = float(value)
except:
pass
# 错别字处理配置
config.enable_char_errors = values['-ENABLE_CHAR_ERRORS-']
config.char_error_intensity = values['char_error_intensity']
config.char_error_db_path = values['char_error_db_path']
try:
config.image_width = float(values['image_width'])
except:
pass
def _update_title_levels(value):
"""更新标题层级"""
try:
config.title_levels = int(value)
except:
pass
if values['align_left']:
config.image_alignment = "left"
elif values['align_right']:
config.image_alignment = "right"
else:
config.image_alignment = "center"
if values['strategy_cycle']:
config.image_strategy = "cycle"
elif values['strategy_truncate']:
config.image_strategy = "truncate"
else:
config.image_strategy = "repeat_last"
def _reset_to_default(char_vars):
"""重置为默认值"""
from config import Config
default_config = Config()
# 更新所有配置
config.txt_encoding = default_config.txt_encoding
config.match_pattern = default_config.match_pattern
config.output_location = default_config.output_location
config.reverse_text_order = default_config.reverse_text_order
config.replace_punctuation = default_config.replace_punctuation
config.enable_char_errors = default_config.enable_char_errors
config.char_error_intensity = default_config.char_error_intensity
config.char_error_db_path = default_config.char_error_db_path
config.add_disclaimer = default_config.add_disclaimer
config.image_sort_by = default_config.image_sort_by
config.image_resize = default_config.image_resize
config.image_width = default_config.image_width
config.image_alignment = default_config.image_alignment
config.image_strategy = default_config.image_strategy
config.line_spacing = default_config.line_spacing
config.title_levels = default_config.title_levels
# 更新界面变量
if char_vars:
char_vars['enable_errors'].set(default_config.enable_char_errors)
char_vars['intensity'].set(default_config.char_error_intensity)
char_vars['db_path'].set(default_config.char_error_db_path)
char_vars['reverse_text'].set(default_config.reverse_text_order)
char_vars['punctuation'].set(default_config.replace_punctuation)
char_vars['disclaimer'].set(default_config.add_disclaimer)
messagebox.showinfo('信息', '配置已重置为默认值')
try:
config.line_spacing = float(values['line_spacing'])
config.title_levels = int(values['title_levels'])
except:
pass
from config import CONFIG_FILE_PATH
config.save_to_file(CONFIG_FILE_PATH)
break
window.close()
def _save_config(window, char_vars):
"""保存配置"""
from config import CONFIG_FILE_PATH
config.save_to_file(CONFIG_FILE_PATH)
messagebox.showinfo('信息', '配置已保存')
window.destroy()

View File

@ -5,11 +5,19 @@ GUI匹配编辑器模块
"""
import os
import PySimpleGUI as sg
import tkinter as tk
from tkinter import ttk, messagebox
def show_matching_editor(matched_pairs, images_root):
"""显示匹配编辑窗口,允许手动调整匹配关系"""
# 创建匹配编辑窗口
editor_window = tk.Toplevel()
editor_window.title('文件匹配编辑')
editor_window.geometry('900x600')
editor_window.transient()
editor_window.grab_set()
# 获取所有图片文件夹
all_image_folders = []
if os.path.isdir(images_root):
@ -18,68 +26,146 @@ def show_matching_editor(matched_pairs, images_root):
folder_path = os.path.join(root, dir_name)
rel_path = os.path.relpath(folder_path, images_root)
all_image_folders.append((folder_path, rel_path))
# 准备表格数据
table_data = []
# 标题
title_frame = ttk.Frame(editor_window)
title_frame.pack(fill='x', padx=10, pady=10)
ttk.Label(title_frame, text='文件匹配编辑', font=('', 14, 'bold')).pack()
ttk.Label(title_frame, text='选择要修改的项目,然后从右侧选择图片文件夹').pack()
# 主体区域使用PanedWindow分割
paned = ttk.PanedWindow(editor_window, orient='horizontal')
paned.pack(fill='both', expand=True, padx=10, pady=10)
# 左侧表格区域
left_frame = ttk.Frame(paned)
paned.add(left_frame, weight=2)
# 表格标题
ttk.Label(left_frame, text='TXT文件匹配列表:', font=('', 10, 'bold')).pack(anchor='w', pady=(0, 5))
# 表格区域
table_frame = ttk.Frame(left_frame)
table_frame.pack(fill='both', expand=True)
# 创建Treeview表格
columns = ('index', 'txt_name', 'image_folder')
tree = ttk.Treeview(table_frame, columns=columns, show='headings', height=15)
tree.heading('index', text='序号')
tree.heading('txt_name', text='TXT文件名')
tree.heading('image_folder', text='匹配的图片文件夹')
tree.column('index', width=60)
tree.column('txt_name', width=200)
tree.column('image_folder', width=250)
# 表格滚动条
table_scrollbar = ttk.Scrollbar(table_frame, orient='vertical', command=tree.yview)
tree.configure(yscrollcommand=table_scrollbar.set)
tree.pack(side='left', fill='both', expand=True)
table_scrollbar.pack(side='right', fill='y')
# 右侧文件夹列表区域
right_frame = ttk.Frame(paned)
paned.add(right_frame, weight=1)
# 文件夹列表标题
ttk.Label(right_frame, text='可用的图片文件夹:', font=('', 10, 'bold')).pack(anchor='w', pady=(0, 5))
# 文件夹列表区域
folders_frame = ttk.Frame(right_frame)
folders_frame.pack(fill='both', expand=True)
# 创建文件夹列表
folders_listbox = tk.Listbox(folders_frame, selectmode='single')
for folder_path, rel_path in all_image_folders:
folders_listbox.insert('end', rel_path)
# 文件夹列表滚动条
folders_scrollbar = ttk.Scrollbar(folders_frame, orient='vertical', command=folders_listbox.yview)
folders_listbox.configure(yscrollcommand=folders_scrollbar.set)
folders_listbox.pack(side='left', fill='both', expand=True)
folders_scrollbar.pack(side='right', fill='y')
# 操作按钮区域
button_frame = ttk.Frame(editor_window)
button_frame.pack(fill='x', padx=10, pady=10)
# 提供通过闭包访问变量的方式
selected_item_id = [None] # type: ignore
def on_tree_select(event):
"""TreeView选中事件"""
selection = tree.selection()
if selection:
selected_item_id[0] = selection[0] # type: ignore
tree.bind('<<TreeviewSelect>>', on_tree_select)
def set_matching():
"""设置选中项的匹配"""
if not selected_item_id[0]:
messagebox.showwarning('警告', '请先选择一个TXT文件')
return
folder_selection = folders_listbox.curselection()
if not folder_selection:
messagebox.showwarning('警告', '请先选择一个图片文件夹')
return
# 获取选中的索引
item_values = tree.item(selected_item_id[0], 'values')
row_index = int(item_values[0])
# 获取选中的文件夹
folder_index = folder_selection[0]
folder_path, folder_rel = all_image_folders[folder_index]
# 更新匹配关系
matched_pairs[row_index]['image_folder'] = {
"path": folder_path,
"name": os.path.basename(folder_path),
"relative_path": folder_rel
}
# 更新表格显示
tree.item(selected_item_id[0], values=(row_index, item_values[1], folder_rel))
messagebox.showinfo('成功', f'已将 "{item_values[1]}" 匹配到 "{folder_rel}"')
def clear_matching():
"""清除选中项的匹配"""
if not selected_item_id[0]:
messagebox.showwarning('警告', '请先选择一个TXT文件')
return
# 获取选中的索引
item_values = tree.item(selected_item_id[0], 'values')
row_index = int(item_values[0])
# 清除匹配关系
matched_pairs[row_index]['image_folder'] = None
# 更新表格显示
tree.item(selected_item_id[0], values=(row_index, item_values[1], '无匹配'))
messagebox.showinfo('成功', f'已清除 "{item_values[1]}" 的匹配关系')
def apply_all():
"""应用所有修改"""
editor_window.destroy()
# 按钮
ttk.Button(button_frame, text='设置选中项', command=set_matching).pack(side='left', padx=5)
ttk.Button(button_frame, text='清除选中项', command=clear_matching).pack(side='left', padx=5)
ttk.Button(button_frame, text='应用所有', command=apply_all).pack(side='right', padx=5)
# 填充表格数据
for i, pair in enumerate(matched_pairs):
txt_name = pair['txt']['name']
img_folder = pair['image_folder']['relative_path'] if pair['image_folder'] else "无匹配"
table_data.append([i, txt_name, img_folder])
layout = [
[sg.Text('文件匹配编辑', font=('bold', 14))],
[sg.Text('选择要修改的项目,然后从右侧选择图片文件夹')],
[
sg.Table(
values=table_data,
headings=['序号', 'TXT文件名', '匹配的图片文件夹'],
key='-TABLE-',
select_mode=sg.TABLE_SELECT_MODE_BROWSE,
enable_events=True,
justification='left',
size=(None, 15)
),
sg.VSeparator(),
sg.Listbox(
values=[f[1] for f in all_image_folders],
key='-FOLDERS-',
size=(40, 15),
enable_events=True
)
],
[sg.Button('设置选中项'), sg.Button('清除选中项'), sg.Button('应用所有')]
]
window = sg.Window('匹配编辑', layout, resizable=True)
selected_row = None
while True:
event, values = window.read()
if event in (sg.WIN_CLOSED, '应用所有'):
break
if event == '-TABLE-':
if values['-TABLE-']:
selected_row = values['-TABLE-'][0]
if event == '设置选中项' and selected_row is not None and values['-FOLDERS-']:
folder_idx = [i for i, f in enumerate(all_image_folders) if f[1] == values['-FOLDERS-'][0]][0]
folder_path, folder_rel = all_image_folders[folder_idx]
matched_pairs[selected_row]['image_folder'] = {
"path": folder_path,
"name": os.path.basename(folder_path),
"relative_path": folder_rel
}
table_data[selected_row][2] = folder_rel
window['-TABLE-'].update(values=table_data)
if event == '清除选中项' and selected_row is not None:
matched_pairs[selected_row]['image_folder'] = None
table_data[selected_row][2] = "无匹配"
window['-TABLE-'].update(values=table_data)
window.close()
img_folder = pair['image_folder']['relative_path'] if pair['image_folder'] else '无匹配'
tree.insert('', 'end', values=(i, txt_name, img_folder))
# 等待窗口关闭
editor_window.wait_window()
return matched_pairs

View File

@ -6,39 +6,133 @@ GUI结果显示模块
import os
import sys
import PySimpleGUI as sg
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
import subprocess
import platform
def show_results_window(results):
"""显示批量处理结果窗口"""
# 准备结果消息
if results['failed'] == 0:
title = '处理完成'
message = f"全部成功!\n共处理 {results['total']} 个文件,全部转换成功。"
if results['main_output_folder']:
message += f"\n主要输出文件夹: {results['main_output_folder']}"
sg.popup('处理完成', message)
_show_simple_result(title, message, results['main_output_folder'])
else:
title = '处理完成'
failed_text = "\n".join([f"- {item['name']}: {item['error']}" for item in results['failed_items']])
message = (f"处理完成!\n共处理 {results['total']} 个文件,"
f"{results['success']} 个成功,{results['failed']} 个失败。\n\n"
f"失败项:\n{failed_text}")
if results['main_output_folder']:
message += f"\n主要输出文件夹: {results['main_output_folder']}"
sg.popup_scrolled('处理完成', message, size=(60, 20))
_show_detailed_result(title, message, results['main_output_folder'])
# 询问是否打开输出文件夹
if results['main_output_folder'] and os.path.exists(results['main_output_folder']):
if sg.popup_yes_no('是否打开主要输出文件夹?') == 'Yes':
_open_folder(results['main_output_folder'])
def _show_simple_result(title, message, output_folder):
"""显示简单结果对话框"""
# 创建结果窗口
result_window = tk.Toplevel()
result_window.title(title)
result_window.geometry('500x200')
result_window.transient()
result_window.grab_set()
# 居中窗口
result_window.geometry('+{}+{}'.format(
result_window.winfo_screenwidth() // 2 - 250,
result_window.winfo_screenheight() // 2 - 100
))
# 消息区域
message_frame = ttk.Frame(result_window)
message_frame.pack(fill='both', expand=True, padx=20, pady=20)
# 消息文本
message_label = ttk.Label(message_frame, text=message, justify='left', wraplength=450)
message_label.pack(expand=True)
# 按钮区域
button_frame = ttk.Frame(result_window)
button_frame.pack(fill='x', padx=20, pady=(0, 20))
def open_folder_and_close():
if output_folder and os.path.exists(output_folder):
_open_folder(output_folder)
result_window.destroy()
def close_window():
result_window.destroy()
# 按钮
if output_folder and os.path.exists(output_folder):
ttk.Button(button_frame, text='打开输出文件夹', command=open_folder_and_close).pack(side='left', padx=(0, 10))
ttk.Button(button_frame, text='关闭', command=close_window).pack(side='right')
# 等待窗口关闭
result_window.wait_window()
def _show_detailed_result(title, message, output_folder):
"""显示详细结果对话框"""
# 创建结果窗口
result_window = tk.Toplevel()
result_window.title(title)
result_window.geometry('700x500')
result_window.transient()
result_window.grab_set()
# 居中窗口
result_window.geometry('+{}+{}'.format(
result_window.winfo_screenwidth() // 2 - 350,
result_window.winfo_screenheight() // 2 - 250
))
# 消息区域
message_frame = ttk.Frame(result_window)
message_frame.pack(fill='both', expand=True, padx=20, pady=20)
# 滚动文本区域
text_widget = scrolledtext.ScrolledText(message_frame, wrap=tk.WORD, state='normal')
text_widget.pack(fill='both', expand=True)
text_widget.insert('1.0', message)
text_widget.configure(state='disabled')
# 按钮区域
button_frame = ttk.Frame(result_window)
button_frame.pack(fill='x', padx=20, pady=(0, 20))
def open_folder_and_close():
if output_folder and os.path.exists(output_folder):
_open_folder(output_folder)
result_window.destroy()
def close_window():
result_window.destroy()
# 按钮
if output_folder and os.path.exists(output_folder):
ttk.Button(button_frame, text='打开输出文件夹', command=open_folder_and_close).pack(side='left', padx=(0, 10))
ttk.Button(button_frame, text='关闭', command=close_window).pack(side='right')
# 等待窗口关闭
result_window.wait_window()
def _open_folder(folder_path):
"""打开文件夹"""
try:
if sys.platform.startswith('win'):
system = platform.system()
if system == 'Windows':
os.startfile(folder_path)
elif sys.platform.startswith('darwin'):
os.system(f'open "{folder_path}"')
else:
os.system(f'xdg-open "{folder_path}"')
elif system == 'Darwin': # macOS
subprocess.run(['open', folder_path])
else: # Linux
subprocess.run(['xdg-open', folder_path])
except Exception as e:
sg.popup_error(f"无法打开文件夹: {e}")
messagebox.showerror('错误', f'无法打开文件夹: {e}')

505
main.py
View File

@ -23,7 +23,10 @@ try:
from batch_processor import BatchProcessor
# GUI相关导入
import PySimpleGUI as sg
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}")
@ -40,244 +43,418 @@ class TxtToDocxApp:
self.file_handler = FileHandler()
self.batch_processor = BatchProcessor()
# 设置GUI主题
sg.theme('BlueMono')
# 初始化主窗口
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._show_main_window()
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
self.root.mainloop()
except Exception as e:
sg.popup_error(f"应用程序运行出错: {str(e)}")
messagebox.showerror("错误", 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)
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)
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']
# 标题
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()
# 初始化窗口,避免更新元素时的警告
window.read(timeout=1)
# 分隔线
ttk.Separator(main_frame, orient='horizontal').pack(fill='x', pady=10)
# 初始化输出根文件夹输入框状态
self._update_output_root_state(output_root_input)
# 路径选择区域
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)
# 状态栏和按钮区域
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()
while True:
event, values = window.read()
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())
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):
def _update_output_root_state(self):
"""根据配置更新输出根文件夹输入框的状态"""
if config.output_location == "custom":
output_root_input.update(disabled=False)
try:
output_root_input.Widget.configure(foreground='black')
except:
pass
self.output_entry.configure(state='normal')
else:
output_root_input.update(disabled=True)
try:
output_root_input.Widget.configure(foreground='gray')
except:
pass
self.output_entry.configure(state='disabled')
def _save_current_settings(self, values):
def _save_current_settings(self):
"""保存当前设置"""
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)
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 _handle_scan_files(self, values, window, status_text, preview_table):
"""处理扫描文件事件"""
txt_folder = values['txt_folder']
images_root = values['images_root']
def _scan_files(self):
"""扫描文件"""
txt_folder = self.txt_folder_var.get()
images_root = self.images_root_var.get()
if not txt_folder:
sg.popup_error('请选择TXT文件所在的文件夹')
messagebox.showerror('错误', '请选择TXT文件所在的文件夹')
return
if not images_root:
sg.popup_error('请选择图片根文件夹')
messagebox.showerror('错误', '请选择图片根文件夹')
return
# 保存路径
config.last_txt_folder = txt_folder
config.last_images_root = images_root
if values['output_root']:
config.last_output_root = values['output_root']
if self.output_root_var.get():
config.last_output_root = self.output_root_var.get()
config.save_to_file(CONFIG_FILE_PATH)
try:
status_text.update('正在扫描TXT文件...')
window.refresh()
self.status_text.set('正在扫描TXT文件...')
self.root.update()
txt_files = self.file_handler.scan_txt_files(txt_folder)
status_text.update('正在匹配图片文件夹...')
window.refresh()
self.status_text.set('正在匹配图片文件夹...')
self.root.update()
self.matched_pairs = self.file_handler.find_matching_image_folders(txt_files, images_root)
# 更新预览表格
table_data = []
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 "无匹配"
table_data.append([
self.tree.insert('', 'end', values=(
pair['txt']['name'],
pair['txt']['relative_path'],
img_folder
])
))
preview_table.update(values=table_data)
status_text.update(f'扫描完成: 找到 {len(self.matched_pairs)} 个TXT文件')
self.status_text.set(f'扫描完成: 找到 {len(self.matched_pairs)} 个TXT文件')
# 启用相关按钮
window['编辑匹配'].update(disabled=False)
window['开始批量转换'].update(disabled=False)
self.edit_btn.configure(state='normal')
self.convert_btn.configure(state='normal')
except Exception as e:
sg.popup_error(f'扫描失败: {str(e)}')
status_text.update('状态: 扫描失败')
messagebox.showerror('错误', f'扫描失败: {str(e)}')
self.status_text.set('状态: 扫描失败')
def _handle_edit_matching(self, values, preview_table):
"""处理编辑匹配事件"""
images_root = values['images_root']
def _edit_matching(self):
"""编辑匹配"""
images_root = self.images_root_var.get()
if not images_root:
sg.popup_error('请选择图片根文件夹')
messagebox.showerror('错误', '请选择图片根文件夹')
return
if not self.matched_pairs:
sg.popup_error('请先扫描文件')
messagebox.showerror('错误', '请先扫描文件')
return
# 显示匹配编辑窗口
self.matched_pairs = self._show_matching_editor(self.matched_pairs, images_root)
# 更新预览表格
table_data = []
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 "无匹配"
table_data.append([
self.tree.insert('', 'end', values=(
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):
"""处理批量转换事件"""
def _start_conversion(self):
"""开始批量转换"""
if not self.matched_pairs:
sg.popup_error('请先扫描文件')
messagebox.showerror('错误', '请先扫描文件')
return
if config.output_location == "custom" and not values['output_root']:
sg.popup_error('请选择输出根文件夹(在"转换设置"中选择了"输出到指定文件夹"')
if config.output_location == "custom" and not self.output_root_var.get():
messagebox.showerror('错误', '请选择输出根文件夹(在"转换设置"中选择了"输出到指定文件夹"')
return
try:
progress_bar.update(0, visible=True)
status_text.update('开始批量转换...')
window.refresh()
# 禁用按钮,显示进度条
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):
progress_bar.update(progress)
status_text.update(f'状态: {text}')
window.refresh()
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,
values['output_root'],
update_batch_progress
)
self._show_results_window(results)
status_text.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:
sg.popup_error(f'批量处理失败: {str(e)}')
status_text.update('状态: 批量转换失败')
finally:
progress_bar.update(0, visible=False)
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):
"""显示配置窗口"""
@ -335,7 +512,7 @@ class TxtToDocxApp:
- 错别字库可自定义JSON格式的错别字映射文件
- 常见映射事等
"""
sg.popup_scrolled('使用帮助', help_text, size=(70, 25))
def _show_matching_editor(self, matched_pairs, images_root):
"""显示匹配编辑窗口"""
@ -350,7 +527,7 @@ class TxtToDocxApp:
def main():
"""主函数"""
print("正在启动批量Markdown TXT转DOCX工具...")
# print("正在启动批量Markdown TXT转DOCX工具...")
try:
app = TxtToDocxApp()
@ -359,7 +536,7 @@ def main():
print("\n用户中断程序运行")
except Exception as e:
print(f"程序运行出错: {e}")
sg.popup_error(f"程序运行出错: {e}")
messagebox.showerror("错误", f"程序运行出错: {e}")
finally:
print("程序已退出")

View File

@ -215,7 +215,7 @@ class FileHandler:
except UnicodeDecodeError:
continue
raise UnicodeDecodeError(f"无法解码文件 '{filename}',尝试的编码格式:{encodings}")
raise ValueError(f"无法解码文件 '{filename}',尝试的编码格式:{encodings}")
@staticmethod
def write_file(filename: str, content: str, encoding: str = 'utf-8') -> None:
@ -394,12 +394,15 @@ def replace_text(text):
run_tests()
sys.exit(0)
if __name__ == "__main__":
# 命令行模式
if len(sys.argv) > 1:
main()
else:
# 示例演示
sample_text = text
sample_text = """阅读此文之前,麻烦您点击一下"关注",既方便您进行讨论和分享,又能给您带来不一样的参与感,创作不易,感谢您的支持。
曾经"半路出家"如今黯然无声他的故事值得一品"""
print("示例演示:")
print("原文:")
@ -423,8 +426,6 @@ def replace_text(text):
print(" python script.py -f input.txt -p '。!?' -s # 自定义标点符号并显示统计")
print(" python script.py test # 运行单元测试")
return processed
text = """阅读此文之前,麻烦您点击一下“关注”,既方便您进行讨论和分享,又能给您带来不一样的参与感,创作不易,感谢您的支持。
@ -469,5 +470,3 @@ text = """阅读此文之前,麻烦您点击一下“关注”,既方便您
欢迎留言讨论你们的每一次互动都是创作的动力"""
result = replace_text(text)
print(result)