diff --git a/HDMI-Tool/release/DLPilot.exe b/HDMI-Tool/release/DLPilot.exe index 28dc14a..08af793 100644 Binary files a/HDMI-Tool/release/DLPilot.exe and b/HDMI-Tool/release/DLPilot.exe differ diff --git a/HDMI-Tool/src/app_gui.py b/HDMI-Tool/src/app_gui.py index 3e2a984..682b4a4 100644 --- a/HDMI-Tool/src/app_gui.py +++ b/HDMI-Tool/src/app_gui.py @@ -94,6 +94,8 @@ class App(ctk.CTk): self._tl_order = [] # 温度查询应答顺序: 'DMD'/'LAMP' 先到先得 self._build() self._load_displays() + # 主窗口按 ESC 也能关闭所有投屏(投屏窗口失焦时的兜底出口) + self.bind('', lambda _e: patterns.close_all()) if dlpc_rs485.HAS_SERIAL: self._poll_dlpc() if auto_close > 0: @@ -208,7 +210,8 @@ class App(ctk.CTk): self.edid_scroll.pack(fill='both', expand=True) # -- 测试图案 -- - card, c = self._card(col,'测试图案 — 黑底 1:1 居中, 按 ESC 退出') + card, c = self._card(col,'测试图案 — 黑底 1:1 居中, 投屏按 ESC 或"恢复桌面"退出', + button='恢复桌面', command=patterns.close_all) card.grid(row=3, column=0, sticky='ew', pady=(10, 0)) row1 = ctk.CTkFrame(c, fg_color='transparent') row1.pack(fill='x') diff --git a/HDMI-Tool/src/patterns.py b/HDMI-Tool/src/patterns.py index ebe3cdd..e590618 100644 --- a/HDMI-Tool/src/patterns.py +++ b/HDMI-Tool/src/patterns.py @@ -9,6 +9,19 @@ from config import DEFAULT_AREA, HAS_PIL, Image, ImageTk, PAT_W, PAT_H +# 当前打开的投屏窗口集合: 支持"新图案替换旧图案"与 close_all() 一键恢复桌面 +_OPEN = set() + + +def close_all(): + """关闭所有打开的投屏窗口, 恢复桌面""" + for top in list(_OPEN): + try: + top.destroy() + except Exception: + pass + _OPEN.clear() + PATTERN_NAMES = {'Red': '纯红', 'Green': '纯绿', 'Blue': '纯蓝', 'Checker': '黑白棋盘格', 'Grid': '网格线'} @@ -43,8 +56,9 @@ def _area_box(spec, w, h): def _open_fullscreen(parent, rect, auto_close=0): """创建覆盖 rect(x,y,w,h) 的无边框全屏黑底窗口, 返回 (top, cv) - 只支持 ESC 退出: overrideredirect 窗口必须 focus_force 强取键盘焦点, - 否则收不到按键(Windows 上该组合有效)。 + ESC 退出(需窗口持有键盘焦点); overrideredirect 窗口必须 focus_force + 强取键盘焦点, 否则收不到按键(Windows 上该组合有效)。 + 窗口登记进 _OPEN: 打开新投屏前会关旧的(替换语义), close_all() 一键全关。 """ import tkinter as tk rx, ry, rw, rh = rect @@ -58,9 +72,16 @@ def _open_fullscreen(parent, rect, auto_close=0): cv.pack(fill='both', expand=True) def close(_evt=None): + _OPEN.discard(top) top.destroy() + def _on_destroy(e): + if e.widget is top: # 子控件销毁也冒泡到此, 只认窗口本身 + _OPEN.discard(top) + top.bind('', close) + top.bind('', _on_destroy) + _OPEN.add(top) if auto_close > 0: top.after(auto_close * 1000, close) top.focus_force() @@ -68,7 +89,9 @@ def _open_fullscreen(parent, rect, auto_close=0): def show_pattern(parent, rect, kind, auto_close=0, area=DEFAULT_AREA): - """在 rect 指定的屏幕上全屏出居中测试卡(无任何 OSD 修饰), 区域由 area 指定""" + """在 rect 指定的屏幕上全屏出居中测试卡(无任何 OSD 修饰), 区域由 area 指定。 + 打开前先关闭已有投屏窗口(替换语义, 不会多层叠罗汉)。""" + close_all() top, cv = _open_fullscreen(parent, rect, auto_close) def draw(_evt=None): @@ -107,7 +130,9 @@ def show_pattern(parent, rect, kind, auto_close=0, area=DEFAULT_AREA): def show_pattern_image(parent, rect, path, auto_close=0, area=DEFAULT_AREA): """全屏黑底, 图片 1:1 居中于 area 指定的测试卡区域: 小于区域 -> 真实像素居中, 四周黑边; - 大于区域 -> 等效从中心截取(区域外用黑矩形遮挡)""" + 大于区域 -> 等效从中心截取(区域外用黑矩形遮挡)。 + 打开前先关闭已有投屏窗口(替换语义)。""" + close_all() top, cv = _open_fullscreen(parent, rect, auto_close) def draw(_evt=None):