diff --git a/error_chars.py b/error_chars.py index ee5a62a..c84ff6b 100644 --- a/error_chars.py +++ b/error_chars.py @@ -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): diff --git a/gui_config.py b/gui_config.py index ca938ec..c2b1ee2 100644 --- a/gui_config.py +++ b/gui_config.py @@ -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('<>', 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() \ No newline at end of file +def _save_config(window, char_vars): + """保存配置""" + from config import CONFIG_FILE_PATH + config.save_to_file(CONFIG_FILE_PATH) + messagebox.showinfo('信息', '配置已保存') + window.destroy() \ No newline at end of file diff --git a/gui_matching_editor.py b/gui_matching_editor.py index 6075022..ddc5f73 100644 --- a/gui_matching_editor.py +++ b/gui_matching_editor.py @@ -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('<>', 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 \ No newline at end of file diff --git a/gui_results.py b/gui_results.py index 0870fbf..71291e4 100644 --- a/gui_results.py +++ b/gui_results.py @@ -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}") \ No newline at end of file + messagebox.showerror('错误', f'无法打开文件夹: {e}') \ No newline at end of file diff --git a/main.py b/main.py index d2b58d2..30fa3e0 100644 --- a/main.py +++ b/main.py @@ -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('', 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() - 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("程序已退出") diff --git a/replacestr.py b/replacestr.py index e35e174..d415703 100644 --- a/replacestr.py +++ b/replacestr.py @@ -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) \ No newline at end of file