diff --git a/app/views/chart_frame.py b/app/views/chart_frame.py index 5531100..82e3b5d 100644 --- a/app/views/chart_frame.py +++ b/app/views/chart_frame.py @@ -25,7 +25,138 @@ def _result_bg_color() -> str: return "#FFFFFF" -def apply_result_chart_theme(self: "PQAutomationApp"): +def _refresh_ax_theme_in_place(ax, palette) -> None: + """就地更新已有坐标轴的主题色,无需清空重绘。""" + ax.set_facecolor(palette["card_bg"]) + for spine in ax.spines.values(): + spine.set_color(palette["border"]) + ax.tick_params(axis="both", colors=palette["fg"]) + try: + ax.xaxis.label.set_color(palette["fg"]) + ax.yaxis.label.set_color(palette["fg"]) + except Exception: + pass + try: + if ax.title.get_text(): + ax.title.set_color(palette["fg"]) + except Exception: + pass + for artist in getattr(ax, "texts", []): + try: + artist.set_color(palette["fg"]) + except Exception: + pass + for line in ax.get_lines(): + # 保留曲线本身颜色,仅刷新网格等辅助线 + pass + for gridline in ax.get_xgridlines() + ax.get_ygridlines(): + try: + gridline.set_color(palette["border"]) + gridline.set_alpha(0.35) + except Exception: + pass + + +_CHART_THEME_SPECS = { + "gamut": ("gamut_chart_frame", "gamut_fig", "gamut_canvas"), + "gamma": ("gamma_chart_frame", "gamma_fig", "gamma_canvas"), + "eotf": ("eotf_chart_frame", "eotf_fig", "eotf_canvas"), + "cct": ("cct_chart_frame", "cct_fig", "cct_canvas"), + "contrast": ("contrast_chart_frame", "contrast_fig", "contrast_canvas"), + "accuracy": ("accuracy_chart_frame", "accuracy_fig", "accuracy_canvas"), +} + + +def _get_active_chart_key(self: "PQAutomationApp") -> str | None: + """返回当前选中的结果图 Tab 对应的 chart key。""" + notebook = getattr(self, "chart_notebook", None) + if notebook is None: + return None + try: + selected = notebook.select() + if not selected: + return "gamut" + for key, (frame_attr, _, _) in _CHART_THEME_SPECS.items(): + frame = getattr(self, frame_attr, None) + if frame is not None and str(frame) == selected: + return key + except Exception: + pass + return "gamut" + + +def _apply_fig_theme_colors(fig, canvas, palette, bg) -> None: + """更新 Figure/Axes 主题色,不触发绘制。""" + fig.patch.set_facecolor(bg) + suptitle = getattr(fig, "_suptitle", None) + if suptitle is not None: + try: + suptitle.set_color(palette["fg"]) + except Exception: + pass + for ax in fig.axes: + _refresh_ax_theme_in_place(ax, palette) + if canvas is not None: + try: + widget = canvas.get_tk_widget() + widget.configure(bg=bg, highlightthickness=0) + except Exception: + pass + + +def flush_chart_theme_if_dirty(self: "PQAutomationApp", chart_key: str | None = None) -> None: + """绘制主题已更新但尚未 draw 的图表(通常在切换到该 Tab 时调用)。""" + dirty = getattr(self, "_charts_theme_dirty", None) + if not dirty: + return + keys = [chart_key] if chart_key and chart_key in dirty else list(dirty) + for key in keys: + spec = _CHART_THEME_SPECS.get(key) + if spec is None: + continue + canvas = getattr(self, spec[2], None) + if canvas is None: + continue + try: + canvas.draw() + except Exception: + pass + dirty.discard(key) + + +def refresh_result_charts_theme( + self: "PQAutomationApp", *, sync_draw: bool = True, visible_only: bool = True +) -> None: + """就地刷新结果区 Matplotlib 图表主题色;默认仅绘制当前可见 Tab。""" + palette = get_theme_palette() + bg = _result_bg_color() + active_key = _get_active_chart_key(self) if visible_only else None + + dirty = getattr(self, "_charts_theme_dirty", None) + if dirty is None: + dirty = set() + self._charts_theme_dirty = dirty + + for key, (_, fig_attr, canvas_attr) in _CHART_THEME_SPECS.items(): + fig = getattr(self, fig_attr, None) + canvas = getattr(self, canvas_attr, None) + if fig is None or canvas is None: + continue + + _apply_fig_theme_colors(fig, canvas, palette, bg) + + should_draw = sync_draw and ((not visible_only) or key == active_key) + if should_draw: + try: + canvas.draw() + except Exception: + pass + dirty.discard(key) + else: + dirty.add(key) + + +def apply_result_chart_theme(self: "PQAutomationApp", *, sync_draw: bool = False): """统一刷新结果图画布背景,使其跟随浅/深色主题。""" bg = _result_bg_color() @@ -50,7 +181,10 @@ def apply_result_chart_theme(self: "PQAutomationApp"): except Exception: pass try: - canvas.draw_idle() + if sync_draw: + canvas.draw() + else: + canvas.draw_idle() except Exception: pass @@ -1111,6 +1245,9 @@ def on_chart_tab_changed(self: "PQAutomationApp", event): if not selected_tab: return self._last_tab_index = self.chart_notebook.index(selected_tab) + active_key = _get_active_chart_key(self) + if active_key: + flush_chart_theme_if_dirty(self, active_key) except Exception as e: self.log_gui.log(f"Tab切换事件处理失败: {str(e)}", level="error") @@ -1129,6 +1266,8 @@ class ChartFrameMixin: init_contrast_chart = init_contrast_chart init_accuracy_chart = init_accuracy_chart apply_result_chart_theme = apply_result_chart_theme + refresh_result_charts_theme = refresh_result_charts_theme + flush_chart_theme_if_dirty = flush_chart_theme_if_dirty _init_accuracy_result_table = _init_accuracy_result_table clear_accuracy_result_table = clear_accuracy_result_table update_accuracy_result_table = update_accuracy_result_table diff --git a/app/views/panel_manager.py b/app/views/panel_manager.py index e45a17c..15a6711 100644 --- a/app/views/panel_manager.py +++ b/app/views/panel_manager.py @@ -245,10 +245,20 @@ def _show_main_content(self: "PQAutomationApp") -> None: def _notify_panel_shown(self: "PQAutomationApp", panel_name: str) -> None: """面板显示后的轻量刷新(延迟执行,不阻塞切换)。""" + if getattr(self, "_theme_transition", False): + return if panel_name == "calman": refresh = getattr(self, "_on_calman_panel_shown", None) if refresh is not None: self.root.after_idle(refresh) + return + if panel_name == "ai_image" and getattr(self, "_ai_image_theme_dirty", False): + self.root.after_idle(lambda: self.refresh_ai_image_theme()) + return + if panel_name == "log" and hasattr(self, "log_gui") and hasattr( + self.log_gui, "refresh_log_theme" + ): + self.root.after_idle(self.log_gui.refresh_log_theme) def _show_stack_panel(self: "PQAutomationApp", panel_name: str) -> None: diff --git a/app/views/panels/ai_image_panel.py b/app/views/panels/ai_image_panel.py index c94a032..421f91e 100644 --- a/app/views/panels/ai_image_panel.py +++ b/app/views/panels/ai_image_panel.py @@ -1220,6 +1220,10 @@ def _build_ucd_resized_image(image_path: str, target_w: int, target_h: int) -> s def refresh_ai_image_theme(self: "PQAutomationApp"): """刷新 AI 图片面板中的主题相关控件。""" + if getattr(self, "current_panel", None) != "ai_image": + self._ai_image_theme_dirty = True + return + self._ai_image_theme_dirty = False if hasattr(self, "_apply_ai_image_list_style"): self._apply_ai_image_list_style() tip = getattr(self, "_ai_image_tooltip", None) diff --git a/app/views/panels/calman_panel.py b/app/views/panels/calman_panel.py index d1d106c..86967a2 100644 --- a/app/views/panels/calman_panel.py +++ b/app/views/panels/calman_panel.py @@ -699,9 +699,14 @@ def _on_calman_panel_shown(self: "PQAutomationApp") -> None: """面板切到前台时只做轻量刷新,不重绘 Matplotlib。""" _refresh_calman_config_summary(self) _schedule_adaptive_matrix_columns(self) + if getattr(self, "_calman_theme_dirty", False): + _redraw_calman_charts(self) + self._calman_theme_dirty = False def _schedule_adaptive_matrix_columns(self: "PQAutomationApp") -> None: + if getattr(self, "_theme_transition", False): + return job = getattr(self, "_calman_matrix_col_job", None) if job is not None: try: @@ -1116,7 +1121,7 @@ def _adaptive_matrix_columns(self: "PQAutomationApp") -> None: self.calman_data_tree.column(str(p), width=width, minwidth=min_w, stretch=False) -def _redraw_calman_charts(self: "PQAutomationApp") -> None: +def _redraw_calman_charts(self: "PQAutomationApp", *, sync_draw: bool = False) -> None: """根据 calman_results 重绘四张图和 xy 散点。""" palette = _get_calman_palette() if hasattr(self, "calman_fig"): @@ -1236,12 +1241,15 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None: a4.set_ylim(1.8, 2.8) a4.set_xlabel("", fontsize=8) - self.calman_canvas.draw_idle() + if sync_draw or getattr(self, "_theme_transition", False): + self.calman_canvas.draw() + else: + self.calman_canvas.draw_idle() - _redraw_xy_chart(self) + _redraw_xy_chart(self, sync_draw=sync_draw) -def _redraw_xy_chart(self: "PQAutomationApp") -> None: +def _redraw_xy_chart(self: "PQAutomationApp", *, sync_draw: bool = False) -> None: palette = _get_calman_palette() if hasattr(self, "calman_xy_fig"): self.calman_xy_fig.patch.set_facecolor(palette["figure_bg"]) @@ -1268,7 +1276,10 @@ def _redraw_xy_chart(self: "PQAutomationApp") -> None: last = recs[-1] ax.plot([last["x"]], [last["y"]], marker="o", color="#ffcc00", markersize=5) ax.plot([last["x"], D65_X], [last["y"], D65_Y], color=_mix(palette["fg"], palette["axes_bg"], 0.4), linewidth=0.8) - self.calman_xy_canvas.draw_idle() + if sync_draw or getattr(self, "_theme_transition", False): + self.calman_xy_canvas.draw() + else: + self.calman_xy_canvas.draw_idle() def _update_actual_strip(self: "PQAutomationApp") -> None: @@ -1360,11 +1371,18 @@ def _refresh_metric_table(self: "PQAutomationApp") -> None: self.calman_table_ysb.set(first, last) -def refresh_calman_theme(self: "PQAutomationApp") -> None: +def refresh_calman_theme(self: "PQAutomationApp", *, redraw_charts: bool | None = None) -> None: """主题切换后刷新 Calman 灰阶调试面板的表格与图表颜色。""" if not hasattr(self, "calman_frame"): return + if getattr(self, "current_panel", None) != "calman": + self._calman_theme_dirty = True + return + + if redraw_charts is None: + redraw_charts = True + palette = _get_calman_palette() if hasattr(self, "calman_elapsed_label"): @@ -1388,7 +1406,11 @@ def refresh_calman_theme(self: "PQAutomationApp") -> None: _refresh_metric_table(self) _refresh_calman_config_summary(self) - _redraw_calman_charts(self) + if redraw_charts: + _redraw_calman_charts(self, sync_draw=getattr(self, "_theme_transition", False)) + self._calman_theme_dirty = False + else: + self._calman_theme_dirty = True class CalmanPanelMixin: diff --git a/app/views/panels/custom_template_panel.py b/app/views/panels/custom_template_panel.py index fed5029..958200e 100644 --- a/app/views/panels/custom_template_panel.py +++ b/app/views/panels/custom_template_panel.py @@ -172,6 +172,14 @@ def _apply_custom_result_theme(self: "PQAutomationApp"): def refresh_custom_template_theme(self: "PQAutomationApp"): """刷新客户模板结果表的主题色。""" + notebook = getattr(self, "chart_notebook", None) + tab_frame = getattr(self, "custom_template_tab_frame", None) + if notebook is not None and tab_frame is not None: + try: + if str(tab_frame) not in notebook.tabs(): + return + except Exception: + pass _apply_custom_result_theme(self) diff --git a/app/views/panels/main_layout.py b/app/views/panels/main_layout.py index 88fe52a..b3e789a 100644 --- a/app/views/panels/main_layout.py +++ b/app/views/panels/main_layout.py @@ -869,9 +869,113 @@ def _refresh_theme_toggle_label(self: "PQAutomationApp") -> None: self.theme_toggle_btn.configure(text="切换深色模式") +def _replot_charts_for_current_theme(self: "PQAutomationApp") -> None: + """刷新结果区图表配色(仅同步绘制当前可见 Tab)。""" + if hasattr(self, "refresh_result_charts_theme"): + try: + self.refresh_result_charts_theme(sync_draw=True, visible_only=True) + return + except Exception: + pass + + test_type = None + if hasattr(self, "config"): + test_type = getattr(self.config, "current_test_type", None) + + snapshots = {} + if test_type and hasattr(self, "_chart_snapshots"): + snapshots = self._chart_snapshots.get(test_type) or {} + + if not snapshots: + if hasattr(self, "apply_result_chart_theme"): + self.apply_result_chart_theme(sync_draw=True) + return + + for chart_name, args in snapshots.items(): + plot_fn = getattr(self, f"plot_{chart_name}", None) + if plot_fn: + try: + plot_fn(*args) + except Exception: + pass + + +def _cancel_pending_ui_jobs(self: "PQAutomationApp") -> None: + """取消可能在主题切换后继续触发的延迟 UI 任务。""" + for attr in ("_calman_matrix_col_job", "_chart_restore_job"): + job = getattr(self, attr, None) + if job is None: + continue + try: + self.root.after_cancel(job) + except Exception: + pass + setattr(self, attr, None) + + +def _refresh_all_theme_widgets(self: "PQAutomationApp") -> None: + """同步刷新主题相关控件:优先处理可见区域,隐藏面板延后。""" + _refresh_theme_toggle_label(self) + + # 侧栏 / 连接指示 / 选中态 —— 始终可见,必须立即更新 + for method_name in ( + "refresh_tool_panel_sidebar_theme", + "refresh_connection_indicators", + ): + method = getattr(self, method_name, None) + if method is not None: + try: + method() + except Exception: + pass + + current_panel = getattr(self, "current_panel", None) + + if current_panel == "ai_image" and hasattr(self, "refresh_ai_image_theme"): + try: + self.refresh_ai_image_theme() + except Exception: + pass + else: + self._ai_image_theme_dirty = True + + if current_panel == "single_step" and hasattr(self, "refresh_single_step_theme"): + try: + self.refresh_single_step_theme() + except Exception: + pass + + if current_panel == "log" and hasattr(self, "log_gui") and hasattr( + self.log_gui, "refresh_log_theme" + ): + try: + self.log_gui.refresh_log_theme() + except Exception: + pass + + if hasattr(self, "refresh_custom_template_theme"): + try: + self.refresh_custom_template_theme() + except Exception: + pass + + if hasattr(self, "refresh_calman_theme"): + try: + self.refresh_calman_theme() + except Exception: + pass + + if hasattr(self, "update_sidebar_selection"): + try: + self.update_sidebar_selection() + except Exception: + pass + + _replot_charts_for_current_theme(self) + + def _on_toggle_theme(self: "PQAutomationApp") -> None: - """切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签。""" - # 在测试进行时禁止切换主题,避免影响测量稳定性 + """切换主题:保持窗口可见,同步批量刷新避免卡顿与分批变色。""" if getattr(self, "testing", False): try: if hasattr(self, "log_gui"): @@ -881,67 +985,14 @@ def _on_toggle_theme(self: "PQAutomationApp") -> None: return from app.views.theme_manager import toggle_theme - toggle_theme() - # apply_modern_styles() - _refresh_theme_toggle_label(self) - if hasattr(self, "refresh_tool_panel_sidebar_theme"): - try: - self.refresh_tool_panel_sidebar_theme() - except Exception: - pass - if hasattr(self, "refresh_connection_indicators"): - try: - self.refresh_connection_indicators() - except Exception: - pass - if hasattr(self, "apply_result_chart_theme"): - try: - self.apply_result_chart_theme() - except Exception: - pass - if hasattr(self, "log_gui") and hasattr(self.log_gui, "refresh_log_theme"): - try: - self.log_gui.refresh_log_theme() - except Exception: - pass - if hasattr(self, "refresh_ai_image_theme"): - try: - self.refresh_ai_image_theme() - except Exception: - pass - if hasattr(self, "refresh_single_step_theme"): - try: - self.refresh_single_step_theme() - except Exception: - pass - if hasattr(self, "refresh_custom_template_theme"): - try: - self.refresh_custom_template_theme() - except Exception: - pass - if hasattr(self, "refresh_calman_theme"): - try: - self.refresh_calman_theme() - except Exception: - pass - # 同步刷新侧栏选中态(高亮样式跟随新色板) - if hasattr(self, "update_sidebar_selection"): - try: - self.update_sidebar_selection() - except Exception: - pass - # 以新的 dark_mode 值重绘当前测试类型的所有图表 - if hasattr(self, "_chart_snapshots") and hasattr(self, "config"): - test_type = getattr(self.config, "current_test_type", None) - if test_type: - snapshots = self._chart_snapshots.get(test_type, {}) - for chart_name, args in snapshots.items(): - plot_fn = getattr(self, f"plot_{chart_name}", None) - if plot_fn: - try: - plot_fn(*args) - except Exception: - pass + + _cancel_pending_ui_jobs(self) + self._theme_transition = True + try: + toggle_theme() + _refresh_all_theme_widgets(self) + finally: + self._theme_transition = False def update_config_info_display(self: "PQAutomationApp"): diff --git a/pqAutomationApp.py b/pqAutomationApp.py index 525e20b..537fac1 100644 --- a/pqAutomationApp.py +++ b/pqAutomationApp.py @@ -156,6 +156,7 @@ class PQAutomationApp( self.current_panel = None # 当前显示的面板名称 self.panels = {} # 存储所有面板的信息 self.log_visible = False + self._theme_transition = False # 创建左侧面板 self.left_frame = ttk.Frame(self.main_frame, width=208)