#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 测试图案全屏投屏窗口(tkinter): 纯色/棋盘格/网格线 + 自定义图片 1:1 居中 ================================================================ 投屏窗口为无边框黑底 Toplevel, ESC 或点击退出; 测试卡区域可选 640x360 / 800x600 / full 铺满(常量与图片加载配置在 config)。 """ 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': '网格线'} # 纯色测试值(位交换测试用, 与 assets/solid_*.png 对应): # 每通道 55/AA 互为镜像对(检测对), FF 全高基准 SOLID_ROWS = ( ('R', (('55', '#550000'), ('AA', '#AA0000'), ('FF', '#FF0000'))), ('G', (('55', '#005500'), ('AA', '#00AA00'), ('FF', '#00FF00'))), ('B', (('55', '#000055'), ('AA', '#0000AA'), ('FF', '#0000FF'))), ) def load_image_raw(path): """按原始像素加载图片(不缩放不裁剪; PIL 仅用于支持 JPG/BMP/WebP 等格式)""" import tkinter as tk if HAS_PIL: return ImageTk.PhotoImage(Image.open(path).convert('RGB')) return tk.PhotoImage(file=path) def _area_box(spec, w, h): """区域选项 -> (x0, y0, aw, ah): 'full' 铺满整个屏幕, 'WxH' 黑底居中""" if spec == 'full': return 0, 0, w, h try: aw, ah = (int(v) for v in spec.lower().split('x', 1)) except Exception: aw, ah = PAT_W, PAT_H return (w - aw) // 2, (h - ah) // 2, aw, ah def _open_fullscreen(parent, rect, auto_close=0): """创建覆盖 rect(x,y,w,h) 的无边框全屏黑底窗口, 返回 (top, cv) ESC 退出(需窗口持有键盘焦点); overrideredirect 窗口必须 focus_force 强取键盘焦点, 否则收不到按键(Windows 上该组合有效)。 窗口登记进 _OPEN: 打开新投屏前会关旧的(替换语义), close_all() 一键全关。 """ import tkinter as tk rx, ry, rw, rh = rect top = tk.Toplevel(parent) top.overrideredirect(True) top.geometry(f'{rw}x{rh}+{rx}+{ry}') top.attributes('-topmost', True) top.configure(bg='black') cv = tk.Canvas(top, bg='black', highlightthickness=0, bd=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() return top, cv def show_pattern(parent, rect, kind, auto_close=0, area=DEFAULT_AREA): """在 rect 指定的屏幕上全屏出居中测试卡(无任何 OSD 修饰), 区域由 area 指定。 打开前先关闭已有投屏窗口(替换语义, 不会多层叠罗汉)。""" close_all() top, cv = _open_fullscreen(parent, rect, auto_close) def draw(_evt=None): cv.delete('all') w, h = max(cv.winfo_width(), rect[2]), max(cv.winfo_height(), rect[3]) x0, y0, aw, ah = _area_box(area, w, h) if kind.startswith('#'): # 纯色测试值: 直接填充指定颜色 cv.create_rectangle(x0, y0, x0 + aw, y0 + ah, fill=kind, outline='') elif kind == 'Red': cv.create_rectangle(x0, y0, x0 + aw, y0 + ah, fill='#FF0000', outline='') elif kind == 'Green': cv.create_rectangle(x0, y0, x0 + aw, y0 + ah, fill='#00FF00', outline='') elif kind == 'Blue': cv.create_rectangle(x0, y0, x0 + aw, y0 + ah, fill='#0000FF', outline='') elif kind == 'Checker': # 40px 黑白格, 格数随区域尺寸自适应(640x360 = 16x9 格) for gy in range((ah + 39) // 40): for gx in range((aw + 39) // 40): if (gx + gy) % 2 == 0: cv.create_rectangle(x0 + gx * 40, y0 + gy * 40, x0 + (gx + 1) * 40, y0 + (gy + 1) * 40, fill='#FFFFFF', outline='') elif kind == 'Grid': # 20px 白色网格线, 黑底, 无其它修饰 for k in range(aw // 20 + 1): cv.create_line(x0 + k * 20, y0, x0 + k * 20, y0 + ah, fill='#FFFFFF') for k in range(ah // 20 + 1): cv.create_line(x0, y0 + k * 20, x0 + aw, y0 + k * 20, fill='#FFFFFF') top.bind('', draw) top.after(50, draw) top.wait_window(top) 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): cv.delete('all') w, h = max(cv.winfo_width(), rect[2]), max(cv.winfo_height(), rect[3]) x0, y0, aw, ah = _area_box(area, w, h) photo = load_image_raw(path) top._photo = photo # 挂在窗口上防止被垃圾回收 iw, ih = photo.width(), photo.height() cv.create_image(x0 + (aw - iw) // 2, y0 + (ah - ih) // 2, image=photo, anchor='nw') # 四块黑矩形盖住测试卡区域之外 -> 大图等效中心裁剪 cv.create_rectangle(-2, -2, w + 2, y0, fill='black', outline='') cv.create_rectangle(-2, y0 + ah, w + 2, h + 2, fill='black', outline='') cv.create_rectangle(-2, y0, x0, y0 + ah, fill='black', outline='') cv.create_rectangle(x0 + aw, y0, w + 2, y0 + ah, fill='black', outline='') top.bind('', draw) top.after(50, draw) top.wait_window(top)