"""OrganicLab v3.0 - 完整的日志和管理系统(修正版)包含装饰器、验证器、日志管理、错误处理、完整渲染器"""import tkinter as tkfrom tkinter import ttk, messagebox, scrolledtextimport mathimport functoolsimport timeimport tracebackimport loggingimport logging.handlersimport jsonimport osfrom dataclasses import dataclass, field, asdictfrom typing import List, Dict, Tuple, Callable, Optional, Any, Unionfrom datetime import datetimefrom enum import Enumfrom pathlib import Pathimport threadingfrom abc import ABC, abstractmethodtry: import numpy as npexcept ImportError: print("请安装 numpy: pip install numpy") import sys sys.exit(1)try: import matplotlib matplotlib.use('TkAgg') from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figureexcept ImportError: print("请安装 matplotlib: pip install matplotlib") import sys sys.exit(1)# ============================================================================# 1. 配置管理系统# ============================================================================@dataclassclass AppConfig: """应用配置""" log_level: str = "INFO" log_file: str = "organiclab.log" log_max_bytes: int = 10 * 1024 * 1024 log_backup_count: int = 5 log_format: str = "%(asctime)s [%(levelname)s] %(name)s: %(message)s" max_atoms: int = 200 max_bond_length: float = 3.0 min_bond_length: float = 0.5 max_coordinate: float = 20.0 window_width: int = 1200 window_height: int = 700 auto_load_default: bool = True default_molecule: str = "苯" config_dir: Path = field(default_factory=lambda: Path.home() / ".organiclab") def save(self): self.config_dir.mkdir(parents=True, exist_ok=True) config_file = self.config_dir / "config.json" with open(config_file, 'w', encoding='utf-8') as f: json.dump(asdict(self), f, indent=2, default=str) @classmethod def load(cls) -> 'AppConfig': config = cls() config_file = config.config_dir / "config.json" if config_file.exists(): with open(config_file, 'r', encoding='utf-8') as f: data = json.load(f) for key, value in data.items(): if hasattr(config, key): setattr(config, key, value) return config# ============================================================================# 2. 日志管理系统# ============================================================================class LogLevel(Enum): DEBUG = "DEBUG" INFO = "INFO" WARNING = "WARNING" ERROR = "ERROR" CRITICAL = "CRITICAL"class LogManager: """单例日志管理器""" _instance = None _lock = threading.Lock() def __new__(cls): if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._initialized = False return cls._instance def __init__(self): if self._initialized: return self.config = AppConfig.load() self.logger = logging.getLogger("OrganicLab") self.logger.setLevel(getattr(logging, self.config.log_level)) self.logger.handlers.clear() file_handler = logging.handlers.RotatingFileHandler( self.config.log_file, maxBytes=self.config.log_max_bytes, backupCount=self.config.log_backup_count, encoding='utf-8' ) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(logging.Formatter( self.config.log_format, datefmt='%Y-%m-%d %H:%M:%S' )) console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) console_handler.setFormatter(logging.Formatter( '%(asctime)s [%(levelname)s] %(message)s', datefmt='%H:%M:%S' )) self.logger.addHandler(file_handler) self.logger.addHandler(console_handler) self.log_buffer: List[Dict[str, Any]] = [] self.max_buffer_size = 1000 self._initialized = True def log(self, level: LogLevel, message: str, *args, **kwargs): log_func = getattr(self.logger, level.value.lower()) log_func(message, *args) entry = { 'timestamp': datetime.now(), 'level': level.value, 'message': message % args if args else message } self.log_buffer.append(entry) if len(self.log_buffer) > self.max_buffer_size: self.log_buffer = self.log_buffer[-self.max_buffer_size:] def debug(self, message: str, *args): self.log(LogLevel.DEBUG, message, *args) def info(self, message: str, *args): self.log(LogLevel.INFO, message, *args) def warning(self, message: str, *args): self.log(LogLevel.WARNING, message, *args) def error(self, message: str, *args): self.log(LogLevel.ERROR, message, *args) def critical(self, message: str, *args): self.log(LogLevel.CRITICAL, message, *args) def get_recent_logs(self, count: int = 100, level: Optional[str] = None) -> List[Dict]: logs = self.log_buffer[-count:] if level: logs = [l for l in logs if l['level'] == level] return logs def clear_buffer(self): self.log_buffer.clear()# ============================================================================# 3. 装饰器系统# ============================================================================class DecoratorManager: """装饰器工厂""" @staticmethod def validate_output(func): """输出验证装饰器""" @functools.wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) if hasattr(result, 'atoms_3d') and hasattr(result, 'bonds'): DecoratorManager._validate_molecule_data(result) elif isinstance(result, tuple) and len(result) >= 4: prob = result[3] if hasattr(prob, 'min'): assert not np.any(np.isnan(prob)), "NaN in probability density" assert not np.any(np.isinf(prob)), "Inf in probability density" assert prob.min() >= -1e-10, f"Negative probability density: {prob.min()}" valid = prob[prob > 0] if len(valid) > 0: log_range = np.log10(valid.max() + 1e-12) - np.log10(valid.min() + 1e-12) if log_range > 15: LogManager().warning( f"Large dynamic range ({log_range:.1f} orders) in {func.__name__}" ) return result return wrapper @staticmethod def _validate_molecule_data(molecule): """验证分子数据""" logger = LogManager() config = AppConfig.load() if len(molecule.atoms_3d) > config.max_atoms: logger.warning(f"原子数量 ({len(molecule.atoms_3d)}) 超过推荐值 ({config.max_atoms})") for atom in molecule.atoms_3d: if abs(atom.x) > config.max_coordinate or abs(atom.y) > config.max_coordinate: logger.warning(f"原子 {atom.label} 坐标超出范围: ({atom.x:.2f}, {atom.y:.2f})") for i, j in molecule.bonds: if i < len(molecule.atoms_3d) and j < len(molecule.atoms_3d): a1 = molecule.atoms_3d[i] a2 = molecule.atoms_3d[j] dist = math.sqrt((a1.x-a2.x)**2 + (a1.y-a2.y)**2 + (a1.z-a2.z)**2) if dist > config.max_bond_length: logger.warning(f"键长过大: {a1.label}-{a2.label} = {dist:.2f}Å") elif dist < config.min_bond_length: logger.warning(f"键长过小: {a1.label}-{a2.label} = {dist:.3f}Å") @staticmethod def log_execution(func): """执行日志装饰器""" @functools.wraps(func) def wrapper(*args, **kwargs): logger = LogManager() logger.debug(f"调用: {func.__name__}") start_time = time.time() try: result = func(*args, **kwargs) elapsed = time.time() - start_time logger.debug(f"完成: {func.__name__}() 耗时 {elapsed:.3f}s") return result except Exception as e: elapsed = time.time() - start_time logger.error(f"失败: {func.__name__}() 耗时 {elapsed:.3f}s - {str(e)}") raise return wrapper @staticmethod def retry_on_failure(max_retries: int = 3, delay: float = 0.5): """失败重试装饰器""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): logger = LogManager() last_exception = None for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception as e: last_exception = e if attempt < max_retries - 1: logger.warning( f"重试 {func.__name__}() - 第 {attempt + 1}/{max_retries} 次: {str(e)}" ) time.sleep(delay) logger.error(f"重试耗尽: {func.__name__}() - {str(last_exception)}") raise last_exception return wrapper return decorator @staticmethod def time_execution(func): """性能计时装饰器""" @functools.wraps(func) def wrapper(*args, **kwargs): start_time = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start_time if elapsed > 1.0: LogManager().warning(f"性能警告: {func.__name__}() 耗时 {elapsed:.2f}s") return result return wrapper# ============================================================================# 4. 分子数据验证器# ============================================================================class MoleculeValidator: """分子数据完整性验证器""" def __init__(self): self.logger = LogManager() self.warnings: List[str] = [] self.errors: List[str] = [] def validate(self, molecule: 'MoleculeData') -> Tuple[bool, List[str], List[str]]: self.warnings.clear() self.errors.clear() self._check_atoms(molecule) self._check_bonds(molecule) self._check_coordinates(molecule) self._check_charge(molecule) self._check_consistency(molecule) is_valid = len(self.errors) == 0 return is_valid, self.errors, self.warnings def _check_atoms(self, molecule): if not molecule.atoms_3d: self.errors.append("没有原子数据") return labels = [a.label for a in molecule.atoms_3d] if len(labels) != len(set(labels)): self.errors.append("原子标签不唯一") valid_elements = {'C', 'H', 'O', 'N', 'S', 'P', 'Cl', 'Br', 'F', 'I'} for atom in molecule.atoms_3d: if atom.element not in valid_elements: self.warnings.append(f"未知元素类型: {atom.element}") def _check_bonds(self, molecule): if not molecule.bonds: self.warnings.append("没有化学键数据") return for i, j in molecule.bonds: max_idx = len(molecule.atoms_3d) if i >= max_idx or j >= max_idx: self.errors.append(f"键索引超出范围: ({i}, {j})") if i == j: self.errors.append(f"原子不能与自己成键: {i}") def _check_coordinates(self, molecule): config = AppConfig.load() for atom in molecule.atoms_3d: if math.isnan(atom.x) or math.isnan(atom.y) or math.isnan(atom.z): self.errors.append(f"原子 {atom.label} 坐标为 NaN") if math.isinf(atom.x) or math.isinf(atom.y) or math.isinf(atom.z): self.errors.append(f"原子 {atom.label} 坐标为 Inf") if abs(atom.x) > config.max_coordinate: self.warnings.append(f"原子 {atom.label} X 坐标异常: {atom.x:.2f}") def _check_charge(self, molecule): total_electrons = sum( 6 if a.element == 'C' else 1 if a.element == 'H' else 8 if a.element == 'O' else 7 if a.element == 'N' else 0 for a in molecule.atoms_3d ) if total_electrons % 2 != 0: self.warnings.append("总电子数为奇数,可能存在自由基") def _check_consistency(self, molecule): if len(molecule.atoms_2d) != len(molecule.atoms_3d): self.warnings.append("2D 和 3D 原子数量不一致")# ============================================================================# 5. 状态管理# ============================================================================class AppState(Enum): INIT = "初始化" READY = "就绪" LOADING = "加载中" RENDERING = "渲染中" ERROR = "错误"class StateManager: """应用状态管理器""" _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._state = AppState.INIT cls._instance._state_history: List[Tuple[AppState, datetime]] = [] cls._instance._error_count = 0 return cls._instance @property def state(self) -> AppState: return self._state def transition(self, new_state: AppState, message: str = ""): old_state = self._state self._state = new_state self._state_history.append((new_state, datetime.now())) if new_state == AppState.ERROR: self._error_count += 1 logger = LogManager() logger.info(f"状态转换: {old_state.value} -> {new_state.value}{message}") def get_error_count(self) -> int: return self._error_count def get_uptime(self) -> float: if self._state_history: start_time = self._state_history[0][1] return (datetime.now() - start_time).total_seconds() return 0.0# ============================================================================# 6. 数据层# ============================================================================@dataclassclass AtomData: """单个原子的数据""" x: float y: float z: float element: str label: str charge: float = 0.0 mass: float = 12.0@dataclassclass MoleculeData: """分子的完整数据对象""" name: str name_en: str formula: str description: str atoms_3d: List[AtomData] = field(default_factory=list) atoms_2d: List[Dict] = field(default_factory=list) bonds: List[Tuple[int, int]] = field(default_factory=list) reactions: Dict[str, str] = field(default_factory=dict) delocalized_indices: Optional[List[int]] = None atom_colors: Dict[str, str] = field(default_factory=lambda: { 'C': '#808080', 'H': '#C0C0C0' }) _validation_errors: List[str] = field(default_factory=list) _validation_warnings: List[str] = field(default_factory=list) def validate(self) -> bool: validator = MoleculeValidator() is_valid, errors, warnings = validator.validate(self) self._validation_errors = errors self._validation_warnings = warnings if errors: LogManager().error(f"分子 {self.name} 验证失败: {', '.join(errors)}") if warnings: LogManager().warning(f"分子 {self.name} 验证警告: {', '.join(warnings)}") return is_valid def get_center(self) -> Tuple[float, float, float]: if not self.atoms_3d: return (0, 0, 0) xs = [a.x for a in self.atoms_3d] ys = [a.y for a in self.atoms_3d] zs = [a.z for a in self.atoms_3d] return (sum(xs)/len(xs), sum(ys)/len(ys), sum(zs)/len(zs))# ============================================================================# 7. 分子注册表(修正版 - 使用类方法)# ============================================================================class MoleculeRegistry: """分子注册表 - 使用类方法,无需实例化""" _molecules: Dict[str, MoleculeData] = {} _builders: Dict[str, Callable[[], MoleculeData]] = {} @classmethod def register(cls, name: str, builder: Callable[[], MoleculeData]): """注册分子构建器""" logger = LogManager() cls._builders[name] = builder logger.debug(f"注册分子: {name}") @classmethod def get(cls, name: str) -> Optional[MoleculeData]: """获取分子数据(延迟构建)""" if name not in cls._molecules: if name in cls._builders: logger = LogManager() logger.info(f"首次加载分子: {name}") cls._molecules[name] = cls._builders[name]() else: return None return cls._molecules[name] @classmethod def get_all_names(cls) -> List[str]: """获取所有注册的分子名称""" return list(cls._builders.keys()) @classmethod def clear_cache(cls): """清除缓存的分子数据""" cls._molecules.clear() LogManager().info("分子缓存已清除")# ============================================================================# 8. 分子构建器# ============================================================================class MoleculeBuilder: """分子构建器""" @staticmethod @DecoratorManager.validate_output @DecoratorManager.log_execution @DecoratorManager.time_execution def build_benzene() -> MoleculeData: """构建苯环""" logger = LogManager() logger.info("开始构建苯环分子") RING_RADIUS = 1.40 CH_BOND = 1.09 atoms_3d = [] atoms_2d = [] # 6个碳原子 for i in range(6): angle = i * math.pi / 3 x = RING_RADIUS * math.cos(angle) y = RING_RADIUS * math.sin(angle) atoms_3d.append(AtomData(x, y, 0.0, 'C', f'C{i+1}')) atoms_2d.append({'x': x, 'y': y, 'element': 'C', 'label': f'C{i+1}'}) # 6个氢原子 for i in range(6): c_atom = atoms_3d[i] angle = i * math.pi / 3 hx = c_atom.x + CH_BOND * math.cos(angle) hy = c_atom.y + CH_BOND * math.sin(angle) atoms_3d.append(AtomData(hx, hy, 0.0, 'H', f'H{i+1}')) atoms_2d.append({'x': hx, 'y': hy, 'element': 'H', 'label': f'H{i+1}'}) # 键连关系 bonds = [] for i in range(6): bonds.append((i, (i+1) % 6)) # C-C for i in range(6): bonds.append((i, i+6)) # C-H molecule = MoleculeData( name="苯", name_en="Benzene", formula="C₆H₆", description="平面六元环,具有芳香性", atoms_3d=atoms_3d, atoms_2d=atoms_2d, bonds=bonds, delocalized_indices=list(range(6)), reactions={ "卤化反应": "C₆H₆ + X₂ → C₆H₅X + HX (催化剂: FeX₃)", "硝化反应": "C₆H₆ + HNO₃ → C₆H₅NO₂ + H₂O (催化剂: H₂SO₄)", "磺化反应": "C₆H₆ + H₂SO₄ → C₆H₅SO₃H + H₂O", } ) molecule.validate() logger.info(f"苯环构建完成,原子数: {len(atoms_3d)}, 键数: {len(bonds)}") return molecule @staticmethod @DecoratorManager.validate_output @DecoratorManager.log_execution @DecoratorManager.time_execution def build_toluene() -> MoleculeData: """构建甲苯""" logger = LogManager() logger.info("开始构建甲苯分子") benzene = MoleculeBuilder.build_benzene() # 用甲基替换一个氢 target_idx = 6 # 第一个氢的索引 c1 = benzene.atoms_3d[0] h1 = benzene.atoms_3d[6] # 甲基碳位置(沿 C1-H1 方向延伸) dx = c1.x - h1.x dy = c1.y - h1.y dist = math.hypot(dx, dy) if dist > 0: dx /= dist dy /= dist ch3_x = c1.x + dx * 1.54 ch3_y = c1.y + dy * 1.54 # 替换氢为甲基碳 benzene.atoms_3d[6] = AtomData(ch3_x, ch3_y, 0.0, 'C', 'CH₃') benzene.atoms_2d[6] = {'x': ch3_x, 'y': ch3_y, 'element': 'C', 'label': 'CH₃'} # 更新键连关系 benzene.bonds = [b for b in benzene.bonds if b != (0, 6)] benzene.bonds.append((0, 6)) benzene.name = "甲苯" benzene.name_en = "Toluene" benzene.formula = "C₆H₅-CH₃" benzene.description = "苯环上有一个甲基取代" benzene.delocalized_indices = list(range(6)) benzene.reactions = { "侧链氯化": "C₆H₅CH₃ + Cl₂ → C₆H₅CH₂Cl + HCl (光照)", "氧化反应": "C₆H₅CH₃ + KMnO₄ → C₆H₅COOH", } benzene.validate() logger.info(f"甲苯构建完成") return benzene# ============================================================================# 9. 渲染器# ============================================================================class BaseRenderer(ABC): """渲染器抽象基类""" @abstractmethod def render(self, molecule: MoleculeData): passclass Canvas2DRenderer(BaseRenderer): """通用的 2D Canvas 渲染器""" def __init__(self, canvas: tk.Canvas): self.canvas = canvas self.scale = 50 self.atom_radius = 15 def render(self, molecule: MoleculeData): """通用渲染方法""" self.canvas.delete("all") if not molecule.atoms_2d: return # 计算画布中心 canvas_w = self.canvas.winfo_width() or 400 canvas_h = self.canvas.winfo_height() or 400 cx, cy = canvas_w // 2, canvas_h // 2 # 绘制化学键 for i, j in molecule.bonds: if i < len(molecule.atoms_2d) and j < len(molecule.atoms_2d): a1 = molecule.atoms_2d[i] a2 = molecule.atoms_2d[j] x1 = cx + a1['x'] * self.scale y1 = cy - a1['y'] * self.scale x2 = cx + a2['x'] * self.scale y2 = cy - a2['y'] * self.scale self.canvas.create_line(x1, y1, x2, y2, width=3, fill='#333') # 绘制离域大π键 if molecule.delocalized_indices: self._draw_delocalized(cx, cy, molecule) # 绘制原子 for atom in molecule.atoms_2d: x = cx + atom['x'] * self.scale y = cy - atom['y'] * self.scale color = molecule.atom_colors.get(atom['element'], '#808080') self.canvas.create_oval( x - self.atom_radius, y - self.atom_radius, x + self.atom_radius, y + self.atom_radius, fill=color, outline='#333', width=2 ) self.canvas.create_text( x, y, text=atom['label'], font=('Arial', 10, 'bold'), fill='white' ) # 标题 self.canvas.create_text( cx, 30, text=f"{molecule.name} ({molecule.name_en}) - {molecule.formula}", font=('Arial', 14, 'bold') ) def _draw_delocalized(self, cx: int, cy: int, molecule: MoleculeData): """绘制离域大π键标记""" if not molecule.delocalized_indices: return points = [] for idx in molecule.delocalized_indices: if idx < len(molecule.atoms_2d): atom = molecule.atoms_2d[idx] points.append(( cx + atom['x'] * self.scale, cy - atom['y'] * self.scale )) if len(points) >= 3: avg_x = sum(p[0] for p in points) / len(points) avg_y = sum(p[1] for p in points) / len(points) r = math.hypot(points[0][0] - avg_x, points[0][1] - avg_y) self.canvas.create_oval( avg_x - r, avg_y - r, avg_x + r, avg_y + r, width=2, outline='#666', dash=(5, 5) )class Matplotlib3DRenderer(BaseRenderer): """通用的 3D 渲染器""" def __init__(self, parent_frame): self.figure = Figure(figsize=(5, 5), dpi=90, facecolor='#1a1a2e') self.ax = self.figure.add_subplot(111, projection='3d') self.canvas = FigureCanvasTkAgg(self.figure, master=parent_frame) self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) self.ax.set_facecolor('#1a1a2e') self.ax.grid(True, alpha=0.3, color='white') def render(self, molecule: MoleculeData): """通用渲染方法""" self.ax.clear() self.ax.set_facecolor('#1a1a2e') # 绘制化学键 for i, j in molecule.bonds: if i < len(molecule.atoms_3d) and j < len(molecule.atoms_3d): a1 = molecule.atoms_3d[i] a2 = molecule.atoms_3d[j] self.ax.plot( [a1.x, a2.x], [a1.y, a2.y], [a1.z, a2.z], 'w-', linewidth=2, alpha=0.6 ) # 绘制原子 for atom in molecule.atoms_3d: color = molecule.atom_colors.get(atom.element, '#808080') size = 300 if atom.element == 'C' else 200 self.ax.scatter( atom.x, atom.y, atom.z, s=size, c=color, edgecolors='black', linewidth=1, alpha=0.9, depthshade=True ) self.ax.text( atom.x, atom.y, atom.z + 0.15, atom.element, color='white', fontsize=10, ha='center', fontweight='bold' ) # 设置视图范围 center = molecule.get_center() all_atoms = molecule.atoms_3d max_range = max( max(abs(a.x - center[0]) for a in all_atoms), max(abs(a.y - center[1]) for a in all_atoms), max(abs(a.z - center[2]) for a in all_atoms) ) * 1.8 self.ax.set_xlim(center[0] - max_range, center[0] + max_range) self.ax.set_ylim(center[1] - max_range, center[1] + max_range) self.ax.set_zlim(center[2] - max_range, center[2] + max_range) title = f"{molecule.name} ({molecule.name_en})\n{molecule.formula}" self.ax.set_title(title, color='white', fontsize=12, pad=20) self.ax.set_xlabel('X (Å)', color='white') self.ax.set_ylabel('Y (Å)', color='white') self.ax.set_zlabel('Z (Å)', color='white') self.ax.tick_params(colors='white') self.canvas.draw()# ============================================================================# 10. GUI 组件# ============================================================================class LogViewerWidget(ttk.Frame): """日志查看器组件""" def __init__(self, parent, *args, **kwargs): super().__init__(parent, *args, **kwargs) self.logger = LogManager() self._build_ui() self._update_display() def _build_ui(self): control_frame = ttk.Frame(self) control_frame.pack(fill=tk.X, pady=2) ttk.Label(control_frame, text="📋 操作日志").pack(side=tk.LEFT) self.level_var = tk.StringVar(value="ALL") level_combo = ttk.Combobox( control_frame, textvariable=self.level_var, values=["ALL", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], state='readonly', width=10 ) level_combo.pack(side=tk.RIGHT, padx=2) level_combo.bind('<<ComboboxSelected>>', lambda e: self._update_display()) ttk.Button( control_frame, text="清除", command=self._clear_logs ).pack(side=tk.RIGHT, padx=2) self.log_text = scrolledtext.ScrolledText( self, height=8, bg='#1e1e2e', fg='#00ff00', font=('Consolas', 9), wrap=tk.WORD ) self.log_text.pack(fill=tk.BOTH, expand=True) self.log_text.tag_config('DEBUG', foreground='#888888') self.log_text.tag_config('INFO', foreground='#00ff00') self.log_text.tag_config('WARNING', foreground='#ffaa00') self.log_text.tag_config('ERROR', foreground='#ff4444') self.log_text.tag_config('CRITICAL', foreground='#ff0000', font=('Consolas', 9, 'bold')) self._schedule_update() def _update_display(self): level_filter = self.level_var.get() if level_filter == "ALL": level_filter = None logs = self.logger.get_recent_logs(100, level_filter) self.log_text.config(state='normal') self.log_text.delete(1.0, tk.END) for entry in logs: timestamp = entry['timestamp'].strftime('%H:%M:%S') line = f"[{timestamp}] {entry['level']:8}{entry['message']}\n" self.log_text.insert(tk.END, line, entry['level']) self.log_text.see(tk.END) self.log_text.config(state='disabled') def _clear_logs(self): self.logger.clear_buffer() self._update_display() def _schedule_update(self): self._update_display() self.after(2000, self._schedule_update)class StatusPanel(ttk.Frame): """状态监控面板""" def __init__(self, parent, *args, **kwargs): super().__init__(parent, *args, **kwargs) self.state_manager = StateManager() self.logger = LogManager() self._build_ui() self._update_status() def _build_ui(self): ttk.Label(self, text="📊 系统状态", font=('Arial', 10, 'bold')).pack(anchor='w', pady=5) self.status_frame = ttk.Frame(self) self.status_frame.pack(fill=tk.X, pady=5) self.status_labels = {} status_items = [ ("应用状态:", AppState.READY.value), ("运行时间:", "0.0s"), ("错误计数:", "0"), ("加载分子:", "无"), ("内存日志:", "0条"), ] for i, (label, value) in enumerate(status_items): ttk.Label(self.status_frame, text=label).grid(row=i, column=0, sticky='w', padx=5) value_label = ttk.Label(self.status_frame, text=value) value_label.grid(row=i, column=1, sticky='w', padx=5) self.status_labels[label] = value_label def _update_status(self): try: self.status_labels["应用状态:"].config( text=self.state_manager.state.value, foreground=self._get_state_color(self.state_manager.state) ) uptime = self.state_manager.get_uptime() self.status_labels["运行时间:"].config(text=f"{uptime:.1f}s") self.status_labels["错误计数:"].config( text=str(self.state_manager.get_error_count()) ) log_count = len(self.logger.log_buffer) self.status_labels["内存日志:"].config(text=f"{log_count}条") except Exception: pass self.after(1000, self._update_status) def _get_state_color(self, state: AppState) -> str: colors = { AppState.INIT: 'gray', AppState.READY: 'green', AppState.LOADING: 'blue', AppState.RENDERING: 'orange', AppState.ERROR: 'red', } return colors.get(state, 'black') def update_molecule(self, name: str): self.status_labels["加载分子:"].config(text=name)# ============================================================================# 11. 主应用# ============================================================================class OrganicLabApp: """应用程序主控制器""" def __init__(self, root: tk.Tk): self.root = root self.config = AppConfig.load() self.logger = LogManager() self.state_manager = StateManager() self.current_molecule: Optional[MoleculeData] = None self.state_manager.transition(AppState.INIT, "应用启动") self._setup_registry() self._build_ui() self.state_manager.transition(AppState.READY, "界面初始化完成") if self.config.auto_load_default: self._load_molecule(self.config.default_molecule) def _setup_registry(self): """设置分子注册表""" MoleculeRegistry.register("苯", MoleculeBuilder.build_benzene) MoleculeRegistry.register("甲苯", MoleculeBuilder.build_toluene) self.logger.info(f"已注册分子: {', '.join(MoleculeRegistry.get_all_names())}") def _build_ui(self): """构建用户界面""" self.root.title('OrganicLab v3.0 - 完整管理系统') self.root.geometry(f"{self.config.window_width}x{self.config.window_height}") # 主容器 main_paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL) main_paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 左侧控制面板 left_frame = ttk.Frame(main_paned) main_paned.add(left_frame, weight=0) self._build_control_panel(left_frame) # 右侧视图面板 right_paned = ttk.PanedWindow(main_paned, orient=tk.VERTICAL) main_paned.add(right_paned, weight=1) # 2D 视图 self.canvas_2d = tk.Canvas(right_paned, bg='white', height=300) right_paned.add(self.canvas_2d, weight=1) self.renderer_2d = Canvas2DRenderer(self.canvas_2d) # 3D 视图 self.frame_3d = ttk.Frame(right_paned) right_paned.add(self.frame_3d, weight=2) # 底部日志面板 self.log_viewer = LogViewerWidget(self.root) self.log_viewer.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=5) def _build_control_panel(self, parent): """构建控制面板""" # 标题 ttk.Label( parent, text="🧪 OrganicLab v3.0", font=('Arial', 14, 'bold') ).pack(pady=10) # 分子选择 mol_frame = ttk.LabelFrame(parent, text="分子选择", padding=10) mol_frame.pack(fill=tk.X, padx=5, pady=5) self.mol_var = tk.StringVar(value=self.config.default_molecule) mol_list = MoleculeRegistry.get_all_names() self.mol_combo = ttk.Combobox( mol_frame, textvariable=self.mol_var, values=mol_list, state='readonly' ) self.mol_combo.pack(fill=tk.X, pady=5) ttk.Button( mol_frame, text="🔬 加载分子", command=self._on_load ).pack(fill=tk.X, pady=5) # 反应方程式 rxn_frame = ttk.LabelFrame(parent, text="反应方程式", padding=10) rxn_frame.pack(fill=tk.X, padx=5, pady=5) self.reaction_var = tk.StringVar() self.reaction_combo = ttk.Combobox( rxn_frame, textvariable=self.reaction_var, state='readonly' ) self.reaction_combo.pack(fill=tk.X, pady=5) self.reaction_combo.bind('<<ComboboxSelected>>', self._on_reaction_select) ttk.Button( rxn_frame, text="⚡ 查看方程式", command=self._show_reaction ).pack(fill=tk.X, pady=5) # 分子信息 info_frame = ttk.LabelFrame(parent, text="📊 分子信息", padding=10) info_frame.pack(fill=tk.X, padx=5, pady=5) self.info_text = tk.Text(info_frame, height=10, width=30, font=('Arial', 9), wrap=tk.WORD) self.info_text.pack(fill=tk.BOTH, expand=True) # 状态面板 self.status_panel = StatusPanel(parent) self.status_panel.pack(fill=tk.X, padx=5, pady=5) @DecoratorManager.log_execution @DecoratorManager.time_execution def _load_molecule(self, name: str): """加载分子""" self.state_manager.transition(AppState.LOADING, f"加载 {name}") try: molecule = MoleculeRegistry.get(name) if molecule is None: raise ValueError(f"未找到分子: {name}") if not molecule.validate(): self.logger.warning(f"{name} 数据验证有警告,继续加载") self.current_molecule = molecule # 更新视图 self.state_manager.transition(AppState.RENDERING, "渲染 2D") self.renderer_2d.render(molecule) self.state_manager.transition(AppState.RENDERING, "渲染 3D") self._update_3d_view(molecule) # 更新 UI self._update_reactions(molecule) self._update_info(molecule) self.status_panel.update_molecule(name) self.state_manager.transition(AppState.READY, f"{name} 加载完成") except Exception as e: self.logger.error(f"加载失败: {str(e)}\n{traceback.format_exc()}") self.state_manager.transition(AppState.ERROR, str(e)) messagebox.showerror("加载错误", f"无法加载 {name}:\n{str(e)}") @DecoratorManager.retry_on_failure(max_retries=2) def _update_3d_view(self, molecule): """更新 3D 视图""" for widget in self.frame_3d.winfo_children(): widget.destroy() self.renderer_3d = Matplotlib3DRenderer(self.frame_3d) self.renderer_3d.render(molecule) def _on_load(self): """处理加载按钮点击""" name = self.mol_var.get() if name: self._load_molecule(name) def _on_reaction_select(self, event): """处理反应选择事件""" self._show_reaction() def _show_reaction(self): """显示反应方程式""" if not self.current_molecule: return rxn_name = self.reaction_var.get() equation = self.current_molecule.reactions.get(rxn_name, "") if equation: self.logger.info(f"查看反应: {rxn_name}") messagebox.showinfo( f"{self.current_molecule.name} - {rxn_name}", equation ) def _update_reactions(self, molecule): """更新反应列表""" reaction_names = list(molecule.reactions.keys()) self.reaction_combo['values'] = reaction_names if reaction_names: self.reaction_combo.set(reaction_names[0]) def _update_info(self, molecule): """更新信息面板""" self.info_text.config(state='normal') self.info_text.delete(1.0, tk.END) info = [ f"名称: {molecule.name} ({molecule.name_en})", f"分子式: {molecule.formula}", f"描述: {molecule.description}", f"原子数: {len(molecule.atoms_3d)}", f"化学键数: {len(molecule.bonds)}", "", "验证结果:", ] if molecule._validation_errors: info.append(f" ❌ 错误: {', '.join(molecule._validation_errors)}") else: info.append(" ✅ 数据验证通过") if molecule._validation_warnings: info.append(f" ⚠️ 警告: {', '.join(molecule._validation_warnings)}") info.extend(["", "反应类型:"]) for rxn_name in molecule.reactions: info.append(f" • {rxn_name}") self.info_text.insert(1.0, '\n'.join(info)) self.info_text.config(state='disabled')# ============================================================================# 12. 程序入口# ============================================================================def setup_exception_handler(): """设置全局异常处理器""" def global_handler(exc_type, exc_value, exc_tb): logger = LogManager() logger.critical( f"未捕获的异常: {exc_type.__name__}: {exc_value}\n" f"{''.join(traceback.format_tb(exc_tb))}" ) AppConfig.load().save() import sys sys.excepthook = global_handlerdef main(): """程序主入口""" setup_exception_handler() logger = LogManager() logger.info("=" * 50) logger.info("OrganicLab v3.0 - 启动") logger.info("完整日志和管理系统已初始化") logger.info("=" * 50) try: root = tk.Tk() app = OrganicLabApp(root) def on_closing(): logger.info("应用关闭中...") AppConfig.load().save() root.destroy() root.protocol("WM_DELETE_WINDOW", on_closing) root.mainloop() except Exception as e: logger.critical(f"应用启动失败: {str(e)}\n{traceback.format_exc()}") raiseif __name__ == '__main__': main()