Electron as GUI of Python Applications

important notice

Do NOT need to read this post. Please checkout the updated post for a better solution and a better explanation.

what

Electron (formerly Atom Shell) is a desktop node.js-powered “shell”. It is designed by Github and used to build Atom Editor.

Python is a simple and powerful programming language.

This post is a note about how to use Electron as the desktop GUI for Python applications.

but why?

Building desktop applications with Python is not easy.

Tkinter is the standard package for Python GUI, but it is very ugly.

The only mature and real-world solution is QT, with Python package PySide or PyQT, and Enaml based on that. However, PySide seems to have died, and PyQT is not free for commercial usages.

IPython, an enhanced shell for Python, has an interesting design: it has kernal, a qtconsole powered by QT, and notebook powered by web pages.

So I was thinking, why not use Electron as the “GUI shell” for the Python applications by embedding web pages? It is free, and hopefully elegant.

the architecture

The basic idea is rather simple:

The first way: Electron as the “launcher and minimal web browser”, loading the web pages dynamically generated by Python, where behind the web pages Python does all the heavy lifting.

The second way: Electron as the “launcher and minimal web browser”, loading the web pages statically written (the static files index.html, etc), where these pages communicate with Python by restful api or something like zeromq.

The first way is easy to understand and implemented, while the second way seems to provide more protentials.

After that, we could use PyInstaller to package the Python files, then use the built-in method of Electron to package all the HTML, CSS, Javascript files and Python binaries together. In the end we are able to distribute the generated binary files. Although we should notice that it may be easy to extract the souce codes in the distributed files.

a complete example

This is the example modified from the “hello world” of Electron, implementing the first way mentioned above. Nothing magic. The key point is to create a child process to run the python script and load the “home page” generated.

Install Python, node.js, then

pip install Flask
npm install electron-prebuilt -g
npm install request-promise -g

Then create a working directory. cd to the directory.

We need a basic package.json:

{
  "name"    : "your-app",
  "version" : "0.1.0",
  "main"    : "main.js",
  "dependencies": {
    "request-promise": "*",
    "electron-prebuilt": "*"
  }
}

as well as the main.js:

const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
electron.crashReporter.start();

var mainWindow = null;

app.on('window-all-closed', function() {
  //if (process.platform != 'darwin') {
    app.quit();
  //}
});

app.on('ready', function() {
  // call python?
  var subpy = require('child_process').spawn('python', ['./hello.py']);
  //var subpy = require('child_process').spawn('./dist/hello.exe');
  var rq = require('request-promise');
  var mainAddr = 'http://localhost:5000';

  var openWindow = function(){
    mainWindow = new BrowserWindow({width: 800, height: 600});
    // mainWindow.loadURL('file://' + __dirname + '/index.html');
    mainWindow.loadURL('http://localhost:5000');
    mainWindow.webContents.openDevTools();
    mainWindow.on('closed', function() {
      mainWindow = null;
      subpy.kill('SIGINT');
    });
  };

  var startUp = function(){
    rq(mainAddr)
      .then(function(htmlString){
        console.log('server started!');
        openWindow();
      })
      .catch(function(err){
        //console.log('waiting for the server start...');
        startUp();
      });
  };

  // fire!
  startUp();
});

Notice that in main.js, we spawn a child process for a Python application. Then we check whether the server has been up or not using unlimited loop (well, bad practice! we should actually check the time required and break the loop after some seconds). After the server has been up, we build an actual electron window pointing to the new local website index page.

Lastly, the hello.py:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function
import time
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World! This is powered by Python backend."

if __name__ == "__main__":
    print('oh hello')
    #time.sleep(5)
    app.run(host='127.0.0.1', port=5000)

After all the files are generated, we could simply run Electron inside bash:

electron . # . as the working directory

A desktop application should be launched as desired.

The full code could be viewed on GitHub.

further thinking

Electron is cool. But according to the issues in Atom Editor, the performance one of the main issue.

“Everything is a website” is also cool. But well, we may easily reach the limitations of web technologies.

That said, I believe “Electron as GUI for Python applications” is still an interesting approach about writing GUI in Python.