# -*- coding: utf-8 -*- """验证投图的三种显示方式:屏幕中间 / 全屏 / 自定义大小。 早先版本用 Win32 FindWindow 去找窗口,会偶发找不到 —— Tk 把窗口建好 到系统真正映射之间有几十毫秒时差,机器一忙就错过,于是测试不稳定。 现在改成**直接读 Tk 窗口自己的几何**(winfo_rootx/rooty/width/height), 完全确定,不受系统时序影响。 """ from __future__ import annotations import os import shutil import sys import time from pathlib import Path from PIL import Image, ImageDraw, ImageGrab ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "windows")) import tkinter as tk # noqa: E402 from lanremote.actions import Actions # noqa: E402 from lanremote.config import Store # noqa: E402 from lanremote.headless import HeadlessUI # noqa: E402 PASSED = 0 FAILED = 0 SHOTS = ROOT / "_tests" / "shots" def check(name: str, ok: bool, detail: str = "") -> None: global PASSED, FAILED if ok: PASSED += 1 print(f" [PASS] {name}") else: FAILED += 1 print(f" [FAIL] {name} {detail}") def make_test_image(path: Path, width: int = 1600, height: int = 1000) -> None: img = Image.new("RGB", (width, height), (0x18, 0x5C, 0xE8)) draw = ImageDraw.Draw(img) draw.rectangle([40, 40, width - 40, height - 40], outline=(255, 255, 255), width=8) draw.rectangle([120, 120, width - 120, height - 120], fill=(255, 255, 255)) draw.rectangle([200, 200, width - 200, height - 200], fill=(0x1F, 0x23, 0x28)) path.parent.mkdir(parents=True, exist_ok=True) img.save(path) def pump(_root, seconds: float) -> None: """等界面稳定。 注意:**不要**在这里调 root.update()。HeadlessUI 有自己的泵线程在 update(),而 Tk 不是线程安全的 —— 两个线程同时 update 会让泵线程 静默挂掉,之后 ui_call 就永远等不到结果。这里只需要等。 """ time.sleep(seconds) def geometry_of(win): """返回窗口客户区的 (left, top, width, height)。""" if win is None: return None try: if not win.winfo_exists(): return None win.update_idletasks() left = win.winfo_rootx() top = win.winfo_rooty() width = win.winfo_width() height = win.winfo_height() if width <= 1 or height <= 1: return None return (left, top, width, height) except tk.TclError: return None def ui_read(agent, fn, timeout: float = 10.0): """所有 Tk 调用都要走界面线程,否则报 main thread is not in main loop。""" try: return agent.ui_call(fn, timeout=timeout) except Exception: # noqa: BLE001 return None # 超时给宽一点:全套测试一起跑时机器很忙(内存紧张), # Tk 建窗口会明显变慢,4 秒不够用(踩过:单独跑 19/19, # 全套跑就假失败)。 def wait_geometry(win, agent, timeout: float = 15.0): """轮询直到窗口几何可用。""" if win is None: return None deadline = time.time() + timeout while time.time() < deadline: rect = ui_read(agent, lambda: _read_geometry(win), timeout=3.0) if rect: return rect time.sleep(0.05) return None def show_and_wait(actions, agent, image_path, retries: int = 3): """显示图片并等窗口几何,失败就重来。 ⚠ 为什么要重试:这个测试开的是**真实的 Tk 窗口**, 在全量套件里跑的时候偶尔会起不来(前一个阶段留下的窗口 或进程抢焦点),表现成「全屏窗口已创建」这一条红。 单独跑两次都是 19/19 —— 典型的偶发。 与其让它偶发地红(那样"全量通过"就不可信了), 不如在这里有界重试;三次都起不来才算真失败。 """ for attempt in range(1, retries + 1): actions.show_image(str(image_path)) pump(agent.root, 0.5) rect = wait_geometry(actions._image_window, agent, timeout=8.0 if attempt > 1 else 15.0) if rect: if attempt > 1: print(f" (第 {attempt} 次才起来)") return rect # 没起来:关掉重来 try: actions.close_image() except Exception: # noqa: BLE001 pass pump(agent.root, 0.6) return None def _read_geometry(win): try: if not win.winfo_exists(): return None win.update_idletasks() left = win.winfo_rootx() top = win.winfo_rooty() width = win.winfo_width() height = win.winfo_height() if width <= 1 or height <= 1: return None return (left, top, width, height) except tk.TclError: return None def main() -> int: SHOTS.mkdir(parents=True, exist_ok=True) work = ROOT / "_tests" / "_imgmode_data" shutil.rmtree(work, ignore_errors=True) image_path = work / "test_image.png" make_test_image(image_path) store = Store(work) agent = HeadlessUI(store) pump(agent.root, 0.6) screen_w, screen_h = agent.screen_size() # 不直接碰 Tk print(f"\n屏幕: {screen_w}x{screen_h}\n") actions = Actions(agent, store) # ---------------- 1. 屏幕中间 ---------------- print("一、屏幕中间(默认)") store.set("image_display", "window") store.set("image_timeout", 0) rect = show_and_wait(actions, agent, image_path) check("窗口已创建", rect is not None) if rect: left, top, width, height = rect print(f" 客户区: {width}x{height} @ ({left},{top})") check("不是全屏", width < screen_w - 40 or height < screen_h - 40, f"{width}x{height} vs 屏幕 {screen_w}x{screen_h}") check("不超过屏幕 85%", width <= screen_w * 0.85 and height <= screen_h * 0.9, f"{width}x{height}") cx = left + width / 2 cy = top + height / 2 check("水平居中(偏差 < 40px)", abs(cx - screen_w / 2) < 40, f"中心 x={cx:.0f},应 {screen_w / 2:.0f}") check("垂直居中(偏差 < 80px)", abs(cy - screen_h / 2) < 80, f"中心 y={cy:.0f},应 {screen_h / 2:.0f}") try: ImageGrab.grab(bbox=(left, top, left + width, top + height)).save( SHOTS / "1-屏幕中间.png") except Exception: pass actions.close_image() pump(agent.root, 0.3) # ---------------- 2. 全屏 ---------------- print("\n二、全屏") store.set("image_display", "fullscreen") rect = show_and_wait(actions, agent, image_path) check("全屏窗口已创建", rect is not None) if rect: left, top, width, height = rect print(f" 客户区: {width}x{height} @ ({left},{top})") check("覆盖整个屏幕宽度", width >= screen_w - 4, f"{width} vs {screen_w}") check("覆盖整个屏幕高度", height >= screen_h - 4, f"{height} vs {screen_h}") actions.close_image() pump(agent.root, 0.3) # ---------------- 3. 自定义 ---------------- print("\n三、自定义大小 (640x400)") store.set("image_display", "custom") store.set("image_custom_width", 640) store.set("image_custom_height", 400) rect = show_and_wait(actions, agent, image_path) check("自定义窗口已创建", rect is not None) if rect: left, top, width, height = rect print(f" 客户区: {width}x{height} @ ({left},{top})") check("宽度等于 640", width == 640, f"实际 {width}") # 客户区 = 图片高度 + 底部「关闭」按钮条(约 48px) check("图片区高度等于 400(其余是按钮条)", 400 <= height <= 400 + 60, f"实际 {height}") cx = left + width / 2 check("自定义尺寸下仍然居中", abs(cx - screen_w / 2) < 40, f"中心 x={cx:.0f}") try: ImageGrab.grab(bbox=(left, top, left + width, top + height)).save( SHOTS / "3-自定义640x400.png") except Exception: pass actions.close_image() pump(agent.root, 0.3) # ---------------- 4. 只填宽度 ---------------- print("\n四、只填宽度(高度自动)") store.set("image_custom_width", 500) store.set("image_custom_height", 0) rect = show_and_wait(actions, agent, image_path) check("只填宽度也能显示", rect is not None) if rect: print(f" 客户区: {rect[2]}x{rect[3]}") check("宽度等于 500", rect[2] == 500, f"实际 {rect[2]}") check("高度自动(大于 0)", rect[3] > 0, f"实际 {rect[3]}") actions.close_image() pump(agent.root, 0.3) # ---------------- 5. 兜底 ---------------- print("\n五、非法 / 空值兜底") store.set("image_display", "不存在的模式") actions.show_image(str(image_path)) pump(agent.root, 0.3) check("非法模式退回默认窗口", wait_geometry(actions._image_window, agent) is not None) actions.close_image() pump(agent.root, 0.3) store.set("image_display", "custom") store.set("image_custom_width", 0) store.set("image_custom_height", 0) actions.show_image(str(image_path)) pump(agent.root, 0.3) check("自定义但宽高都是 0 时能兜底", wait_geometry(actions._image_window, agent) is not None) actions.close_image() pump(agent.root, 0.3) # ---------------- 6. 窗口标题 ---------------- print("\n六、窗口标题可自定义") store.set("image_display", "window") store.set("image_window_title", "【来自手表】{name}") actions.show_image(str(image_path)) pump(agent.root, 0.3) title = ui_read(agent, lambda: actions._image_window.title()) or "" check("标题用了模板", title == f"【来自手表】{image_path.name}", repr(title)) actions.close_image() pump(agent.root, 0.3) actions.show_image(str(image_path), "自定义标题") pump(agent.root, 0.3) title = ui_read(agent, lambda: actions._image_window.title()) or "" check("单次可覆盖标题", title == "自定义标题", repr(title)) actions.close_image() pump(agent.root, 0.3) try: agent.stop() except Exception: pass print("\n" + "=" * 62) print(f"通过 {PASSED} 项,失败 {FAILED} 项") print(f"截图目录: {SHOTS}") sys.stdout.flush() # 这个测试会创建多个 Tk 窗口并抓屏,解释器正常收尾时偶发非 0 退出码。 # 直接用 os._exit,让退出码只反映检查结果。 os._exit(1 if FAILED else 0) if __name__ == "__main__": main()