12f2880201
v1 created a separate Win32 window — same problem as external Chrome. v2 creates CEF as a CHILD WINDOW of Kivy's SDL_app window: - No separate taskbar entry - No z-order fighting (CEF is INSIDE Kivy) - No desktop flash - CEF message loop pumped via Kivy Clock (main thread) - Resize handler attached so CEF follows Kivy window changes - build.spec includes cef_browser in hidden imports
193 lines
5.8 KiB
Python
193 lines
5.8 KiB
Python
"""
|
|
cef_browser.py v2 — Embedded Chromium INSIDE Kivy's SDL2 window
|
|
|
|
v1 created a separate Win32 window (same as external Chrome).
|
|
v2 creates CEF as a **child window** of Kivy's SDL_app window:
|
|
- No separate taskbar entry
|
|
- No z-order fighting
|
|
- No desktop flash
|
|
- CEF message loop pumped via Kivy Clock (main thread)
|
|
"""
|
|
|
|
import ctypes
|
|
import os
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from cefpython3 import cefpython as cef
|
|
CEF_AVAILABLE = True
|
|
except ImportError:
|
|
CEF_AVAILABLE = False
|
|
|
|
WS_CHILD = 0x40000000
|
|
WS_VISIBLE = 0x10000000
|
|
WS_CLIPSIBLINGS = 0x04000000
|
|
WS_CLIPCHILDREN = 0x02000000
|
|
SW_HIDE = 0
|
|
SW_SHOWNORMAL = 1
|
|
|
|
|
|
class CefBrowser:
|
|
def __init__(self):
|
|
self._browser = None
|
|
self._cef_initialized = False
|
|
self._child_hwnd = None
|
|
self._kivy_hwnd = None
|
|
self._clock_event = None
|
|
self._showing = False
|
|
|
|
# ── Public API ──────────────────────────────────────────────────
|
|
|
|
def show(self, url):
|
|
if not CEF_AVAILABLE:
|
|
return False
|
|
if not self._cef_initialized:
|
|
self._init_cef()
|
|
if self._browser is not None:
|
|
self._browser.Navigate(url)
|
|
self._show_in_kivy()
|
|
return True
|
|
return self._create_embedded(url)
|
|
|
|
def hide(self):
|
|
self._showing = False
|
|
if self._clock_event is not None:
|
|
try:
|
|
from kivy.clock import Clock
|
|
Clock.unschedule(self._clock_event)
|
|
except Exception:
|
|
pass
|
|
self._clock_event = None
|
|
if self._child_hwnd:
|
|
try:
|
|
ctypes.windll.user32.ShowWindow(self._child_hwnd, SW_HIDE)
|
|
except Exception:
|
|
pass
|
|
if self._browser is not None:
|
|
try:
|
|
self._browser.CloseBrowser(True)
|
|
except Exception:
|
|
pass
|
|
self._browser = None
|
|
if self._child_hwnd:
|
|
try:
|
|
ctypes.windll.user32.DestroyWindow(self._child_hwnd)
|
|
except Exception:
|
|
pass
|
|
self._child_hwnd = None
|
|
|
|
def shutdown(self):
|
|
self.hide()
|
|
if self._cef_initialized:
|
|
try:
|
|
cef.Shutdown()
|
|
except Exception:
|
|
pass
|
|
self._cef_initialized = False
|
|
|
|
def navigate(self, url):
|
|
if self._browser is not None:
|
|
self._browser.Navigate(url)
|
|
|
|
def is_showing(self):
|
|
return self._showing
|
|
|
|
def resize(self, width, height):
|
|
"""Called when Kivy window resizes — repositions CEF child."""
|
|
if self._child_hwnd:
|
|
ctypes.windll.user32.SetWindowPos(
|
|
self._child_hwnd, 0, 0, 0, width, height, 0x0004
|
|
)
|
|
if self._browser:
|
|
self._browser.SetBounds(0, 0, width, height)
|
|
|
|
# ── Internal ────────────────────────────────────────────────────
|
|
|
|
def _init_cef(self):
|
|
settings = {
|
|
"multi_threaded_message_loop": False,
|
|
"single_process": True,
|
|
"log_severity": cef.LOGSEVERITY_WARNING,
|
|
"user_agent": "Mozilla/5.0 KiwySignage/1.0",
|
|
"cache_path": str(
|
|
Path(os.environ.get("KIWY_DATA_DIR", ".")) / ".cef_cache"
|
|
),
|
|
}
|
|
cef.Initialize(settings=settings)
|
|
self._cef_initialized = True
|
|
|
|
def _get_kivy_hwnd(self):
|
|
if self._kivy_hwnd is not None:
|
|
return self._kivy_hwnd
|
|
try:
|
|
import win32gui
|
|
hwnd = win32gui.FindWindow("SDL_app", None)
|
|
if hwnd:
|
|
self._kivy_hwnd = hwnd
|
|
return hwnd
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
def _create_embedded(self, url):
|
|
kivy_hwnd = self._get_kivy_hwnd()
|
|
if not kivy_hwnd:
|
|
return False
|
|
|
|
user32 = ctypes.windll.user32
|
|
rect = (ctypes.c_long * 4)()
|
|
user32.GetClientRect(kivy_hwnd, ctypes.byref(rect))
|
|
w, h = rect[2], rect[3]
|
|
|
|
hinstance = ctypes.windll.kernel32.GetModuleHandleW(None)
|
|
self._child_hwnd = user32.CreateWindowExW(
|
|
0, b'#32770', b'',
|
|
WS_CHILD | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
|
|
0, 0, w, h, kivy_hwnd, 0, hinstance, 0,
|
|
)
|
|
if not self._child_hwnd:
|
|
return False
|
|
|
|
winfo = cef.WindowInfo()
|
|
winfo.SetAsChild(self._child_hwnd, [0, 0, w, h])
|
|
self._browser = cef.CreateBrowserSync(
|
|
window_info=winfo,
|
|
settings={"background_color": 0x00000000},
|
|
url=url,
|
|
)
|
|
self._showing = True
|
|
self._show_in_kivy()
|
|
self._start_clock_pump()
|
|
return True
|
|
|
|
def _show_in_kivy(self):
|
|
if not self._child_hwnd:
|
|
return
|
|
kivy_hwnd = self._get_kivy_hwnd()
|
|
if kivy_hwnd:
|
|
user32 = ctypes.windll.user32
|
|
rect = (ctypes.c_long * 4)()
|
|
user32.GetClientRect(kivy_hwnd, ctypes.byref(rect))
|
|
user32.SetWindowPos(
|
|
self._child_hwnd, 0, 0, 0, rect[2], rect[3], 0x0004
|
|
)
|
|
ctypes.windll.user32.ShowWindow(self._child_hwnd, SW_SHOWNORMAL)
|
|
self._showing = True
|
|
|
|
def _start_clock_pump(self):
|
|
if self._clock_event is not None:
|
|
return
|
|
|
|
def _pump(dt):
|
|
if self._cef_initialized:
|
|
try:
|
|
cef.MessageLoopWork()
|
|
except Exception:
|
|
pass
|
|
if self._showing:
|
|
from kivy.clock import Clock
|
|
self._clock_event = Clock.schedule_once(_pump, 0.01)
|
|
|
|
from kivy.clock import Clock
|
|
self._clock_event = Clock.schedule_once(_pump, 0)
|