Skip to content

Instantly share code, notes, and snippets.

@nbrochu
Last active June 9, 2019 17:13
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nbrochu/a2e87541406c596a1b3d296fa2b011f2 to your computer and use it in GitHub Desktop.
Save nbrochu/a2e87541406c596a1b3d296fa2b011f2 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()
@nbrochu
Copy link
Author

nbrochu commented May 10, 2019

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

  • Native Win32 Window Creation and Customization
  • Fully configurable CEF (application and browser settings + command line switches)
  • Remote Debugging in Browser (Automatically opens if enabled)
  • Automatically creates JavaScript bindings to a provided Python class. No HTTP Servers or JSON involved.
  • Automatically spins up a thread for message processing and Python => JS communication when the CEF browser is ready.
  • Only 235 lines of readable code. 3 classes: Browser, API, Window.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment