import tkinter as tk from tkinter import ttk, messagebox, simpledialog import cv2 from config import config_manager import threading import serial.tools.list_ports class ConfigGUI: """配置GUI界面""" def __init__(self): self.root = tk.Tk() self.root.title("配置管理 - 火炬之光自动化") self.root.geometry("1000x720") self.root.minsize(900, 600) # 设置最小尺寸 self.selected_index = 0 self.preview_thread = None # 添加预览线程引用 self.setup_ui() self.load_current_config() def setup_ui(self): """设置UI界面""" # 左侧:配置组列表 left_frame = ttk.Frame(self.root, padding="10") left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=False) ttk.Label(left_frame, text="配置组列表", font=('Arial', 12, 'bold')).pack() # 配置组列表 self.group_listbox = tk.Listbox(left_frame, width=20, height=25) self.group_listbox.pack(fill=tk.BOTH, expand=True, pady=5) self.group_listbox.bind('<>', self.on_group_select) # 按钮 btn_frame = ttk.Frame(left_frame) btn_frame.pack(fill=tk.X) ttk.Button(btn_frame, text="添加组", command=self.add_group).pack(fill=tk.X, pady=2) ttk.Button(btn_frame, text="删除组", command=self.delete_group).pack(fill=tk.X, pady=2) ttk.Button(btn_frame, text="设为活动", command=self.set_active).pack(fill=tk.X, pady=2) # 右侧:配置详情 right_frame = ttk.Frame(self.root, padding="10") right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) ttk.Label(right_frame, text="配置详情", font=('Arial', 12, 'bold')).pack() # 配置项 self.config_vars = {} # 基本配置 basic_frame = ttk.LabelFrame(right_frame, text="基本配置", padding="10") basic_frame.pack(fill=tk.X, pady=5) self.create_entry(basic_frame, "name", "配置组名称:") # 采集卡索引:使用下拉+扫描 cam_row = ttk.Frame(basic_frame) cam_row.pack(fill=tk.X, pady=3) ttk.Label(cam_row, text="采集卡索引:", width=15).pack(side=tk.LEFT) self.camera_index_var = tk.StringVar() self.camera_index_cb = ttk.Combobox(cam_row, textvariable=self.camera_index_var, width=23, state="readonly") self.camera_index_cb.pack(side=tk.LEFT, padx=5) ttk.Button(cam_row, text="扫描采集卡", command=self.scan_cameras).pack(side=tk.LEFT, padx=5) # 将变量放进config_vars统一管理 self.config_vars["camera_index"] = self.camera_index_var self.create_entry(basic_frame, "camera_width", "采集宽度:") self.create_entry(basic_frame, "camera_height", "采集高度:") # 串口配置 serial_frame = ttk.LabelFrame(right_frame, text="串口配置", padding="10") serial_frame.pack(fill=tk.X, pady=5) # 串口:使用下拉+扫描 port_row = ttk.Frame(serial_frame) port_row.pack(fill=tk.X, pady=3) ttk.Label(port_row, text="串口:", width=15).pack(side=tk.LEFT) self.serial_port_var = tk.StringVar() self.serial_port_cb = ttk.Combobox(port_row, textvariable=self.serial_port_var, width=23, state="readonly") self.serial_port_cb.pack(side=tk.LEFT, padx=5) ttk.Button(port_row, text="扫描串口", command=self.scan_ports).pack(side=tk.LEFT, padx=5) # 将变量放进config_vars统一管理 self.config_vars["serial_port"] = self.serial_port_var self.create_entry(serial_frame, "serial_baudrate", "波特率:") # 游戏配置 game_frame = ttk.LabelFrame(right_frame, text="游戏配置", padding="10") game_frame.pack(fill=tk.X, pady=5) self.create_entry(game_frame, "move_velocity", "移动速度(v值):") # 预览配置 preview_frame = ttk.LabelFrame(right_frame, text="预览配置", padding="10") preview_frame.pack(fill=tk.X, pady=5) self.create_entry(preview_frame, "preview_width", "预览宽度:", prefix="display") self.create_entry(preview_frame, "preview_height", "预览高度:", prefix="display") self.create_entry(preview_frame, "preview_columns", "预览列数:", prefix="display") self.create_entry(preview_frame, "preview_rows", "预览行数:", prefix="display") # 多窗口预览开关 multi_row = ttk.Frame(preview_frame) multi_row.pack(fill=tk.X, pady=3) ttk.Label(multi_row, text="多窗口预览:", width=15).pack(side=tk.LEFT) self.preview_multi_window_var = tk.BooleanVar(value=False) multi_cb = ttk.Checkbutton(multi_row, variable=self.preview_multi_window_var) multi_cb.pack(side=tk.LEFT, padx=5) self.config_vars["display_preview_multi_window"] = self.preview_multi_window_var # 预览全部配置组开关 all_row = ttk.Frame(preview_frame) all_row.pack(fill=tk.X, pady=3) ttk.Label(all_row, text="预览全部配置组:", width=15).pack(side=tk.LEFT) self.preview_use_all_groups_var = tk.BooleanVar(value=True) all_cb = ttk.Checkbutton(all_row, variable=self.preview_use_all_groups_var) all_cb.pack(side=tk.LEFT, padx=5) self.config_vars["display_preview_use_all_groups"] = self.preview_use_all_groups_var # 保存按钮 save_frame = ttk.Frame(right_frame) save_frame.pack(fill=tk.X, pady=10) ttk.Button(save_frame, text="保存配置", command=self.save_config).pack(side=tk.LEFT, padx=5) ttk.Button(save_frame, text="启动预览", command=self.start_preview).pack(side=tk.LEFT, padx=5) # 启动程序按钮组 start_frame = ttk.Frame(right_frame) start_frame.pack(fill=tk.X, pady=5) ttk.Button(start_frame, text="启动单个配置组", command=self.start_program).pack(side=tk.LEFT, padx=5) ttk.Button(start_frame, text="启动多个配置组", command=self.start_multi_program).pack(side=tk.LEFT, padx=5) def create_entry(self, parent, key, label, prefix=None): """创建输入框""" frame = ttk.Frame(parent) frame.pack(fill=tk.X, pady=3) ttk.Label(frame, text=label, width=15).pack(side=tk.LEFT) var = tk.StringVar() entry = ttk.Entry(frame, textvariable=var, width=25) entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) # 保存变量引用 if prefix: full_key = f"{prefix}_{key}" else: full_key = key self.config_vars[full_key] = var def load_current_config(self): """加载当前配置到界面""" # 加载配置组列表 self.update_group_list() # 如果有配置组,默认选择第一个 if len(config_manager.config['groups']) > 0: self.selected_index = 0 self.group_listbox.selection_set(0) self.load_group_config(0) # 加载预览配置 display = config_manager.config.get('display', {}) for key in ['preview_width', 'preview_height', 'preview_columns', 'preview_rows']: if key in display: self.config_vars[f"display_{key}"].set(str(display[key])) # 多窗口预览 if 'preview_multi_window' in display: self.preview_multi_window_var.set(bool(display['preview_multi_window'])) # 预览全部配置组 if 'preview_use_all_groups' in display: self.preview_use_all_groups_var.set(bool(display['preview_use_all_groups'])) # 初次自动扫描一次采集卡和串口 try: self.scan_cameras() except Exception: pass try: self.scan_ports() except Exception: pass def update_group_list(self): """更新配置组列表""" self.group_listbox.delete(0, tk.END) for i, group in enumerate(config_manager.config['groups']): status = "✓" if group.get('active', False) else " " self.group_listbox.insert(tk.END, f"{status} {group['name']}") def on_group_select(self, event): """选择配置组""" selection = self.group_listbox.curselection() if selection: self.selected_index = selection[0] self.load_group_config(self.selected_index) def load_group_config(self, index): """加载配置组详情""" group = config_manager.get_group_by_index(index) if group: for key, var in self.config_vars.items(): if not key.startswith('display_'): if key in group: var.set(str(group[key])) # 确保下拉框列表与当前值一致 if group and 'camera_index' in group: # 如果当前索引不在选项里,追加 values = list(self.camera_index_cb.cget('values')) if self.camera_index_cb.cget('values') else [] display_value = f"{group['camera_index']}" if display_value not in values: values.append(display_value) self.camera_index_cb['values'] = values self.camera_index_cb.set(display_value) # 确保串口下拉框列表与当前值一致 if group and 'serial_port' in group: # 如果当前串口不在选项里,追加 values = list(self.serial_port_cb.cget('values')) if self.serial_port_cb.cget('values') else [] port_value = group['serial_port'] if port_value not in values: values.append(port_value) self.serial_port_cb['values'] = values self.serial_port_cb.set(port_value) def scan_cameras(self, max_index: int = 10): """扫描系统可用的采集卡索引,并填充下拉框""" import warnings import sys import io found = [] # 临时设置OpenCV日志级别(兼容不同版本) import os old_level = os.environ.get('OPENCV_LOG_LEVEL', '') os.environ['OPENCV_LOG_LEVEL'] = 'SILENT' # 尝试更严格的级别 os.environ['OPENCV_IO_ENABLE_OPENEXR'] = '0' # 尝试设置日志级别(不同版本的OpenCV API不同) try: if hasattr(cv2, 'setLogLevel'): # OpenCV 4.x if hasattr(cv2, 'LOG_LEVEL_SILENT'): cv2.setLogLevel(cv2.LOG_LEVEL_SILENT) elif hasattr(cv2, 'LOG_LEVEL_ERROR'): cv2.setLogLevel(cv2.LOG_LEVEL_ERROR) elif hasattr(cv2, 'utils'): cv2.utils.setLogLevel(0) # 0=SILENT/ERROR级别 except Exception: pass # 重定向stderr来捕获OpenCV的错误输出 old_stderr = sys.stderr suppressed_output = io.StringIO() try: # 暂时重定向stderr以抑制OpenCV的错误消息 sys.stderr = suppressed_output for idx in range(max_index + 1): cap = None try: with warnings.catch_warnings(): warnings.filterwarnings('ignore') # 尝试DSHOW后端 cap = cv2.VideoCapture(idx, cv2.CAP_DSHOW) if not cap.isOpened(): # 回退默认后端再试 cap = cv2.VideoCapture(idx) if cap.isOpened(): # 测试读取一帧,确保真正可用 ret, test_frame = cap.read() if ret and test_frame is not None: found.append(str(idx)) cap.release() except Exception: if cap: try: cap.release() except: pass continue finally: # 恢复stderr sys.stderr = old_stderr # 恢复原来的日志级别 if old_level: os.environ['OPENCV_LOG_LEVEL'] = old_level else: os.environ.pop('OPENCV_LOG_LEVEL', None) os.environ.pop('OPENCV_IO_ENABLE_OPENEXR', None) if not found: found = ["0"] # 至少给一个默认项,避免为空 messagebox.showwarning("扫描完成", "未发现可用采集卡,已添加默认选项 0") else: messagebox.showinfo("扫描完成", f"发现可用采集卡索引: {', '.join(found)}") self.camera_index_cb['values'] = found # 若当前无选择,则选择第一项 if not self.camera_index_var.get() and found: self.camera_index_cb.set(found[0]) def scan_ports(self): """扫描系统可用的串口,并填充下拉框""" found_real = [] try: ports = serial.tools.list_ports.comports() found_real = [port.device for port in ports] # 按端口名排序 found_real.sort(key=lambda x: int(x.replace('COM', '')) if x.replace('COM', '').isdigit() else 999) except Exception as e: print(f"扫描串口错误: {e}") # 如果没有发现实际端口,使用默认端口列表 if not found_real: found = ["COM1", "COM2", "COM3", "COM4", "COM5", "COM6"] # 默认给一些常见端口 messagebox.showwarning("警告", "未发现可用串口设备,已添加常用默认选项") else: found = found_real messagebox.showinfo("扫描完成", f"发现可用串口: {', '.join(found)}") self.serial_port_cb['values'] = found # 若当前无选择,则选择第一项 if not self.serial_port_var.get() and found: self.serial_port_cb.set(found[0]) def add_group(self): """添加配置组""" name = simpledialog.askstring("添加配置组", "请输入配置组名称:", initialvalue=f"配置组{len(config_manager.config['groups'])+1}") if name: config_manager.add_group(name) self.update_group_list() messagebox.showinfo("成功", f"已添加配置组: {name}") def delete_group(self): """删除配置组""" selection = self.group_listbox.curselection() if not selection: messagebox.showwarning("警告", "请先选择要删除的配置组") return if messagebox.askyesno("确认", "确定要删除选中的配置组吗?"): config_manager.delete_group(selection[0]) self.update_group_list() messagebox.showinfo("成功", "配置组已删除") def set_active(self): """设置为活动配置组""" selection = self.group_listbox.curselection() if not selection: messagebox.showwarning("警告", "请先选择要设为活动的配置组") return config_manager.set_active_group(selection[0]) self.update_group_list() messagebox.showinfo("成功", "已设置为活动配置组") def save_config(self): """保存当前编辑的配置""" # 检查索引有效性 if self.selected_index < 0 or self.selected_index >= len(config_manager.config['groups']): messagebox.showerror("错误", f"请先选择一个有效的配置组") return False # 保存当前组的配置 group = config_manager.get_group_by_index(self.selected_index) if not group: messagebox.showerror("错误", f"配置组不存在") return False for key, var in self.config_vars.items(): if not key.startswith('display_'): try: value_str = var.get().strip() if var.get() else "" # 特殊处理:某些字段需要转换为整数 if key in ['camera_index', 'camera_width', 'camera_height', 'serial_baudrate', 'move_velocity']: try: # 如果为空,使用当前值或默认值 if value_str: value = int(value_str) else: # 如果下拉框为空,尝试从当前配置获取 value = group.get(key, 0) except ValueError: # 转换失败,使用当前值 value = group.get(key, 0) print(f"⚠️ 字段 {key} 的值 '{value_str}' 无效,使用当前值 {value}") else: # 字符串字段 value = value_str if value_str else group.get(key, '') group[key] = value except Exception as e: print(f"保存字段 {key} 时出错: {e}") import traceback traceback.print_exc() # 保存失败时使用当前值 if key in group: pass # 保持原值不变 # 保存预览配置 display = config_manager.config.get('display', {}) for key in ['preview_width', 'preview_height', 'preview_columns', 'preview_rows']: var = self.config_vars.get(f"display_{key}") if var: try: display[key] = int(var.get()) except: pass # 保存多窗口预览布尔值 display['preview_multi_window'] = bool(self.preview_multi_window_var.get()) # 保存预览全部配置组布尔值 display['preview_use_all_groups'] = bool(self.preview_use_all_groups_var.get()) config_manager.config['display'] = display # 保存到文件 if config_manager.save_config(): # 更新左侧列表显示 self.update_group_list() messagebox.showinfo("成功", "配置已保存") return True else: messagebox.showerror("错误", "配置保存失败") return False def start_preview(self): """启动预览窗口(主线程,弹窗模式)""" if not self.save_config_silent(): messagebox.showerror("错误", "配置保存失败,无法启动预览") return config_manager.load_config() from preview import PreviewWindow PreviewWindow().run(self.root) # 以主窗口为父Toplevel弹出独立预览窗 def save_config_silent(self): """静默保存配置(不显示消息框)""" # 保存当前组的配置 if self.selected_index < 0 or self.selected_index >= len(config_manager.config['groups']): print(f"⚠️ 选中索引 {self.selected_index} 无效") return False group = config_manager.get_group_by_index(self.selected_index) if not group: print(f"⚠️ 配置组不存在") return False for key, var in self.config_vars.items(): if not key.startswith('display_'): try: value_str = var.get().strip() if var.get() else "" # 特殊处理:某些字段需要转换为整数 if key in ['camera_index', 'camera_width', 'camera_height', 'serial_baudrate', 'move_velocity']: try: # 如果为空,使用当前值或默认值 if value_str: value = int(value_str) else: # 如果下拉框为空,尝试从当前配置获取 value = group.get(key, 0) except ValueError: # 转换失败,使用当前值 value = group.get(key, 0) print(f"⚠️ 字段 {key} 的值 '{value_str}' 无效,使用当前值 {value}") else: # 字符串字段 value = value_str if value_str else group.get(key, '') group[key] = value except Exception as e: print(f"保存字段 {key} 时出错: {e}") import traceback traceback.print_exc() # 保存失败时保持原值不变 # 保存预览配置 display = config_manager.config.get('display', {}) for key in ['preview_width', 'preview_height', 'preview_columns', 'preview_rows']: var = self.config_vars.get(f"display_{key}") if var: try: display[key] = int(var.get()) except: pass # 保存多窗口预览布尔值(静默) display['preview_multi_window'] = bool(self.preview_multi_window_var.get()) # 保存预览全部配置组布尔值(静默) display['preview_use_all_groups'] = bool(self.preview_use_all_groups_var.get()) config_manager.config['display'] = display # 保存到文件 result = config_manager.save_config() # 更新左侧列表显示 if result: self.update_group_list() return result def start_program(self): """启动单个配置组的主程序""" # 保存配置(静默) if not self.save_config_silent(): messagebox.showerror("错误", "配置保存失败") return # 检查是否有活动配置组 active_groups = [g for g in config_manager.config['groups'] if g.get('active', False)] if not active_groups: messagebox.showwarning("警告", "没有活动的配置组\n\n请先选择一个配置组并设置为活动") return # 启动单个配置组 import subprocess import sys try: # 找到活动配置组的索引 active_group = active_groups[0] group_index = config_manager.config['groups'].index(active_group) subprocess.Popen([ sys.executable, "main_single.py", str(group_index) ], creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == 'win32' else 0) messagebox.showinfo("成功", f"已启动配置组: {active_group['name']}\n\n请在控制台查看运行状态") except Exception as e: messagebox.showerror("错误", f"启动失败: {e}") def start_multi_program(self): """启动多个配置组的主程序""" # 保存配置(静默) if not self.save_config_silent(): messagebox.showerror("错误", "配置保存失败") return # 启动多配置组管理器 import subprocess import sys try: subprocess.Popen([ sys.executable, "main_multi.py" ], creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == 'win32' else 0) messagebox.showinfo("提示", "多配置组启动器已打开\n\n请在控制台中选择要启动的配置组") except Exception as e: messagebox.showerror("错误", f"启动失败: {e}") def run(self): """运行GUI""" self.root.mainloop() if __name__ == "__main__": app = ConfigGUI() app.run()