""" imgui_webview.py 后台 playwright 无头浏览器 + 截图 → Panda3D 纹理,供 ImGui 面板显示。 """ from __future__ import annotations import threading import io import time class ImGuiWebView: """ 后台线程运行 playwright Chromium,定期截图并转换为 Panda3D 纹理。 ImGui 直接用 imgui.image() 显示纹理,鼠标/滚轮事件转发给浏览器。 """ def __init__(self, width: int = 1280, height: int = 720): self.view_width = width self.view_height = height # 截图数据(bytes) self._screenshot: bytes | None = None self._screenshot_lock = threading.Lock() self.tex_dirty = False # 有新截图待上传 GPU # 状态 self.current_url = "" self.title = "" self.is_loading = False self.error: str | None = None # 待处理指令(由 ImGui 线程写,浏览器线程读) self._cmd_navigate: str | None = None self._cmd_click: tuple | None = None # (x_ratio, y_ratio) self._cmd_scroll: float | None = None # pixels self._cmd_back = False self._cmd_forward = False self._cmd_reload = False self._lock = threading.Lock() self._running = False self._thread: threading.Thread | None = None # ------------------------------------------------------------------ # # 公开控制 API(由 ImGui 线程调用,线程安全) # ------------------------------------------------------------------ # def start(self, url: str): if self._running: return self._running = True self._cmd_navigate = url self._thread = threading.Thread(target=self._run, daemon=True, name="imgui-webview") self._thread.start() def stop(self): self._running = False def navigate(self, url: str): if not url.startswith(('http://', 'https://', 'file://')): url = 'https://' + url with self._lock: self._cmd_navigate = url self.is_loading = True def click(self, x_ratio: float, y_ratio: float): with self._lock: self._cmd_click = (x_ratio, y_ratio) def scroll(self, delta_px: float): with self._lock: self._cmd_scroll = delta_px def go_back(self): with self._lock: self._cmd_back = True def go_forward(self): with self._lock: self._cmd_forward = True def reload(self): with self._lock: self._cmd_reload = True def get_screenshot_bytes(self) -> bytes | None: with self._screenshot_lock: return self._screenshot # ------------------------------------------------------------------ # # 内部线程 # ------------------------------------------------------------------ # def _run(self): import asyncio loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: loop.run_until_complete(self._async_run()) except Exception as exc: self.error = f"WebView 线程异常: {exc}" import traceback; traceback.print_exc() finally: loop.close() async def _async_run(self): try: from playwright.async_api import async_playwright except ImportError: self.error = ( "playwright 未安装。\n" "请运行: pip install playwright\n" "然后运行: playwright install chromium" ) self._running = False return try: async with async_playwright() as pw: browser = await pw.chromium.launch(headless=True) ctx = await browser.new_context( viewport={"width": self.view_width, "height": self.view_height}, user_agent=( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/121.0.0.0 Safari/537.36" ), ) page = await ctx.new_page() # 初次导航 start_url = self._cmd_navigate or "about:blank" self._cmd_navigate = None await self._goto(page, start_url) await self._snap(page) # 主循环 import asyncio while self._running: await asyncio.sleep(0.05) with self._lock: nav_url = self._cmd_navigate; self._cmd_navigate = None clk = self._cmd_click; self._cmd_click = None scr = self._cmd_scroll; self._cmd_scroll = None do_back = self._cmd_back; self._cmd_back = False do_fwd = self._cmd_forward; self._cmd_forward = False do_reload = self._cmd_reload; self._cmd_reload = False changed = False if nav_url: self.is_loading = True await self._goto(page, nav_url) changed = True self.is_loading = False if do_back: await page.go_back() await asyncio.sleep(0.5) changed = True if do_fwd: await page.go_forward() await asyncio.sleep(0.5) changed = True if do_reload: await page.reload(wait_until="domcontentloaded") changed = True if clk is not None: xr, yr = clk x = int(xr * self.view_width) y = int(yr * self.view_height) await page.mouse.click(x, y) await asyncio.sleep(0.4) changed = True if scr is not None: await page.evaluate( f"window.scrollBy(0, {int(scr)})" ) await asyncio.sleep(0.15) changed = True if changed: self.current_url = page.url try: self.title = await page.title() except Exception: pass await self._snap(page) await browser.close() except Exception as exc: self.error = str(exc) import traceback; traceback.print_exc() finally: self._running = False async def _goto(self, page, url: str): import asyncio try: await page.goto(url, wait_until="domcontentloaded", timeout=20_000) self.current_url = page.url try: self.title = await page.title() except Exception: pass except Exception as exc: print(f"[WebView] 导航失败 {url}: {exc}") async def _snap(self, page): """截图并更新 self._screenshot""" try: data = await page.screenshot(type="png", full_page=False) with self._screenshot_lock: self._screenshot = data self.tex_dirty = True except Exception as exc: print(f"[WebView] 截图失败: {exc}")