#!/usr/bin/env python3 """ 主窗口 """ import customtkinter as ctk from tkinter import filedialog, messagebox import threading import yaml import logging from logging.handlers import QueueHandler import queue from pathlib import Path from gui.tabs.main_config import MainConfigTab from gui.tabs.result_preview import ResultPreviewTab from gui.tabs.hardware_control import HardwareControlTab from core.simulator import EventSimulatorV4 from core.data_writer import DataWriterV4 from utils.config_manager import ConfigManager from utils.logger import get_logger, LogLevel # 配置日志队列 log_queue = queue.Queue() class QueueListener(logging.handlers.QueueListener): def __init__(self, queue, gui_callback): self.gui_callback = gui_callback super().__init__(queue, *[]) def handle(self, record): # 调用GUI回调函数 if self.gui_callback: self.gui_callback(record) class MainWindow(ctk.CTk): def __init__(self): super().__init__() # 配置日志 self._setup_logging() self.title("探测器信号模拟器 V4.2") self.geometry("1400x900") ctk.set_appearance_mode("dark") ctk.set_default_color_theme("blue") # 初始化after任务列表 self.after_tasks = [] self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1) # 创建选项卡视图 self.tabview = ctk.CTkTabview(self) self.tabview.grid(row=0, column=0, sticky="nsew", padx=10, pady=10) self.tabview.add("硬件控制") self.tabview.add("探测器信号谱模拟") # 绑定选项卡切换事件 self.tabview.configure(command=self._on_tab_change) # 创建探测器信号谱模拟布局 main_tab = self.tabview.tab("探测器信号谱模拟") # 固定左右面板的宽度,避免布局变化 main_tab.grid_columnconfigure(0, weight=0, minsize=600) # 左侧固定宽度(增加1/4) main_tab.grid_columnconfigure(1, weight=1) # 右侧自适应 main_tab.grid_rowconfigure(0, weight=1) # 左侧配置面板(带滚动条) left_frame = ctk.CTkFrame(main_tab, fg_color="#2a2a2a") # 设置黑色背景 left_frame.grid(row=0, column=0, sticky="nsew", padx=10, pady=10) # 固定左侧面板宽度(增加1/4) left_frame.configure(width=600) left_frame.grid_propagate(False) # 创建滚动容器 canvas = ctk.CTkCanvas(left_frame, bg="#2a2a2a", highlightthickness=0) canvas.pack(fill="both", expand=True) # 不添加可见的滚动条,只保留鼠标滚轮滚动功能 # 这样可以避免滚动条显示/隐藏时影响布局 # 创建滚动内容框架 scrollable_frame = ctk.CTkFrame(canvas, fg_color="#2a2a2a") scrollable_frame.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) window_id = canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") # 调整宽度当窗口大小改变时 def _on_configure(event): canvas.itemconfig(window_id, width=left_frame.winfo_width()) left_frame.bind("", _on_configure) # 在滚动框架中添加分页控件 self.left_tabview = ctk.CTkTabview(scrollable_frame) self.left_tabview.pack(fill="both", expand=True, padx=5, pady=5) # 添加"设置"标签页 settings_tab = self.left_tabview.add("设置") # 在"设置"标签页中添加主配置选项卡 self.main_config_tab = MainConfigTab(settings_tab) self.main_config_tab.pack(fill="both", expand=True, padx=5, pady=5) # 添加"日志"标签页 log_tab = self.left_tabview.add("日志") # 配置日志标签页的布局 log_tab.grid_rowconfigure(1, weight=1) log_tab.grid_columnconfigure(0, weight=1) # ctk.CTkLabel( # log_tab, # text="生成日志", # font=ctk.CTkFont(size=14, weight="bold") # ).grid(row=0, column=0, sticky="w", padx=10, pady=5) # 创建日志文本框容器框架,用于动态调整高度 log_container = ctk.CTkFrame(log_tab, fg_color="transparent") log_container.grid(row=1, column=0, sticky="nsew", padx=10, pady=5) log_container.grid_rowconfigure(0, weight=1) log_container.grid_columnconfigure(0, weight=1) log_container.grid_propagate(False) # 防止子组件收缩框架 self.log_textbox = ctk.CTkTextbox( log_container, fg_color="#1e1e1e", text_color="white" ) self.log_textbox.grid(row=0, column=0, sticky="nsew") # 动态调整日志容器高度 def _update_log_container_height(event=None): # 计算标签页标题和其他元素占用的总高度 other_height = 60 # 标题栏和边距的估算值 # 计算可用高度 available_height = max(400, left_frame.winfo_height() - other_height ) log_container.configure(height=available_height) # 绑定到窗口大小变化事件 left_frame.bind("", lambda e: self.after(100, _update_log_container_height), add="+") # 加载默认配置 self.load_default_config() # 在所有组件创建完成后,递归绑定滚轮事件 def _on_mousewheel(event): canvas.yview_scroll(int(-1*(event.delta/120)), "units") def _bind_mousewheel(widget): try: widget.bind("", _on_mousewheel) except NotImplementedError: pass # 忽略不支持bind方法的组件 for child in widget.winfo_children(): _bind_mousewheel(child) _bind_mousewheel(scrollable_frame) # 右侧结果预览 right_frame = ctk.CTkFrame(main_tab) right_frame.grid(row=0, column=1, sticky="nsew", padx=10, pady=10) right_frame.grid_rowconfigure(0, weight=1) right_frame.grid_columnconfigure(0, weight=1) # 固定右侧面板最小宽度 right_frame.grid_propagate(False) self.result_preview_tab = ResultPreviewTab(right_frame) self.result_preview_tab.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) # 硬件控制选项卡 hardware_tab = self.tabview.tab("硬件控制") # 配置硬件控制选项卡的布局 hardware_tab.grid_columnconfigure(0, weight=1) hardware_tab.grid_rowconfigure(0, weight=1) self.hardware_control_tab = HardwareControlTab(hardware_tab) self.hardware_control_tab.grid(row=0, column=0, sticky="nsew", padx=10, pady=10) # 加载硬件控制配置 from utils.config_manager import ConfigManager config = ConfigManager.load_default_config() if config and "hardware_control" in config: self.hardware_control_tab.set_config(config["hardware_control"]) # 创建控制按钮框架 self.control_frame = ctk.CTkFrame(self) self.control_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=(0, 10)) self.control_frame.grid_columnconfigure(0, weight=1) self.control_frame.grid_columnconfigure(1, weight=0) self.control_frame.grid_columnconfigure(2, weight=0) self.control_frame.grid_columnconfigure(3, weight=0) self.control_frame.grid_columnconfigure(4, weight=0) self.control_frame.grid_columnconfigure(5, weight=0) self.control_frame.grid_columnconfigure(6, weight=0) # 状态栏 self.status_var = ctk.StringVar(value="就绪") self.status_label = ctk.CTkLabel( self.control_frame, textvariable=self.status_var, font=ctk.CTkFont(size=12) ) self.status_label.grid(row=0, column=0, sticky="w", padx=10, pady=10) # 清除DDR内存按钮(红色)- 硬件控制页面专用 self.clear_ddr_button = ctk.CTkButton( self.control_frame, text="清除DDR内存", command=self.clear_ddr_memory, width=120, fg_color=["#ff6b6b", "#c0392b"], hover_color=["#e74c3c", "#a93226"] ) self.clear_ddr_button.grid(row=0, column=1, padx=5, pady=10) # 加载配置按钮 self.load_button = ctk.CTkButton( self.control_frame, text="加载配置", command=self.load_config, width=100 ) self.load_button.grid(row=0, column=2, padx=5, pady=10) # 保存配置按钮 self.save_button = ctk.CTkButton( self.control_frame, text="保存配置", command=self.save_config, width=100 ) self.save_button.grid(row=0, column=3, padx=5, pady=10) # 生成样本空间按钮 self.generate_sample_button = ctk.CTkButton( self.control_frame, text="生成样本空间", command=self.generate_sample_space, width=120, fg_color="#3498db", hover_color="#2980b9" ) self.generate_sample_button.grid(row=0, column=4, padx=5, pady=10) # 生成探测器信号按钮(原开始模拟按钮) self.start_button = ctk.CTkButton( self.control_frame, text="生成探测器信号", command=self.start_simulation, width=140, fg_color="#2ecc71", hover_color="#27ae60" ) self.start_button.grid(row=0, column=5, padx=10, pady=10) # 停止模拟器按钮(黄色)- 硬件控制页面专用 self.stop_simulator_button = ctk.CTkButton( self.control_frame, text="停止模拟器", command=self.stop_hardware_simulator, width=120, fg_color=["#f1c40f", "#f39c12"], hover_color=["#d4ac0d", "#d68910"] ) self.stop_simulator_button.grid(row=0, column=6, padx=5, pady=10) # 初始化变量 self.simulator = None self.simulation_thread = None self.all_events = [] self.data_writer = None self.is_closing = False self.sample_spaces = {} # 存储预生成的样本空间 self.safe_after(0, self._update_result_preview_thread_safe) self.safe_after(0, lambda: self.start_button.configure(state="normal")) # 初始化当前选项卡为硬件控制 self._current_tab = "硬件控制" # 设置默认选中硬件控制选项卡 self.tabview.set("硬件控制") # 更新底部按钮 self._update_control_buttons() def _on_tab_change(self): """选项卡切换时的回调""" current_tab = self.tabview.get() self._current_tab = current_tab self._update_control_buttons() def _update_control_buttons(self): """根据当前选项卡更新控制按钮""" current_tab = self._current_tab # 隐藏所有特定按钮 self.generate_sample_button.grid_remove() self.start_button.grid_remove() self.clear_ddr_button.grid_remove() self.stop_simulator_button.grid_remove() # 根据当前选项卡显示相应的按钮 if current_tab == "探测器信号谱模拟": # 探测器信号谱模拟页面:加载配置、保存配置、生成样本空间、生成探测器信号 self.generate_sample_button.configure(text="生成样本空间", command=self.generate_sample_space) self.generate_sample_button.grid(row=0, column=4, padx=5, pady=10) self.start_button.configure(text="生成探测器信号", command=self.start_simulation) self.start_button.grid(row=0, column=5, padx=10, pady=10) elif current_tab == "硬件控制": # 硬件控制页面:清除DDR内存、加载配置、保存配置、发送模拟数据、启动模拟器、停止模拟器 self.clear_ddr_button.grid(row=0, column=1, padx=5, pady=10) self.generate_sample_button.configure(text="发送模拟数据", command=self.send_simulated_data) self.generate_sample_button.grid(row=0, column=4, padx=5, pady=10) self.start_button.configure(text="启动模拟器", command=self.start_hardware_simulator) self.start_button.grid(row=0, column=5, padx=10, pady=10) self.stop_simulator_button.grid(row=0, column=6, padx=5, pady=10) def _setup_logging(self): """配置日志""" # 获取根日志器 root_logger = logging.getLogger() root_logger.setLevel(logging.INFO) # 清除现有的处理器 for handler in root_logger.handlers[:]: root_logger.removeHandler(handler) # 添加队列处理器 queue_handler = QueueHandler(log_queue) root_logger.addHandler(queue_handler) # 启动队列监听器 self.log_listener = QueueListener(log_queue, self._handle_log_record) self.log_listener.start() def _handle_log_record(self, record): """处理日志记录""" # 格式化日志消息 import datetime current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') log_message = f"[{current_time}] {record.getMessage()}" # 在主线程中更新日志文本框 self.after(0, lambda msg=log_message: self._update_log_textbox(msg)) def _update_log_textbox(self, message): """更新日志文本框""" if hasattr(self, 'log_textbox'): self.log_textbox.insert("end", message + "\n") self.log_textbox.see("end") def update_status(self, message): """更新状态栏""" self.status_var.set(message) # 同时更新日志文本框 if hasattr(self, 'log_textbox'): # 获取当前时间 import datetime current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') # 格式化为日志条目 log_entry = f"[{current_time}] {message}\n" # 插入到日志文本框 self.log_textbox.insert("end", log_entry) # 自动滚动到底部 self.log_textbox.see("end") self.update_idletasks() def generate_sample_space(self): """生成样本空间""" # 禁用生成按钮 self.generate_sample_button.configure(state="disabled") self.update_status("正在生成样本空间...") # 获取配置 full_config = self.get_config() # 启动生成样本空间线程 self.sample_space_thread = threading.Thread(target=self._generate_sample_space_thread, args=(full_config,)) self.sample_space_thread.daemon = True self.sample_space_thread.start() def _generate_sample_space_thread(self, full_config): """在线程中生成样本空间""" try: from core.simulator import SampleSpaceGenerator sample_generator = SampleSpaceGenerator() # 从完整配置中提取探测器配置 detector_configs = full_config.get('detectors', {}) # 清空之前的样本空间 self.sample_spaces = {} # 为每个探测器生成样本空间 for detector_name, detector_config in detector_configs.items(): self.update_status(f"正在生成 {detector_name} 的样本空间...") detector_config_copy = detector_config.copy() detector_config_copy['name'] = detector_name self.sample_spaces[detector_name] = sample_generator.generate_sample_space(detector_config_copy) self.update_status("样本空间生成完成!") # 更新可视化预览 if not self.is_closing: self.safe_after(0, self._update_preview_with_sample_space) except Exception as e: self.update_status(f"生成样本空间失败: {str(e)}") print(f"生成样本空间错误: {e}") finally: # 重新启用生成按钮 if not self.is_closing: self.safe_after(0, lambda: self.generate_sample_button.configure(state="normal")) def _update_preview_with_sample_space(self): """使用生成的样本空间更新预览""" try: # 更新可视化预览 basic_config, detector_configs = self.main_config_tab.get_config() self.result_preview_tab.update_chart_with_sample_space(self.sample_spaces, detector_configs) except Exception as e: print(f"更新预览失败: {e}") def start_simulation(self): """开始模拟(生成时间序列)""" # 禁用开始按钮 self.start_button.configure(state="disabled") self.update_status("正在准备模拟...") # 获取配置(返回字典:{simulation: {}, detectors: {}, hardware_control: {}}) full_config = self.get_config() # 启动模拟线程 self.simulation_thread = threading.Thread(target=self._run_time_series_simulation, args=(full_config,)) self.simulation_thread.daemon = True self.simulation_thread.start() def _run_time_series_simulation(self, full_config): """运行时间序列模拟""" try: from core.simulator import SampleSpaceGenerator, DetectorSampler, TimeSeriesGenerator from core.data_writer import DataWriterV4 # 从完整配置中提取基本配置和探测器配置 config = full_config.get('simulation', {}) detector_configs = full_config.get('detectors', {}) # 获取序列数量 num_sequences = config.get('num_events', 1) # 使用配置中的输出文件名(如果有),否则自动生成 output_filename = config.get('output_file') # 如果没有输出文件名,自动生成 if not output_filename: work_mode = config.get('work_mode', 'CO') signal_type = config.get('signal_type', 0x0070) # 编码信号类型 signal_code = 'V' if signal_type == 0x0070 else 'C' # 编码同步脉冲 sync_pulses = config.get('sync_pulses', []) pulse_codes = [] for pulse in sync_pulses: count = pulse.get('count', 0) period = pulse.get('period', 0) if count > 0: pulse_codes.append(f"{count}x{period}") pulse_str = '_'.join(pulse_codes) if pulse_codes else '0x0' # 生成文件名: {工作模式}{信号类型}_{脉冲配置} output_filename = f"{work_mode}{signal_code}_{pulse_str}" config['output_file'] = output_filename # 更新UI中的输出文件名显示 if not self.is_closing: self.safe_after(0, lambda: self._update_output_filename_display(output_filename)) # 1. 生成样本空间 self.update_status("正在生成样本空间...") sample_generator = SampleSpaceGenerator() sample_spaces = {} samplers = {} for detector_name, detector_config in detector_configs.items(): self.update_status(f"正在生成 {detector_name} 的样本空间...") detector_config_copy = detector_config.copy() detector_config_copy['name'] = detector_name sample_spaces[detector_name] = sample_generator.generate_sample_space(detector_config_copy) # 创建采样器 allow_replacement = detector_config.get('allow_replacement', True) samplers[detector_name] = DetectorSampler(sample_spaces[detector_name], allow_replacement) self.sample_spaces = sample_spaces # 2. 生成时间序列 self.update_status("正在生成时间序列...") work_mode = config.get('work_mode', 'CO') sync_pulses = config.get('sync_pulses', []) energy_K = config.get('energy_conversion', {}).get('K', 1.0) energy_B = config.get('energy_conversion', {}).get('B', 0.0) # 获取同步脉冲探测器信号数配置 sync_pulse_signals = config.get('sync_pulse_signals', {}) time_series_generator = TimeSeriesGenerator( work_mode=work_mode, sync_pulses=sync_pulses, sample_spaces=sample_spaces, samplers=samplers, detectors=detector_configs, energy_K=energy_K, energy_B=energy_B, sync_pulse_signals=sync_pulse_signals ) # 将config包装为DataWriterV4需要的格式 writer_config = { "simulation": config, "detectors": detector_configs } data_writer = DataWriterV4(writer_config) sync_pulse1_count = sync_pulses[0]['count'] if len(sync_pulses) > 0 else 0 sync_pulse1_period = sync_pulses[0]['period'] if len(sync_pulses) > 0 else 0 sync_pulse2_count = sync_pulses[1]['count'] if len(sync_pulses) > 1 else 0 sync_pulse2_period = sync_pulses[1]['period'] if len(sync_pulses) > 1 else 0 sync_pulse3_count = sync_pulses[2]['count'] if len(sync_pulses) > 2 else 0 sync_pulse3_period = sync_pulses[2]['period'] if len(sync_pulses) > 2 else 0 sync_pulse4_count = sync_pulses[3]['count'] if len(sync_pulses) > 3 else 0 sync_pulse4_period = sync_pulses[3]['period'] if len(sync_pulses) > 3 else 0 signal_type = config.get('signal_type', 0x0070) # 生成多个序列并合并到一个文件 self.update_status(f"正在生成 {num_sequences} 个序列...") # 收集所有序列的编码信号 all_encoded_signals = [] total_duration = 0 for sequence_idx in range(num_sequences): self.update_status(f"正在生成序列 {sequence_idx + 1}/{num_sequences}...") # 生成时间序列 time_series = time_series_generator.generate_time_series() # 编码时间序列(包含事件结束标志) encoded_signals = time_series_generator.encode_time_series(time_series) # 保存最后一个时间序列用于可视化 if sequence_idx == num_sequences - 1: self.current_time_series = time_series # 将当前序列的信号添加到总列表 all_encoded_signals.extend(encoded_signals) # 累加总时长 total_duration += time_series.get('total_duration', 0) # 计算总信号数(原始数据,未补零) total_signals = len(all_encoded_signals) # 写入数据文件 self.update_status(f"正在写入数据文件...") data_writer.write_time_series_with_instructions( time_series={"detector1": [], "detector2": [], "detector3": [], "total_duration": total_duration}, output_file=output_filename + ".bin", signal_type=signal_type, simulation_mode=config.get('simulation_mode', 'CO'), sync_pulse1_count=sync_pulse1_count, sync_pulse1_period=sync_pulse1_period, sync_pulse2_count=sync_pulse2_count, sync_pulse2_period=sync_pulse2_period, sync_pulse3_count=sync_pulse3_count, sync_pulse3_period=sync_pulse3_period, sync_pulse4_count=sync_pulse4_count, sync_pulse4_period=sync_pulse4_period, encoded_signals=all_encoded_signals ) # 更新结果预览 if not self.is_closing: self.safe_after(0, self._update_time_series_preview_thread_safe) self.update_status(f"模拟完成! 共生成 {num_sequences} 个序列") except Exception as e: self.update_status(f"模拟失败: {str(e)}") print(f"模拟错误: {e}") import traceback traceback.print_exc() finally: # 重新启用开始按钮 if not self.is_closing: self.safe_after(0, self.enable_start_button) def _update_time_series_preview_thread_safe(self): """线程安全地更新时间序列预览(确保在主线程中执行)""" try: self._update_time_series_preview() except Exception as e: print(f"更新时间序列预览失败: {e}") def _update_time_series_preview(self): """更新时间序列预览""" # 获取时间序列数据和探测器配置 time_series = getattr(self, 'current_time_series', None) basic_config, detector_configs = self.main_config_tab.get_config() if time_series: # 更新图表 - 传入时间序列数据和探测器配置 self.result_preview_tab.update_chart_with_time_series(time_series, detector_configs) # 计算并输出统计信息到日志 total_signals = sum(len(time_series[det]) for det in ["detector1", "detector2", "detector3"]) self.update_status(f"时间序列生成完成,总信号数: {total_signals}") # 验证文件并输出到日志 output_file = basic_config["output_file"] + ".bin" if Path(output_file).exists(): size = Path(output_file).stat().st_size self.update_status(f"输出文件已生成: {output_file} ({size} 字节)") def _update_result_preview_thread_safe(self): """线程安全地更新结果预览(确保在主线程中执行)""" try: self.update_result_preview() except Exception as e: print(f"更新结果预览失败: {e}") def update_result_preview(self): """更新结果预览""" if not self.all_events: return # 计算统计信息 total_events = len(self.all_events) total_signals = sum(len(event) for event in self.all_events) valid_signals = sum(sum(1 for sig in event if sig != 0) for event in self.all_events) avg_signals = total_signals / total_events if total_events > 0 else 0 valid_rate = (valid_signals / total_signals * 100) if total_signals > 0 else 0 # 将统计信息输出到日志 stats_message = f"统计信息: 总事件数={total_events}, 总信号数={total_signals}, 有效信号数={valid_signals}, 平均每事件={avg_signals:.1f}个信号, 有效率={valid_rate:.1f}%" self.update_status(stats_message) # 更新图表 _, detector_configs = self.main_config_tab.get_config() self.result_preview_tab.update_chart(self.all_events, detector_configs) # 验证文件并更新左侧面板文件列表 basic_config, _ = self.main_config_tab.get_config() output_file = basic_config["output_file"] files = self.result_preview_tab.verify_files(output_file) # 更新左侧面板文件列表 self.file_list.configure(state="normal") self.file_list.delete(1.0, "end") for file_info in files: filename = file_info.get("name", "") size = file_info.get("size", 0) status = "✓" if file_info.get("exists", False) else "✗" self.file_list.insert("end", f"{status} {filename} ({size} 字节)\n") self.file_list.configure(state="disabled") def load_config(self): """加载配置文件""" filename = filedialog.askopenfilename( filetypes=[("YAML files", "*.yaml"), ("YAML files", "*.yml"), ("All files", "*.*")] ) if filename: try: config = ConfigManager.load_config(filename) self.set_config(config) self.update_status(f"配置已加载: {filename}") except Exception as e: self.update_status(f"加载配置失败: {str(e)}") def save_config(self): """保存配置文件""" filename = filedialog.asksaveasfilename( defaultextension=".yaml", filetypes=[("YAML files", "*.yaml"), ("YAML files", "*.yml"), ("All files", "*.*")] ) if filename: try: config = self.get_config() ConfigManager.save_config(config, filename) self.update_status(f"配置已保存: {filename}") except Exception as e: self.update_status(f"保存配置失败: {str(e)}") def get_config(self): """获取当前配置""" basic_config, detector_configs = self.main_config_tab.get_config() config = { "simulation": basic_config, "detectors": detector_configs } # 添加硬件控制配置 if hasattr(self, 'hardware_control_tab') and self.hardware_control_tab: config["hardware_control"] = self.hardware_control_tab.get_config() return config def send_simulated_data(self): """发送模拟数据(硬件控制页面)""" if hasattr(self, 'hardware_control_tab') and self.hardware_control_tab: # 调用硬件控制选项卡的发送数据方法 self.hardware_control_tab.send_data() else: self.update_status("硬件控制选项卡未初始化") def start_hardware_simulator(self): """启动模拟器(硬件控制页面)""" if hasattr(self, 'hardware_control_tab') and self.hardware_control_tab: # 连接到设备并启动 if not self.hardware_control_tab.is_connected: self.hardware_control_tab.connect_to_device() if self.hardware_control_tab.is_connected: self.hardware_control_tab.start_device() self.update_status("模拟器已启动") else: self.update_status("硬件控制选项卡未初始化") def stop_hardware_simulator(self): """停止模拟器(硬件控制页面)""" if hasattr(self, 'hardware_control_tab') and self.hardware_control_tab: if self.hardware_control_tab.is_connected: self.hardware_control_tab.stop_device() self.update_status("模拟器已停止") else: self.update_status("硬件控制选项卡未初始化") def clear_ddr_memory(self): """清除DDR内存(硬件控制页面)""" if hasattr(self, 'hardware_control_tab') and self.hardware_control_tab: if self.hardware_control_tab.is_connected: self.hardware_control_tab.clear_ddr_memory() self.update_status("DDR内存已清除") else: self.update_status("请先连接设备") else: self.update_status("硬件控制选项卡未初始化") def safe_after(self, ms, func): """安全的after方法,记录任务ID""" if self.is_closing: return def wrapper(): if not self.is_closing: try: func() except Exception as e: print(f"执行after任务失败: {e}") # 从任务列表中删除 if aid in self.after_tasks: self.after_tasks.remove(aid) aid = self.after(ms, wrapper) self.after_tasks.append(aid) return aid def enable_start_button(self): """安全地启用开始按钮""" if not self.is_closing and self.winfo_exists(): self.start_button.configure(state="normal") def _update_output_filename_display(self, filename): """更新输出文件名显示""" if hasattr(self, 'main_config_tab') and hasattr(self.main_config_tab, 'output_file_var'): self.main_config_tab.output_file_var.set(filename) def set_config(self, config): """设置配置""" simulation_config = config.get("simulation", config) detector_configs = config.get("detectors", {}) self.main_config_tab.set_config(simulation_config, detector_configs) def load_default_config(self): """加载默认配置""" from utils.config_manager import ConfigManager config = ConfigManager.load_default_config() if config: # 恢复窗口位置和大小 if "window" in config: window_config = config["window"] if "x" in window_config and "y" in window_config: self.geometry(f"{window_config.get('width', 1400)}x{window_config.get('height', 900)}+{window_config['x']}+{window_config['y']}") else: self.geometry(f"{window_config.get('width', 1400)}x{window_config.get('height', 900)}") # 恢复窗口最大化状态(使用after延迟,确保窗口完全初始化) if window_config.get("maximized", False): def _maximize_window(): try: self.state("zoomed") # Windows系统 except: try: self.attributes("-zoomed", True) # 其他系统 except: pass self.after(100, _maximize_window) # 恢复模拟配置 if "simulation" in config or "detectors" in config: simulation_config = config.get("simulation", {}) detector_configs = config.get("detectors", {}) self.main_config_tab.set_config(simulation_config, detector_configs) # 恢复硬件控制配置 if "hardware_control" in config: hardware_config = config.get("hardware_control", {}) if hasattr(self, 'hardware_control_tab') and self.hardware_control_tab: self.hardware_control_tab.set_config(hardware_config) def on_closing(self): """窗口关闭时的处理""" self.is_closing = True # 停止状态栏的时间更新 if hasattr(self, 'status_bar'): self.status_bar.stop() # 取消所有after任务 for aid in self.after_tasks: try: self.after_cancel(aid) except Exception as e: print(f"取消 after 任务失败: {e}") # 保存默认配置 try: self.save_default_config() except Exception as e: print(f"保存默认配置失败: {e}") # 立即销毁窗口 self.destroy() def save_default_config(self): """保存默认配置""" from utils.config_manager import ConfigManager print("开始保存默认配置...") # 获取当前配置 config = self.get_config() print(f"获取到配置: {config.keys()}") # 添加窗口位置和大小 geometry = self.geometry() print(f"窗口几何信息: {geometry}") if geometry: # 解析几何信息: widthxheight+x+y parts = geometry.split('+') size_part = parts[0] width, height = map(int, size_part.split('x')) x, y = 0, 0 if len(parts) > 1: x = int(parts[1]) if len(parts) > 2: y = int(parts[2]) # 检查窗口是否最大化 is_maximized = False try: # 尝试获取窗口状态 is_maximized = self.state() == "zoomed" # Windows系统 except: try: is_maximized = self.attributes("-zoomed") # 其他系统 except: pass config["window"] = { "width": width, "height": height, "x": x, "y": y, "maximized": is_maximized } print(f"添加窗口配置: {config['window']}") # 保存默认配置 try: print(f"保存配置到: {ConfigManager.DEFAULT_CONFIG_FILE}") ConfigManager.save_default_config(config) print("保存默认配置成功") except Exception as e: print(f"保存默认配置失败: {e}") if __name__ == "__main__": app = MainWindow() app.protocol("WM_DELETE_WINDOW", app.on_closing) try: app.mainloop() except Exception as e: print(f"主循环发生错误: {e}")