Skip to content

Instantly share code, notes, and snippets.

@rgarrigue
Forked from nbrochu/browser.py
Created May 10, 2019 06:55
Show Gist options
  • Save rgarrigue/1c024282fe9f63571a2d9727766a9c33 to your computer and use it in GitHub Desktop.
Save rgarrigue/1c024282fe9f63571a2d9727766a9c33 to your computer and use it in GitHub Desktop.
Python Pseudo-Electron Boilerplate (Windows)
import os
import sys
import math
import time
import threading
import webbrowser # To launch the remote debugging page
import win32api #
import win32con # pip install pywin32
import win32gui #
from cefpython3 import cefpython as cef # pip install cefpython3
sys.excepthook = cef.ExceptHook
class API:
def hello(self, name, js_callback): # Available in JS as 'window.python.hello'
js_callback.Call(f"Hello {name}!")
def add(self, n1, n2, js_callback): # Available in JS as 'window.python.add'
js_callback.Call(n1 + n2)
class Browser:
def __init__(
self,
title="Browser",
icon="", # Path to a .ICO file
url="https://www.python.org",
api=API,
width=1280,
height=720,
resizable=True,
remote_debugging=True,
cef_application_settings=None, # https://github.com/cztomczak/cefpython/blob/master/api/ApplicationSettings.md
cef_browser_settings=None, # https://github.com/cztomczak/cefpython/blob/master/api/BrowserSettings.md
cef_switches=None, # https://github.com/cztomczak/cefpython/blob/master/api/CommandLineSwitches.md
):
self.browser = None
self.api = api()
self.remote_debugging = remote_debugging
self.stop = threading.Event()
cef_application_settings = cef_application_settings or dict()
cef_application_settings = {
**cef_application_settings,
**{"multi_threaded_message_loop": False, "remote_debugging_port": 56741 if self.remote_debugging else -1}
}
cef_browser_settings = cef_browser_settings or dict()
cef_switches = cef_switches or dict()
cef.Initialize(settings=cef_application_settings, switches=cef_switches)
cef.DpiAware.EnableHighDpiSupport()
window = Window.create(
title=title,
width=width,
height=height,
icon=icon,
resizable=resizable,
on_close=Browser.on_close,
on_resize=Browser.on_resize,
on_focus=Browser.on_focus,
on_erase_background=Browser.on_erase_background
)
window_info = cef.WindowInfo()
window_info.SetAsChild(window.window_handle)
self.create_browser(window_info, cef_browser_settings, url)
cef.MessageLoop()
self.stop.set()
cef.Shutdown()
def loop(self):
if self.remote_debugging:
webbrowser.open("http://127.0.0.1:56741", new=2)
while not self.stop.is_set():
self.browser.ExecuteJavascript("console.log('Hi from Python!')")
time.sleep(1)
def create_browser(self, window_info, settings, url):
assert(cef.IsThread(cef.TID_UI))
self.browser = cef.CreateBrowserSync(window_info=window_info, settings=settings, url=url)
# Define a load handler to start our side-loop in a thread when the browser is ready
class LoadHandler:
def __init__(self, parent):
self.parent = parent
def OnLoadEnd(self, browser, **kwargs):
self.parent._start_loop()
self.browser.SetClientHandler(LoadHandler(self))
# Make our API instance available as window.python
bindings = cef.JavascriptBindings()
bindings.SetObject("python", self.api)
self.browser.SetJavascriptBindings(bindings)
def _start_loop(self):
loop_thread = threading.Thread(target=self.loop)
loop_thread.start()
@staticmethod
def on_close(*args):
browser = cef.GetBrowserByWindowHandle(args[0])
browser.CloseBrowser(True)
return win32gui.DefWindowProc(*args)
@staticmethod
def on_resize(*args):
return cef.WindowUtils.OnSize(*args)
@staticmethod
def on_focus(*args):
return cef.WindowUtils.OnSetFocus(*args)
@staticmethod
def on_erase_background(*args):
return cef.WindowUtils.OnEraseBackground(*args)
class Window:
def __init__(self, window_handle):
self.window_handle = window_handle
@classmethod
def create(
cls,
title="Browser",
class_name="browser.window",
width=1280,
height=720,
icon="",
resizable=True,
on_close=None,
on_destroy=None,
on_resize=None,
on_focus=None,
on_erase_background=None
):
# Assemble Window Procedures
window_procedures = {
win32con.WM_SIZE: on_resize if on_resize is not None else cls.default_window_procedure,
win32con.WM_SETFOCUS: on_focus if on_focus is not None else cls.default_window_procedure,
win32con.WM_ERASEBKGND: on_erase_background if on_erase_background is not None else cls.default_window_procedure,
win32con.WM_CLOSE: on_close if on_close is not None else cls.default_window_procedure,
win32con.WM_DESTROY: on_destroy if on_destroy is not None else cls.on_destroy
}
# Attempt to Register a New Window Class
window_class = win32gui.WNDCLASS()
window_class.hInstance = win32api.GetModuleHandle(None)
window_class.lpszClassName = class_name
window_class.style = win32con.CS_VREDRAW | win32con.CS_HREDRAW
window_class.hbrBackground = win32con.COLOR_WINDOW
window_class.hCursor = win32gui.LoadCursor(0, win32con.IDC_ARROW)
window_class.lpfnWndProc = window_procedures
try:
win32gui.RegisterClass(window_class)
except:
pass
# Calculate Window Position (Centered)
display_width = win32api.GetSystemMetrics(win32con.SM_CXSCREEN)
display_height = win32api.GetSystemMetrics(win32con.SM_CYSCREEN)
x = max(int(math.floor((display_width - width) / 2)), 0)
y = max(int(math.floor((display_height - height) / 2)), 0)
# Assemble Window Styles
if resizable:
window_styles = win32con.WS_OVERLAPPEDWINDOW | win32con.WS_CLIPCHILDREN | win32con.WS_VISIBLE
else:
window_styles = win32con.WS_OVERLAPPED | win32con.WS_CAPTION | win32con.WS_SYSMENU | \
win32con.WS_BORDER | win32con.WS_MINIMIZEBOX | win32con.WS_CLIPCHILDREN | \
win32con.WS_VISIBLE
# Create Window and Get Window Handle
window_handle = win32gui.CreateWindow(
class_name,
title,
window_styles,
x, y,
width, height,
0, 0,
window_class.hInstance,
None
)
# Set Window Icons
if os.path.isfile(icon):
x = win32api.GetSystemMetrics(win32con.SM_CXICON)
y = win32api.GetSystemMetrics(win32con.SM_CYICON)
icon_big = win32gui.LoadImage(0, icon, win32con.IMAGE_ICON, x, y, win32con.LR_LOADFROMFILE)
x = win32api.GetSystemMetrics(win32con.SM_CXSMICON)
y = win32api.GetSystemMetrics(win32con.SM_CYSMICON)
icon_small = win32gui.LoadImage(0, icon, win32con.IMAGE_ICON, x, y, win32con.LR_LOADFROMFILE)
win32api.SendMessage(window_handle, win32con.WM_SETICON, win32con.ICON_BIG, icon_big)
win32api.SendMessage(window_handle, win32con.WM_SETICON, win32con.ICON_SMALL, icon_small)
return cls(window_handle)
@classmethod
def default_window_procedure(cls, window_handle, message, wparam, lparam):
return win32gui.DefWindowProc(window_handle, message, wparam, lparam)
@classmethod
def on_destroy(cls, window_handle, message, wparam, lparam):
win32gui.PostQuitMessage(0)
return 0
if __name__ == "__main__":
Browser()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment