157 lines
6.2 KiB
Python
157 lines
6.2 KiB
Python
#!/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('<Escape>', close)
|
|
top.bind('<Destroy>', _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('<Configure>', 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('<Configure>', draw)
|
|
top.after(50, draw)
|
|
top.wait_window(top)
|
|
|