Last active
June 9, 2019 17:13
-
-
Save nbrochu/a2e87541406c596a1b3d296fa2b011f2 to your computer and use it in GitHub Desktop.
Python Pseudo-Electron Boilerplate (Windows)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
This is for Windows users but you could replace the Window class with native windowing for another OS. Written and tested on Python 3.7. A little proof of concept for a minimal pseudo-Electron for web-based GUIs.
I got annoyed with the current Web View + Python offerings. I don't want HTTP shenanigans. I don't want opinionated abstractions that use 10% of the underlying technology. I don't want large, bloated code bases. Enough! The problem that needs to be solved isn't that complicated.
Complaining solves nothing though so I sat down and tried to see if I could pull off a quick MVP. Turns out it's surprisingly easy!
Features
My conclusion is that CEF Python is criminally underrated. CEF is a beast and the Python bindings are a massive undertaking. Give it a star and try it out, it's a fantastic project.