#!/usr/bin/env python # -*- coding: utf-8 -*- """主窗口模块""" import os import sys import subprocess import tkinter as tk from tkinter import messagebox, filedialog, scrolledtext from app.config.settings import ConfigManager from app.core.utils.log_utils import set_log_level from .theme import THEMES, get_theme_mode, set_theme_mode, create_modern_button, create_card_frame from .logging_ui import add_to_log, poll_log_queue from .ui_widgets import StatusBar from .user_settings import ( load_user_settings, save_user_settings, refresh_recent_list_widget, _extract_path_from_recent_item, clear_recent_files, RECENT_LIST_WIDGET, ) from .file_operations import ( ensure_directories, open_result_directory, clean_cache, clean_data_files, clean_result_files, ) from .action_handlers import ( process_single_image_with_status, run_pipeline_directly, batch_ocr_with_status, batch_process_orders_with_status, merge_orders_with_status, process_excel_file_with_status, process_dropped_file, ) from .memory_editor import show_memory_editor from .config_dialog import show_config_dialog from .barcode_editor import edit_barcode_mappings from .shortcuts import bind_keyboard_shortcuts from app.core.utils.dialog_utils import show_cloud_sync_dialog def _init_window(): """初始化窗口、主题和设置,返回 (root, theme, settings, dnd_supported)""" ensure_directories() dnd_supported = False try: from tkinterdnd2 import TkinterDnD, DND_FILES root = TkinterDnD.Tk() dnd_supported = True except Exception: root = tk.Tk() settings = load_user_settings() theme_mode = settings.get('theme_mode', get_theme_mode()) set_theme_mode(theme_mode) try: cfg_for_title = ConfigManager() ver = cfg_for_title.get('App', 'version', fallback='dev') root.title(f"益选-OCR订单处理系统 v{ver} by 欢欢欢") except Exception: root.title("益选-OCR订单处理系统 by 欢欢欢") root.geometry("900x600") settings['window_size'] = "900x600" theme = THEMES[get_theme_mode()] root.configure(bg=theme["bg"]) try: log_level = settings.get('log_level') if log_level: set_log_level(log_level) concurrency = settings.get('concurrency_max_workers') if concurrency: cfg = ConfigManager() cfg.update('Performance', 'max_workers', str(concurrency)) cfg.save_config() except Exception: pass try: root.iconbitmap(default="") except Exception: pass return root, theme, settings, dnd_supported def _create_left_panel(content_frame, theme, log_text, status_bar): """创建左侧面板:完整流程、OCR处理、Excel处理、最近文件""" left_panel = create_card_frame(content_frame) left_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=False, padx=(0, 5), pady=5) left_panel.configure(width=160) panel_content = tk.Frame(left_panel, bg=theme["card_bg"]) panel_content.pack(fill=tk.BOTH, expand=True, padx=10, pady=(5, 10)) # 完整流程区 pipeline_section = tk.LabelFrame( panel_content, text="完整流程", bg=theme["card_bg"], fg=theme["fg"], font=("Microsoft YaHei UI", 10, "bold"), relief="flat", borderwidth=0 ) pipeline_section.pack(fill=tk.X, pady=(0, 8)) pipeline_frame = tk.Frame(pipeline_section, bg=theme["card_bg"]) pipeline_frame.pack(fill=tk.X, padx=8, pady=6) create_modern_button(pipeline_frame, "一键处理", lambda: run_pipeline_directly(log_text, status_bar), "primary", px_width=150, px_height=32).pack(anchor='w', pady=3) # OCR处理区 core_section = tk.LabelFrame( panel_content, text="OCR处理", bg=theme["card_bg"], fg=theme["fg"], font=("Microsoft YaHei UI", 10, "bold"), relief="flat", borderwidth=0 ) core_section.pack(fill=tk.X, pady=(0, 8)) core_buttons_frame = tk.Frame(core_section, bg=theme["card_bg"]) core_buttons_frame.pack(fill=tk.X, padx=8, pady=6) core_row1 = tk.Frame(core_buttons_frame, bg=theme["card_bg"]) core_row1.pack(fill=tk.X, pady=3) create_modern_button(core_row1, "批量识别", lambda: batch_ocr_with_status(log_text, status_bar), "primary", px_width=72, px_height=32).pack(side=tk.LEFT, padx=(0, 3)) create_modern_button(core_row1, "单个识别", lambda: process_single_image_with_status(log_text, status_bar), "primary", px_width=72, px_height=32).pack(side=tk.LEFT, padx=(3, 0)) # Excel处理区 ocr_section = tk.LabelFrame( panel_content, text="Excel处理", bg=theme["card_bg"], fg=theme["fg"], font=("Microsoft YaHei UI", 10, "bold"), relief="flat", borderwidth=0 ) ocr_section.pack(fill=tk.X, pady=(0, 8)) ocr_buttons_frame = tk.Frame(ocr_section, bg=theme["card_bg"]) ocr_buttons_frame.pack(fill=tk.X, padx=8, pady=6) ocr_row1 = tk.Frame(ocr_buttons_frame, bg=theme["card_bg"]) ocr_row1.pack(fill=tk.X, pady=3) create_modern_button(ocr_row1, "批量处理", lambda: batch_process_orders_with_status(log_text, status_bar), "primary", px_width=72, px_height=32).pack(side=tk.LEFT, padx=(0, 3)) create_modern_button(ocr_row1, "单个处理", lambda: process_excel_file_with_status(log_text, status_bar), "primary", px_width=72, px_height=32).pack(side=tk.LEFT, padx=(3, 0)) # 最近文件区 _create_recent_files_section(panel_content, theme, log_text) def _create_recent_files_section(parent, theme, log_text): """创建最近文件列表区域""" recent_section = tk.LabelFrame( parent, text="最近文件", bg=theme["card_bg"], fg=theme["fg"], font=("Microsoft YaHei UI", 10, "bold"), relief="flat", borderwidth=0 ) recent_section.pack(fill=tk.BOTH, pady=(0, 12)) recent_frame = tk.Frame(recent_section, bg=theme["card_bg"]) recent_frame.pack(fill=tk.BOTH, padx=8, pady=6) recent_top = tk.Frame(recent_frame, bg=theme["card_bg"]) recent_top.pack(fill=tk.X) def _resize_recent_top(e): try: h = max(int(e.height * 0.85), 180) recent_top.configure(height=h) except Exception: pass try: recent_top.pack_propagate(False) except Exception: pass recent_frame.bind('', _resize_recent_top) recent_rect = tk.Frame(recent_top, bg=theme["card_bg"], highlightbackground=theme["border"], highlightthickness=1) recent_rect.pack(fill=tk.BOTH, expand=True) recent_list = tk.Listbox(recent_rect, height=20) recent_scrollbar = tk.Scrollbar(recent_rect) recent_list.configure(yscrollcommand=recent_scrollbar.set) recent_scrollbar.configure(command=recent_list.yview) recent_list.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) recent_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) import app.ui.user_settings as _us_mod _us_mod.RECENT_LIST_WIDGET = recent_list def _open_selected_event(evt=None): try: idxs = recent_list.curselection() if not idxs: return p = _extract_path_from_recent_item(recent_list.get(idxs[0])) if os.path.exists(p): os.startfile(p) else: messagebox.showwarning("文件不存在", p) except Exception as e: messagebox.showerror("打开失败", str(e)) recent_list.bind('', _open_selected_event) refresh_recent_list_widget() rf_btns = tk.Frame(recent_frame, bg=theme["card_bg"]) rf_btns.pack(fill=tk.X, pady=6) def clear_list(): clear_recent_files() recent_list.delete(0, tk.END) create_modern_button(rf_btns, "清空列表", clear_list, "primary", px_width=72, px_height=32).pack(side=tk.LEFT, padx=(3, 0)) def purge_invalid(): try: kept = [] for i in range(recent_list.size()): item = recent_list.get(i) p = _extract_path_from_recent_item(item) if os.path.exists(p): kept.append(p) try: kept_sorted = sorted(kept, key=lambda p: os.path.getmtime(p), reverse=True) except Exception: kept_sorted = kept s = load_user_settings() s['recent_files'] = kept_sorted save_user_settings(s) recent_list.delete(0, tk.END) for i, p in enumerate(s['recent_files'][:recent_list.size() or len(s['recent_files'])], start=1): recent_list.insert(tk.END, f"{i}. {p}") refresh_recent_list_widget() add_to_log(log_text, "已清理无效的最近文件条目\n", "success") except Exception as e: messagebox.showerror("清理失败", str(e)) create_modern_button(rf_btns, "清理无效", purge_invalid, "primary", px_width=72, px_height=32).pack(side=tk.LEFT, padx=(3, 0)) def _create_right_panel(content_frame, theme, log_text, root): """创建右侧面板:快捷操作、系统设置""" right_panel = create_card_frame(content_frame) right_panel.pack(side=tk.RIGHT, fill=tk.BOTH, expand=False, padx=(5, 0), pady=5) right_panel.configure(width=380) right_panel_content = tk.Frame(right_panel, bg=theme["card_bg"]) right_panel_content.pack(fill=tk.BOTH, expand=True, padx=10, pady=(5, 10)) # 工具功能区 tools_section = tk.LabelFrame( right_panel_content, text="快捷操作", bg=theme["card_bg"], fg=theme["fg"], font=("Microsoft YaHei UI", 10, "bold"), relief="flat", borderwidth=0 ) tools_section.pack(fill=tk.X, pady=(0, 8)) tools_buttons_frame = tk.Frame(tools_section, bg=theme["card_bg"]) tools_buttons_frame.pack(fill=tk.X, padx=8, pady=6) tk.Frame(tools_buttons_frame, bg=theme["card_bg"]).pack(fill=tk.X, pady=3) create_modern_button(tools_buttons_frame, "打开结果目录", lambda: open_result_directory(), "primary", px_width=132, px_height=32).pack(anchor='w', pady=3) create_modern_button(tools_buttons_frame, "打开输出目录", lambda: os.startfile(ConfigManager().get_path('Paths', 'output_folder', fallback='data/output', create=True)), "primary", px_width=132, px_height=32).pack(anchor='w', pady=3) create_modern_button(tools_buttons_frame, "打开输入目录", lambda: os.startfile(ConfigManager().get_path('Paths', 'input_folder', fallback='data/input', create=True)), "primary", px_width=132, px_height=32).pack(anchor='w', pady=3) create_modern_button(tools_buttons_frame, "合并订单", lambda: merge_orders_with_status(log_text, StatusBar(root)), "primary", px_width=132, px_height=32).pack(anchor='w', pady=3) create_modern_button(tools_buttons_frame, "清除缓存", lambda: clean_cache(log_text), "primary", px_width=132, px_height=32).pack(anchor='w', pady=3) create_modern_button(tools_buttons_frame, "清理input/out文件", lambda: clean_data_files(log_text), "primary", px_width=132, px_height=32).pack(anchor='w', pady=3) create_modern_button(tools_buttons_frame, "清理result文件", lambda: clean_result_files(log_text), "primary", px_width=132, px_height=32).pack(anchor='w', pady=3) # 系统设置区 settings_section = tk.LabelFrame( right_panel_content, text="系统设置", bg=theme["card_bg"], fg=theme["fg"], font=("Microsoft YaHei UI", 10, "bold"), relief="flat", borderwidth=0 ) settings_section.pack(fill=tk.X, pady=(0, 8)) settings_buttons_frame = tk.Frame(settings_section, bg=theme["card_bg"]) settings_buttons_frame.pack(fill=tk.X, padx=8, pady=6) create_modern_button(settings_buttons_frame, "系统设置", lambda: show_config_dialog(root, ConfigManager()), "primary", px_width=132, px_height=32).pack(anchor='w', pady=3) create_modern_button(settings_buttons_frame, "条码映射", lambda: edit_barcode_mappings(log_text), "primary", px_width=132, px_height=32).pack(anchor='w', pady=3) create_modern_button(settings_buttons_frame, "云端同步", lambda: show_cloud_sync_dialog(root), "primary", px_width=132, px_height=32).pack(anchor='w', pady=3) create_modern_button(settings_buttons_frame, "商品记忆库", lambda: show_memory_editor(root), "primary", px_width=132, px_height=32).pack(anchor='w', pady=3) def _setup_drag_area(mid_container, theme, dnd_supported, log_text, status_bar): """创建拖拽/点击选择文件区域""" drag_panel = create_card_frame(mid_container) drag_panel.pack(side=tk.TOP, fill=tk.X, padx=(5, 5), pady=(0, 5)) drag_panel_content = tk.Frame(drag_panel, bg=theme["card_bg"]) drag_panel_content.pack(fill=tk.X, padx=10, pady=6) dnd_section = tk.LabelFrame( drag_panel_content, bg=theme["card_bg"], fg=theme["fg"], font=("Microsoft YaHei UI", 10, "bold"), relief="flat", borderwidth=0 ) dnd_section.pack(fill=tk.X, pady=(0, 0)) dnd_frame = tk.Frame(dnd_section, bg=theme["card_bg"], highlightthickness=1, highlightbackground=theme["border"]) dnd_frame.configure(height=60) dnd_frame.pack(fill=tk.X, padx=8, pady=6) try: dnd_frame.pack_propagate(False) except Exception: pass def _set_highlight(active: bool): try: dnd_frame.configure(highlightbackground=theme["info"] if active else theme["border"]) except Exception: pass dnd_frame.bind('', lambda e: _set_highlight(True)) dnd_frame.bind('', lambda e: _set_highlight(False)) msg_row = tk.Frame(dnd_frame, bg=theme["card_bg"]) msg_row.pack(fill=tk.X) if dnd_supported: tk.Label( msg_row, text="拖拽已启用:拖拽或点击此区域选择文件", bg=theme["card_bg"], fg="#999999", justify="center" ).pack(fill=tk.X) else: tk.Label( msg_row, text="点击此区域选择文件;可安装拖拽支持", bg=theme["card_bg"], fg="#999999", justify="center" ).pack(fill=tk.X) if not dnd_supported: btn_row = tk.Frame(dnd_frame, bg=theme["card_bg"]) btn_row.pack(fill=tk.X) is_frozen = getattr(sys, 'frozen', False) def copy_install(): try: mid_container.winfo_toplevel().clipboard_clear() mid_container.winfo_toplevel().clipboard_append("pip install tkinterdnd2") messagebox.showinfo("已复制", "已复制安装命令:pip install tkinterdnd2") except Exception as e: messagebox.showwarning("复制失败", str(e)) if is_frozen: tk.Label( btn_row, text="EXE版不支持运行时安装,请用源码版安装后重新打包", bg=theme["card_bg"], fg="#999999", font=("Microsoft YaHei UI", 8) ).pack(side=tk.RIGHT, padx=4) else: def install_and_restart(): try: add_to_log(log_text, "开始安装拖拽支持库 tkinterdnd2...\n", "info") cmd = [sys.executable, "-m", "pip", "install", "tkinterdnd2"] result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) add_to_log(log_text, result.stdout + "\n", "info") add_to_log(log_text, "安装成功,准备重启程序以启用拖拽...\n", "success") if messagebox.askyesno("安装完成", "已安装拖拽支持,是否立即重启应用?"): os.execl(sys.executable, sys.executable, *sys.argv) except subprocess.CalledProcessError as e: add_to_log(log_text, f"安装失败: {e.stderr}\n", "error") messagebox.showerror("安装失败", f"安装输出:\n{e.stderr}") except Exception as e: add_to_log(log_text, f"安装失败: {str(e)}\n", "error") messagebox.showerror("安装失败", str(e)) create_modern_button(btn_row, "一键安装拖拽", install_and_restart, "primary", px_width=132, px_height=28).pack(side=tk.RIGHT, padx=(3, 0)) create_modern_button(btn_row, "复制安装命令", copy_install, "primary", px_width=132, px_height=28).pack(side=tk.RIGHT) # 点击拖拽框选择文件 def _click_select(evt=None): try: files = filedialog.askopenfilenames( title="选择图片或Excel文件", filetypes=[ ("支持文件", "*.xlsx *.xls *.jpg *.jpeg *.png *.bmp"), ("Excel", "*.xlsx *.xls"), ("图片", "*.jpg *.jpeg *.png *.bmp"), ("所有文件", "*.*"), ] ) if not files: return for p in files: process_dropped_file(log_text, status_bar, p) except Exception as e: messagebox.showerror("选择失败", str(e)) dnd_frame.bind('', _click_select) msg_row.bind('', _click_select) if dnd_supported: def _on_drop(event): try: data = event.data paths = [] buf = "" in_brace = False for ch in data: if ch == '{': in_brace = True buf = "" elif ch == '}': in_brace = False paths.append(buf) buf = "" elif ch == ' ' and not in_brace: if buf: paths.append(buf) buf = "" else: buf += ch if buf: paths.append(buf) for p in paths: process_dropped_file(log_text, status_bar, p) except Exception as e: add_to_log(log_text, f"拖拽处理失败: {str(e)}\n", "error") try: from tkinterdnd2 import DND_FILES dnd_frame.drop_target_register(DND_FILES) dnd_frame.dnd_bind('<>', _on_drop) except Exception: pass def _create_log_panel(mid_container, theme): """创建中间日志面板,返回 log_text widget""" log_panel = create_card_frame(mid_container, "处理日志") log_panel.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=(5, 5), pady=5) log_text = scrolledtext.ScrolledText( log_panel, wrap=tk.WORD, width=68, height=26, bg=theme["log_bg"], fg=theme["log_fg"], font=("Consolas", 9), state=tk.DISABLED, relief="flat", borderwidth=0 ) log_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=(5, 10)) log_text.tag_configure("command", foreground=theme["info"], font=("Consolas", 9, "bold")) log_text.tag_configure("time", foreground=theme["secondary_bg"], font=("Consolas", 8)) log_text.tag_configure("separator", foreground=theme["border"]) log_text.tag_configure("success", foreground=theme["success"], font=("Consolas", 9, "bold")) log_text.tag_configure("error", foreground=theme["error"], font=("Consolas", 9, "bold")) log_text.tag_configure("warning", foreground=theme["warning"], font=("Consolas", 9, "bold")) log_text.tag_configure("info", foreground=theme["info"], font=("Consolas", 9)) poll_log_queue(log_text) try: _ver = ConfigManager().get('App', 'version', fallback='') _ver_str = f" v{_ver}" if _ver else "" except Exception: _ver_str = "" add_to_log(log_text, f"欢迎使用 益选-OCR订单处理系统{_ver_str}\n", "success") add_to_log(log_text, "系统已就绪,请选择相应功能进行操作。\n\n", "info") add_to_log(log_text, "功能说明:\n", "command") add_to_log(log_text, "• 完整处理流程:一键完成OCR识别和Excel处理\n", "info") add_to_log(log_text, "• 批量处理订单:批量处理多个订单文件\n", "info") add_to_log(log_text, "• 处理烟草订单:专门处理烟草类订单\n", "info") add_to_log(log_text, "• 合并订单:将多个订单合并为一个文件\n\n", "info") cfg = ConfigManager() add_to_log(log_text, f"请将需要处理的图片文件放入 {cfg.get_path('Paths', 'input_folder', fallback='data/input')} 目录中。\n", "warning") add_to_log(log_text, f"OCR识别结果保存在 {cfg.get_path('Paths', 'output_folder', fallback='data/output')} 目录,处理完成的订单保存在 {cfg.get_path('Paths', 'result_folder', fallback='data/result')} 目录中。\n\n", "warning") add_to_log(log_text, "=" * 50 + "\n\n", "separator") return log_text def main(): """主函数""" try: root, theme, settings, dnd_supported = _init_window() # 主容器 main_container = tk.Frame(root, bg=theme["bg"]) main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) content_frame = tk.Frame(main_container, bg=theme["bg"]) content_frame.pack(fill=tk.BOTH, expand=True) # 中间容器(拖拽区 + 日志区) mid_container = tk.Frame(content_frame, bg=theme["bg"]) mid_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 5), pady=5) log_text = _create_log_panel(mid_container, theme) # 状态栏 status_bar = StatusBar(root) status_bar.pack(side=tk.BOTTOM, fill=tk.X) # 左侧面板 _create_left_panel(content_frame, theme, log_text, status_bar) # 右侧面板 _create_right_panel(content_frame, theme, log_text, root) # 拖拽区域 _setup_drag_area(mid_container, theme, dnd_supported, log_text, status_bar) # 快捷键 + 关闭事件 def on_close(): try: w = root.winfo_width() h = root.winfo_height() settings['window_size'] = f"{w}x{h}" settings['theme_mode'] = get_theme_mode() save_user_settings(settings) except Exception: pass root.destroy() root.protocol("WM_DELETE_WINDOW", on_close) bind_keyboard_shortcuts(root, log_text, status_bar) root.mainloop() except Exception as e: import traceback error_msg = f"程序启动失败: {str(e)}\n详细错误信息:\n{traceback.format_exc()}" print(error_msg) try: import tkinter.messagebox as mb mb.showerror("启动错误", f"程序启动失败:\n{str(e)}") except Exception: pass