"""现代化的可折叠面板(取代 v1 的图标按钮版本)。 升级要点(保留 ``add(child, title=...)`` 旧签名兼容): - header 整条可点击切换展开/收起; - 使用 Unicode chevron (▾/▸),无需 PNG 资源; - 新增 ``preview_textvariable``:折叠时在 header 显示当前配置摘要; - 新增 ``header_actions``:在 header 右侧注入自定义按钮(如顶部工具条)。 """ import tkinter from tkinter import ttk class CollapsingFrame(ttk.Frame): """A modern collapsible frame widget.""" CHEVRON_OPEN = "\u25be" # ▾ CHEVRON_CLOSED = "\u25b8" # ▸ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.columnconfigure(0, weight=1) self.cumulative_rows = 0 # 兼容旧代码可能引用 self.images self.images: list = [] # ------------------------------------------------------------------ # 公共 API # ------------------------------------------------------------------ def add( self, child, title: str = "", style: str = "primary.TButton", # 兼容旧签名(不再使用) preview_textvariable=None, header_actions=None, **kwargs, ): """添加一个子区段到折叠面板。 :param child: 必须是一个 ttk.Frame; :param title: 标题文本; :param preview_textvariable: 折叠时显示在 header 上的状态摘要 StringVar; :param header_actions: 回调 ``fn(actions_frame)``,可在 header 右侧添加按钮。 """ if child.winfo_class() != "TFrame": return header = ttk.Frame(self, style="ConfigHeader.TFrame", padding=(12, 6)) header.grid(row=self.cumulative_rows, column=0, sticky="ew") header.columnconfigure(1, weight=1) # 左:chevron + 标题 title_box = ttk.Frame(header, style="ConfigHeader.TFrame") title_box.grid(row=0, column=0, sticky="w") chevron = ttk.Label( title_box, text=self.CHEVRON_OPEN, style="ConfigChevron.TLabel" ) chevron.pack(side="left", padx=(0, 8)) title_lbl = ttk.Label(title_box, text=title, style="ConfigHeader.TLabel") if kwargs.get("textvariable"): title_lbl.configure(textvariable=kwargs.get("textvariable")) title_lbl.pack(side="left") # 中:折叠状态预览 preview_lbl = None if preview_textvariable is not None: preview_lbl = ttk.Label( header, textvariable=preview_textvariable, style="ConfigPreview.TLabel", ) preview_lbl.grid(row=0, column=1, sticky="w", padx=(16, 8)) # 右:actions(如顶部工具条按钮) actions_frame = ttk.Frame(header, style="ConfigHeader.TFrame") actions_frame.grid(row=0, column=2, sticky="e") if callable(header_actions): try: header_actions(actions_frame) except Exception: # 注入失败不应影响整体折叠面板渲染 pass # 整条 header 点击切换 clickable = [header, title_box, chevron, title_lbl] if preview_lbl is not None: clickable.append(preview_lbl) for w in clickable: w.bind( "", lambda _e, c=child: self._toggle_open_close(c), ) try: w.configure(cursor="hand2") except tkinter.TclError: pass child._chevron = chevron child._header = header child._preview_lbl = preview_lbl # 兼容旧代码 child.btn.invoke() / child.btn.configure(image=...) child.btn = _HeaderToggleProxy(self, child, chevron) child.grid(row=self.cumulative_rows + 1, column=0, sticky="news") self.cumulative_rows += 2 # ------------------------------------------------------------------ # 内部实现 # ------------------------------------------------------------------ def _toggle_open_close(self, child): if child.winfo_viewable(): child.grid_remove() try: child._chevron.configure(text=self.CHEVRON_CLOSED) except (AttributeError, tkinter.TclError): pass else: child.grid() try: child._chevron.configure(text=self.CHEVRON_OPEN) except (AttributeError, tkinter.TclError): pass class _HeaderToggleProxy: """兼容旧代码:``child.btn.invoke()`` / ``child.btn.configure(image=...)``。""" def __init__(self, owner: "CollapsingFrame", child, chevron): self._owner = owner self._child = child self._chevron = chevron def invoke(self): self._owner._toggle_open_close(self._child) def configure(self, **kwargs): image = kwargs.get("image") if image == "closed": self._chevron.configure(text=CollapsingFrame.CHEVRON_CLOSED) elif image == "open": self._chevron.configure(text=CollapsingFrame.CHEVRON_OPEN) config = configure # class Application(tkinter.Tk): # def __init__(self): # super().__init__() # self.title('Collapsing Frame') # # self.style = Style() # cf = CollapsingFrame(self) # cf.pack(fill='both') # # option group 1 # group1 = ttk.Frame(cf, padding=10) # for x in range(5): # ttk.Checkbutton(group1, text=f'Option {x + 1}').pack(fill='x') # cf.add(group1, title='Option Group 1', style='primary.TButton') # # option group 2 # group2 = ttk.Frame(cf, padding=10) # for x in range(5): # ttk.Checkbutton(group2, text=f'Option {x + 1}').pack(fill='x') # cf.add(group2, title='Option Group 2', style='danger.TButton') # if __name__ == '__main__': # Application().mainloop()