├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── main.js ├── old-post-backup ├── .gitignore ├── README.md ├── hello.py ├── main.js └── package.json ├── package.json ├── pycalc ├── .gitignore ├── api.py ├── calc.py └── requirements.txt └── renderer.js /.gitignore: -------------------------------------------------------------------------------- 1 | api.spec 2 | .venv/ 3 | node_modules/ 4 | build/ 5 | dist/ 6 | pycalcdist/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Github user @fyears 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Electron as GUI of Python Applications (Updated) 2 | 3 | ## tl;dr 4 | 5 | This post shows how to use Electron as the GUI component of Python applications. (Updated version of one of my previous posts.) The frontend and backend communicate with each other using `zerorpc`. The complete code is on [GitHub repo](https://github.com/fyears/electron-python-example). 6 | 7 | ## important notice 8 | 9 | **Disclaimer on Dec 2019: This article was originally written in 2017, and I haven't updated or maintained this repo for a long time. Right now (Dec 2019), the code in this article may be outdated, and may or may not be working!!!** 10 | 11 | The following are copied from my [original post](https://www.fyears.org/2017/02/electron-as-gui-of-python-apps-updated.html). They should be the same. **If there are inconsistencies, the `README.md` on the GitHub repo is more accurate.** 12 | 13 | ## original post and debates 14 | 15 | ### attention 16 | 17 | The current post is a **updated** version of the [previous post](https://www.fyears.org/2015/06/electron-as-gui-of-python-apps.html) a few years before. Readers do **NOT** need to read the previous post if haven't. 18 | 19 | ### debates 20 | 21 | I didn't expect that the previous post attracted so many visitors. Some other people even posted it on Hacker News and Reddit. The previous post also attracted some criticisms. Here I would like to share my replies to some debates. 22 | 23 | #### Do you know `Tkinter`, `GTK`, `QT` (`PySide` and `PyQT`), `wxPython`, `Kivy`, [`thrust`](https://github.com/breach/thrust), ...? 24 | 25 | Yes, I know at least their existences and try a few of them. I still think `QT` is the best among them. BTW, [`pyotherside`](https://github.com/thp/pyotherside) is one of the actively maintaining bindings for Python. I am just offering another "web technology oriented" way here. 26 | 27 | #### ... And [`cefpython`](https://github.com/cztomczak/cefpython). 28 | 29 | It's more or less be in "lower level" than where Electron is. For example, `PySide` is based on it. 30 | 31 | #### I can directly write things in JavaScript! 32 | 33 | Correct. Unless some libraries such as `numpy` are not available in JS. Moreover, the original intention is using Electron / JavaScript / web technologies to **enhance Python Applications**. 34 | 35 | #### I can use [`QT WebEngine`](http://doc.qt.io/qt-5/qtwebengine-index.html). 36 | 37 | Go ahead and give it a try. But since you are using "web engine", why not also give Electron a try? 38 | 39 | #### You have two runtimes! 40 | 41 | Yes. One for JavaScript and one for Python. Unfortunately, Python and JavaScript are dynamic languages, which usually need run-time support. 42 | 43 | ## the architectures and the choice 44 | 45 | In the previous post, I showed an example architecture: Python to build up a `localhost` server, then Electron is just a local web browser. 46 | 47 | ```text 48 | start 49 | | 50 | V 51 | +------------+ 52 | | | start 53 | | +-------------> +-------------------+ 54 | | electron | sub process | | 55 | | | | python web server | 56 | | (basically | http | | 57 | | browser) | <-----------> | (business logic) | 58 | | | communication | | 59 | | | | (all html/css/js) | 60 | | | | | 61 | +------------+ +-------------------+ 62 | ``` 63 | 64 | That is **just one not-so-efficient solution**. 65 | 66 | Let's reconsider the core needs here: we have a Python application, and a Node.js application (Electron). How to combine them and communicate with each other? 67 | 68 | **We actually need an interprocess communication (IPC) mechanism.** It is unavoidable unless Python and JavaScript have direct `FFI` for each other. 69 | 70 | HTTP is merely one of the popular ways to implement IPC, and it was merely the first thing came up to my mind when I was writing the previous post. 71 | 72 | We have more choices. 73 | 74 | We can (and should) use `socket`. Then, based on that, we want an abstract messaging layer that could be implemented with [`ZeroMq`](http://zeromq.org/) that is one of the best messaging libraries. Moreover, based on that, we need to define some schema upon raw data that could be implemented with [`zerorpc`](http://www.zerorpc.io/). 75 | 76 | (Luckily, `zerorpc` fits our needs here because it supports Python and Node.js. For more general languages support, check out [`gRPC`](http://www.grpc.io/).) 77 | 78 | **Thus, in this post, I will show another example using `zerorpc` for communication as follows, which should be more efficient than what I showed in my previous post.** 79 | 80 | ```text 81 | start 82 | | 83 | V 84 | +--------------------+ 85 | | | start 86 | | electron +-------------> +------------------+ 87 | | | sub process | | 88 | | (browser) | | python server | 89 | | | | | 90 | | (all html/css/js) | | (business logic) | 91 | | | zerorpc | | 92 | | (node.js runtime, | <-----------> | (zeromq server) | 93 | | zeromq client) | communication | | 94 | | | | | 95 | +--------------------+ +------------------+ 96 | ``` 97 | 98 | ## preparation 99 | 100 | Attention: the example could be successfully run on my Windows 10 machine with Python 3.6, Electron 1.7, Node.js v6. 101 | 102 | We need the python application, `python`, `pip`, `node`, `npm`, available in command line. For using `zerorpc`, we also need the C/C++ compilers (`cc` and `c++` in the command line, and/or MSVC on Windows). 103 | 104 | The structure of this project is 105 | 106 | ```text 107 | . 108 | |-- index.html 109 | |-- main.js 110 | |-- package.json 111 | |-- renderer.js 112 | | 113 | |-- pycalc 114 | | |-- api.py 115 | | |-- calc.py 116 | | `-- requirements.txt 117 | | 118 | |-- LICENSE 119 | `-- README.md 120 | ``` 121 | 122 | As shown above, the Python application is wrapped in a subfolder. In this example, Python application `pycalc/calc.py` provides a function: `calc(text)` that could take a text like `1 + 1 / 2` and return the result like `1.5` (assuming it be like `eval()`). The `pycalc/api.py` is what we are going to figure out. 123 | 124 | And the `index.html`, `main.js`, `package.json` and `renderer.js` are modified from [`electron-quick-start`](https://github.com/electron/electron-quick-start). 125 | 126 | ### Python part 127 | 128 | First of all, since we already have the Python application running, the Python environment should be fine. I strongly recommend developing Python applications in `virtualenv`. 129 | 130 | Try install `zerorpc`, and `pyinstaller` (for packaging). On Linux / Ubuntu we may need to run `sudo apt-get install libzmq3-dev` **before** `pip install`. 131 | 132 | ```bash 133 | pip install zerorpc 134 | pip install pyinstaller 135 | 136 | # for windows only 137 | pip install pypiwin32 # for pyinstaller 138 | ``` 139 | 140 | If properly configured, the above commands should have no problem. Otherwise, please check out the guides online. 141 | 142 | ### Node.js / Electron part 143 | 144 | Secondly, try to configure the Node.js and Electron environment. I assume that `node` and `npm` can be invoked in the command line and are of latest versions. 145 | 146 | We need to configure the `package.json`, especially the `main` entry: 147 | 148 | ```json 149 | { 150 | "name": "pretty-calculator", 151 | "main": "main.js", 152 | "scripts": { 153 | "start": "electron ." 154 | }, 155 | "dependencies": { 156 | "zerorpc": "git+https://github.com/0rpc/zerorpc-node.git" 157 | }, 158 | "devDependencies": { 159 | "electron": "^1.7.6", 160 | "electron-packager": "^9.0.1" 161 | } 162 | } 163 | ``` 164 | 165 | Clean the caches: 166 | 167 | ```bash 168 | # On Linux / OS X 169 | # clean caches, very important!!!!! 170 | rm -rf ~/.node-gyp 171 | rm -rf ~/.electron-gyp 172 | rm -rf ./node_modules 173 | ``` 174 | 175 | ```powershell 176 | # On Window PowerShell (not cmd.exe!!!) 177 | # clean caches, very important!!!!! 178 | Remove-Item "$($env:USERPROFILE)\.node-gyp" -Force -Recurse -ErrorAction Ignore 179 | Remove-Item "$($env:USERPROFILE)\.electron-gyp" -Force -Recurse -ErrorAction Ignore 180 | Remove-Item .\node_modules -Force -Recurse -ErrorAction Ignore 181 | ``` 182 | 183 | Then run `npm`: 184 | 185 | ```bash 186 | # 1.7.6 is the version of electron 187 | # It's very important to set the electron version correctly!!! 188 | # check out the version value in your package.json 189 | npm install --runtime=electron --target=1.7.6 190 | 191 | # verify the electron binary and its version by opening it 192 | ./node_modules/.bin/electron 193 | ``` 194 | 195 | ~~The `npm install` will install `zerorpc-node` from [my fork](https://github.com/0rpc/zerorpc-node/pull/84) to skip building from sources.~~ Updated: the pull request of `zerorpc-node` was [merged](https://github.com/0rpc/zerorpc-node/pull/84) so everyone is encouraged to use the official repo instead. 196 | 197 | (Consider [adding `./.npmrc`](https://docs.npmjs.com/files/npmrc) in the project folder if necessary.) 198 | 199 | All libraries should be fine now. 200 | 201 | #### optional: building from sources 202 | 203 | If the above installation causes any errors **even while setting the electron version correctly**, we may have to build the packages from sources. 204 | 205 | Ironically, to compile Node.js C/C++ native codes, we need to have `python2` configured, no matter what Python version we are using for our Python application. Check out the [official guide](https://github.com/nodejs/node-gyp). 206 | 207 | Especially, if working on Windows, open PowerShell **as Administrator**, and run `npm install --global --production windows-build-tools` to install a separated Python 2.7 in `%USERPROFILE%\.windows-build-tools\python27` and other required VS libraries. We only need to do it at once. 208 | 209 | Then, **clean `~/.node-gyp` and `./node_modules` caches as described above at first.** 210 | 211 | Set the `npm` [for Electron](https://github.com/electron/electron/blob/master/docs/tutorial/using-native-node-modules.md), and install the required libraries. 212 | 213 | Set the environment variables for Linux (Ubuntu) / OS X / Windows: 214 | 215 | ```bash 216 | # On Linux / OS X: 217 | 218 | # env 219 | export npm_config_target=1.7.6 # electron version 220 | export npm_config_runtime=electron 221 | export npm_config_disturl=https://atom.io/download/electron 222 | export npm_config_build_from_source=true 223 | 224 | # may not be necessary 225 | #export npm_config_arch=x64 226 | #export npm_config_target_arch=x64 227 | 228 | npm config ls 229 | ``` 230 | 231 | ```powershell 232 | # On Window PowerShell (not cmd.exe!!!) 233 | 234 | $env:npm_config_target="1.7.6" # electron version 235 | $env:npm_config_runtime="electron" 236 | $env:npm_config_disturl="https://atom.io/download/electron" 237 | $env:npm_config_build_from_source="true" 238 | 239 | # may not be necessary 240 | #$env:npm_config_arch="x64" 241 | #$env:npm_config_target_arch="x64" 242 | 243 | npm config ls 244 | ``` 245 | 246 | Then install things: 247 | 248 | ```bash 249 | # in the same shell as above!!! 250 | # because you want to make good use of the above environment variables 251 | 252 | # install everything based on the package.json 253 | npm install 254 | 255 | # verify the electron binary and its version by opening it 256 | ./node_modules/.bin/electron 257 | ``` 258 | 259 | (Consider [adding `./.npmrc`](https://docs.npmjs.com/files/npmrc) in the project folder if necessary.) 260 | 261 | ## core functions 262 | 263 | ### Python part 264 | 265 | We want to build up a ZeroMQ server in Python end. 266 | 267 | Put `calc.py` into folder `pycalc/`. Then create another file `pycalc/api.py`. Check [`zerorpc-python`](https://github.com/0rpc/zerorpc-python) for reference. 268 | 269 | ```python 270 | from __future__ import print_function 271 | from calc import calc as real_calc 272 | import sys 273 | import zerorpc 274 | 275 | class CalcApi(object): 276 | def calc(self, text): 277 | """based on the input text, return the int result""" 278 | try: 279 | return real_calc(text) 280 | except Exception as e: 281 | return 0.0 282 | def echo(self, text): 283 | """echo any text""" 284 | return text 285 | 286 | def parse_port(): 287 | return 4242 288 | 289 | def main(): 290 | addr = 'tcp://127.0.0.1:' + parse_port() 291 | s = zerorpc.Server(CalcApi()) 292 | s.bind(addr) 293 | print('start running on {}'.format(addr)) 294 | s.run() 295 | 296 | if __name__ == '__main__': 297 | main() 298 | ``` 299 | 300 | To test the correctness, run `python pycalc/api.py` in one terminal. Then **open another terminal**, run this command and see the result: 301 | 302 | ```bash 303 | zerorpc tcp://localhost:4242 calc "1 + 1" 304 | ## connecting to "tcp://localhost:4242" 305 | ## 2.0 306 | ``` 307 | 308 | After debugging, **remember to terminate the Python function**. 309 | 310 | Actually, this is yet another **server**, communicated over `zeromq` over TCP, rather than traditional web server over HTTP. 311 | 312 | ### Node.js / Electron part 313 | 314 | Basic idea: In the main process, spawn the Python child process and create the window. In the render process, use Node.js runtime and `zerorpc` library to communicate with Python child process. All the HTML / JavaScript / CSS are managed by Electron, instead of by Python web server (The example in the previous post used Python web server to dynamically generate HTML codes). 315 | 316 | In `main.js`, these are default codes to start from, with nothing special: 317 | 318 | ```js 319 | // main.js 320 | 321 | const electron = require('electron') 322 | const app = electron.app 323 | const BrowserWindow = electron.BrowserWindow 324 | const path = require('path') 325 | 326 | let mainWindow = null 327 | const createWindow = () => { 328 | mainWindow = new BrowserWindow({width: 800, height: 600}) 329 | mainWindow.loadURL(require('url').format({ 330 | pathname: path.join(__dirname, 'index.html'), 331 | protocol: 'file:', 332 | slashes: true 333 | })) 334 | mainWindow.webContents.openDevTools() 335 | mainWindow.on('closed', () => { 336 | mainWindow = null 337 | }) 338 | } 339 | app.on('ready', createWindow) 340 | app.on('window-all-closed', () => { 341 | if (process.platform !== 'darwin') { 342 | app.quit() 343 | } 344 | }) 345 | app.on('activate', () => { 346 | if (mainWindow === null) { 347 | createWindow() 348 | } 349 | }) 350 | ``` 351 | 352 | We want to add some code to spawn Python child process: 353 | 354 | ```js 355 | // add these to the end or middle of main.js 356 | 357 | let pyProc = null 358 | let pyPort = null 359 | 360 | const selectPort = () => { 361 | pyPort = 4242 362 | return pyPort 363 | } 364 | 365 | const createPyProc = () => { 366 | let port = '' + selectPort() 367 | let script = path.join(__dirname, 'pycalc', 'api.py') 368 | pyProc = require('child_process').spawn('python', [script, port]) 369 | if (pyProc != null) { 370 | console.log('child process success') 371 | } 372 | } 373 | 374 | const exitPyProc = () => { 375 | pyProc.kill() 376 | pyProc = null 377 | pyPort = null 378 | } 379 | 380 | app.on('ready', createPyProc) 381 | app.on('will-quit', exitPyProc) 382 | ``` 383 | 384 | In `index.html`, we have an `` for input, and `
` for output: 385 | 386 | ```html 387 | 388 | 389 | 390 | 391 | 392 | Hello Calculator! 393 | 394 | 395 |

Hello Calculator!

396 |

Input something like 1 + 1.

397 |

This calculator supports +-*/^(), 398 | whitespaces, and integers and floating numbers.

399 | 400 |
401 | 402 | 405 | 406 | ``` 407 | 408 | In `renderer.js`, we have codes for initialization of `zerorpc` **client**, and the code for watching the changes in the input. Once the user types some formula into the text area, the JS send the text to Python backend and retrieve the computed result. 409 | 410 | ```js 411 | // renderer.js 412 | 413 | const zerorpc = require("zerorpc") 414 | let client = new zerorpc.Client() 415 | client.connect("tcp://127.0.0.1:4242") 416 | 417 | let formula = document.querySelector('#formula') 418 | let result = document.querySelector('#result') 419 | formula.addEventListener('input', () => { 420 | client.invoke("calc", formula.value, (error, res) => { 421 | if(error) { 422 | console.error(error) 423 | } else { 424 | result.textContent = res 425 | } 426 | }) 427 | }) 428 | formula.dispatchEvent(new Event('input')) 429 | ``` 430 | 431 | ## running 432 | 433 | Run this to see the magic: 434 | 435 | ```bash 436 | ./node_modules/.bin/electron . 437 | ``` 438 | 439 | Awesome! 440 | 441 | If something like dynamic linking errors shows up, try to clean the caches and install the libraries again. 442 | 443 | ```bash 444 | rm -rf node_modules 445 | rm -rf ~/.node-gyp ~/.electron-gyp 446 | 447 | npm install 448 | ``` 449 | 450 | ## packaging 451 | 452 | Some people are asking for the packaging. This is easy: apply the knowledge of how to package Python applications and Electron applications. 453 | 454 | ### Python part 455 | 456 | User [PyInstaller](http://www.pyinstaller.org/). 457 | 458 | Run the following in the terminal: 459 | 460 | ```bash 461 | pyinstaller pycalc/api.py --distpath pycalcdist 462 | 463 | rm -rf build/ 464 | rm -rf api.spec 465 | ``` 466 | 467 | If everything goes well, the `pycalcdist/api/` folder should show up, as well as the executable inside that folder. This is the complete independent Python executable that could be moved to somewhere else. 468 | 469 | **Attention: the independent Python executable has to be generated!** Because the target machine we want to distribute to may not have correct Python shell and/or required Python libraries. It's almost impossible to just copy the Python source codes. 470 | 471 | ### Node.js / Electron part 472 | 473 | This is tricky because of the Python executable. 474 | 475 | In the above example code, I write 476 | 477 | ```js 478 | // part of main.js 479 | let script = path.join(__dirname, 'pycalc', 'api.py') 480 | pyProc = require('child_process').spawn('python', [script, port]) 481 | ``` 482 | 483 | However, once we package the Python code, **we should no longer `spawn` Python script**. Instead, **we should `execFile` the generated excutable**. 484 | 485 | Electron doesn't provide functions to check whether the app is under distributed or not (at least I don't find it). So I use a workaround here: check whether the Python executable has been generated or not. 486 | 487 | In `main.js`, add the following functions: 488 | 489 | ```js 490 | // main.js 491 | 492 | const PY_DIST_FOLDER = 'pycalcdist' 493 | const PY_FOLDER = 'pycalc' 494 | const PY_MODULE = 'api' // without .py suffix 495 | 496 | const guessPackaged = () => { 497 | const fullPath = path.join(__dirname, PY_DIST_FOLDER) 498 | return require('fs').existsSync(fullPath) 499 | } 500 | 501 | const getScriptPath = () => { 502 | if (!guessPackaged()) { 503 | return path.join(__dirname, PY_FOLDER, PY_MODULE + '.py') 504 | } 505 | if (process.platform === 'win32') { 506 | return path.join(__dirname, PY_DIST_FOLDER, PY_MODULE, PY_MODULE + '.exe') 507 | } 508 | return path.join(__dirname, PY_DIST_FOLDER, PY_MODULE, PY_MODULE) 509 | } 510 | ``` 511 | 512 | And change the function `createPyProc` to this: 513 | 514 | ```js 515 | // main.js 516 | // the improved version 517 | const createPyProc = () => { 518 | let script = getScriptPath() 519 | let port = '' + selectPort() 520 | 521 | if (guessPackaged()) { 522 | pyProc = require('child_process').execFile(script, [port]) 523 | } else { 524 | pyProc = require('child_process').spawn('python', [script, port]) 525 | } 526 | 527 | if (pyProc != null) { 528 | //console.log(pyProc) 529 | console.log('child process success on port ' + port) 530 | } 531 | } 532 | ``` 533 | 534 | The key point is, check whether the `*dist` folder has been generated or not. If generated, it means we are in "production" mode, `execFile` the executable directly; otherwise, `spawn` the script using a Python shell. 535 | 536 | In the end, run [`electron-packager`](https://github.com/electron-userland/electron-packager) to generate the bundled application. We also want to exclude some folders (For example, `pycalc/` is no longer needed to be bundled), **using regex** (instead of glob, surprise!). The name, platform, and arch are inferred from `package.json`. For more options, check out the docs. 537 | 538 | ```bash 539 | # we need to make sure we have bundled the latest Python code 540 | # before running the below command! 541 | # Or, actually, we could bundle the Python executable later, 542 | # and copy the output into the correct distributable Electron folder... 543 | 544 | ./node_modules/.bin/electron-packager . --overwrite --ignore="pycalc$" --ignore="\.venv" --ignore="old-post-backup" 545 | ## Packaging app for platform win32 x64 using electron v1.7.6 546 | ## Wrote new app to ./pretty-calculator-win32-x64 547 | ``` 548 | 549 | I do not check `asar` format's availability. I guess it will slow down the startup speed. 550 | 551 | After that, we have the generated packaged Electron in current directory! For me, the result is `./pretty-calculator-win32-x64/`. On my machine, it's around 170 MB (Electron itself occupies more than 84.2 MB). I also tried to compress it, the generated `.7z` file is around 43.3 MB. 552 | 553 | Copy / Move the folder(s) to anywhere or other machines to check the result! :-) 554 | 555 | ## further faq 556 | 557 | ### full code? 558 | 559 | See [GitHub `electron-python-example`](https://github.com/fyears/electron-python-example). 560 | 561 | ### solutions to errors 562 | 563 | [issue #6](https://github.com/fyears/electron-python-example/issues/6): `... failed with KeyError` 564 | 565 | [issue #7](https://github.com/fyears/electron-python-example/issues/7): `Uncaught Error: Module version mismatch. Expected 50, got 48.` 566 | 567 | Uninstall everything, **set up the npm environment variables correctly especially for the electron version**, remember to `activate` the virtualenv if using Python `virtualenv`. 568 | 569 | ### further optimization? 570 | 571 | Trim some unnecessary files in Python executable by configuring `pyinstaller` further. Trim Electron (is it possible?). Use even faster IPC methods (though `ZeroMQ` is one of the fastest in most cases). 572 | 573 | What's more, use QT (huh??), rewrite necessary codes in Node.js / Go / C / C++ (huh??). You name it. 574 | 575 | ### Can I use other programming languages besides Python? 576 | 577 | Sure. The solution described here can also be applied to any other programming languages besides Python. Except that, if you want to use Electron as GUI of C/C++ applications, I strongly recommend using Node.js native C/C++ communication mechanism instead of using IPC. Moreover, if you have Java, C# application, using `Swing` or `WPF` are much more mature choices. 578 | 579 | But, unfortunately, Electron is not for mobile applications and it makes little sense even if possible. Please use native GUI on those platforms. 580 | 581 | ## conclusion and further thinkings 582 | 583 | It's still a promising solution. For drawing interface, we want to use some markup language for declarative UI. HTML happens to be one of the best choices, and its companions JS and CSS happen to have one of the most optimized renderers: the web browser. That's why I am (so) interested in using web technologies for GUI when possible. A few years before the web browsers were not powerful enough, but the story is kind of different now. 584 | 585 | I hope this post is helpful to you. 586 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello Calculator! 6 | 7 | 8 |

Hello Calculator!

9 |

We are using Node.js , 10 | Chromium , 11 | and Electron .

12 |

Input something like 1 + 1.

13 |

This calculator supports +-*/^(), 14 | whitespaces, and integers and floating numbers.

15 | 16 |
17 | 18 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron') 2 | const app = electron.app 3 | const BrowserWindow = electron.BrowserWindow 4 | const path = require('path') 5 | 6 | 7 | /************************************************************* 8 | * py process 9 | *************************************************************/ 10 | 11 | const PY_DIST_FOLDER = 'pycalcdist' 12 | const PY_FOLDER = 'pycalc' 13 | const PY_MODULE = 'api' // without .py suffix 14 | 15 | let pyProc = null 16 | let pyPort = null 17 | 18 | const guessPackaged = () => { 19 | const fullPath = path.join(__dirname, PY_DIST_FOLDER) 20 | return require('fs').existsSync(fullPath) 21 | } 22 | 23 | const getScriptPath = () => { 24 | if (!guessPackaged()) { 25 | return path.join(__dirname, PY_FOLDER, PY_MODULE + '.py') 26 | } 27 | if (process.platform === 'win32') { 28 | return path.join(__dirname, PY_DIST_FOLDER, PY_MODULE, PY_MODULE + '.exe') 29 | } 30 | return path.join(__dirname, PY_DIST_FOLDER, PY_MODULE, PY_MODULE) 31 | } 32 | 33 | const selectPort = () => { 34 | pyPort = 4242 35 | return pyPort 36 | } 37 | 38 | const createPyProc = () => { 39 | let script = getScriptPath() 40 | let port = '' + selectPort() 41 | 42 | if (guessPackaged()) { 43 | pyProc = require('child_process').execFile(script, [port]) 44 | } else { 45 | pyProc = require('child_process').spawn('python', [script, port]) 46 | } 47 | 48 | if (pyProc != null) { 49 | //console.log(pyProc) 50 | console.log('child process success on port ' + port) 51 | } 52 | } 53 | 54 | const exitPyProc = () => { 55 | pyProc.kill() 56 | pyProc = null 57 | pyPort = null 58 | } 59 | 60 | app.on('ready', createPyProc) 61 | app.on('will-quit', exitPyProc) 62 | 63 | 64 | /************************************************************* 65 | * window management 66 | *************************************************************/ 67 | 68 | let mainWindow = null 69 | 70 | const createWindow = () => { 71 | mainWindow = new BrowserWindow({width: 800, height: 600}) 72 | mainWindow.loadURL(require('url').format({ 73 | pathname: path.join(__dirname, 'index.html'), 74 | protocol: 'file:', 75 | slashes: true 76 | })) 77 | mainWindow.webContents.openDevTools() 78 | 79 | mainWindow.on('closed', () => { 80 | mainWindow = null 81 | }) 82 | } 83 | 84 | app.on('ready', createWindow) 85 | 86 | app.on('window-all-closed', () => { 87 | if (process.platform !== 'darwin') { 88 | app.quit() 89 | } 90 | }) 91 | 92 | app.on('activate', () => { 93 | if (mainWindow === null) { 94 | createWindow() 95 | } 96 | }) 97 | -------------------------------------------------------------------------------- /old-post-backup/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .venv 3 | venv 4 | *.pyc 5 | -------------------------------------------------------------------------------- /old-post-backup/README.md: -------------------------------------------------------------------------------- 1 | # important 2 | 3 | **This (sub-) demo is deprecated.** 4 | 5 | Checkout the [paraent folder](https://github.com/fyears/electron-python-example) for a better solution / demo and the better explanation. 6 | 7 | This folder is intented for backup only. 8 | 9 | # An example to use electron and python. 10 | 11 | The following are copied from my original [blog post](https://www.fyears.org/2015/06/electron-as-gui-of-python-apps.html). 12 | 13 | ## what 14 | 15 | [Electron](http://electron.atom.io/) (formerly Atom Shell) is a desktop node.js-powered "shell". It is designed by Github and used to build [Atom Editor](https://atom.io/). 16 | 17 | Python is a simple and powerful programming language. 18 | 19 | This post is a note about how to use Electron as the desktop GUI for Python applications. 20 | 21 | ## but why? 22 | 23 | Building desktop applications with Python is not easy. 24 | 25 | [Tkinter](https://wiki.python.org/moin/TkInter) is the standard package for Python GUI, but it is ~~very~~ ugly. 26 | 27 | The only mature and real-world solution is [QT](http://www.qt.io/developers/), with Python package [PySide](https://wiki.qt.io/Category:LanguageBindings::PySide) or [PyQT](http://www.riverbankcomputing.co.uk/software/pyqt/intro), and [Enaml](https://github.com/nucleic/enaml) based on that. However, PySide seems to have died, and PyQT is not free for commercial usages. 28 | 29 | [IPython](http://ipython.org/), an enhanced shell for Python, has an interesting design: it has kernal, a `qtconsole` powered by QT, and `notebook` powered by web pages. 30 | 31 | 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. 32 | 33 | ## the architecture 34 | 35 | The basic idea is rather simple: 36 | 37 | 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. 38 | 39 | 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. 40 | 41 | The first way is easy to understand and implemented, while the second way seems to provide more protentials. 42 | 43 | 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. 44 | 45 | ## a complete example 46 | 47 | 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. 48 | 49 | Install Python, node.js, then 50 | 51 | ```bash 52 | pip install Flask 53 | npm install electron-prebuilt -g 54 | npm install request-promise -g 55 | ``` 56 | 57 | Then create a working directory. `cd` to the directory. 58 | 59 | We need a basic `package.json`: 60 | 61 | ```js 62 | { 63 | "name" : "your-app", 64 | "version" : "0.1.0", 65 | "main" : "main.js", 66 | "dependencies": { 67 | "request-promise": "*", 68 | "electron-prebuilt": "*" 69 | } 70 | } 71 | ``` 72 | 73 | as well as the `main.js`: 74 | 75 | ```js 76 | const electron = require('electron'); 77 | const app = electron.app; 78 | const BrowserWindow = electron.BrowserWindow; 79 | //electron.crashReporter.start(); 80 | 81 | var mainWindow = null; 82 | 83 | app.on('window-all-closed', function() { 84 | //if (process.platform != 'darwin') { 85 | app.quit(); 86 | //} 87 | }); 88 | 89 | app.on('ready', function() { 90 | // call python? 91 | var subpy = require('child_process').spawn('python', ['./hello.py']); 92 | //var subpy = require('child_process').spawn('./dist/hello.exe'); 93 | var rq = require('request-promise'); 94 | var mainAddr = 'http://localhost:5000'; 95 | 96 | var openWindow = function(){ 97 | mainWindow = new BrowserWindow({width: 800, height: 600}); 98 | // mainWindow.loadURL('file://' + __dirname + '/index.html'); 99 | mainWindow.loadURL('http://localhost:5000'); 100 | mainWindow.webContents.openDevTools(); 101 | mainWindow.on('closed', function() { 102 | mainWindow = null; 103 | subpy.kill('SIGINT'); 104 | }); 105 | }; 106 | 107 | var startUp = function(){ 108 | rq(mainAddr) 109 | .then(function(htmlString){ 110 | console.log('server started!'); 111 | openWindow(); 112 | }) 113 | .catch(function(err){ 114 | //console.log('waiting for the server start...'); 115 | startUp(); 116 | }); 117 | }; 118 | 119 | // fire! 120 | startUp(); 121 | }); 122 | ``` 123 | 124 | 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. 125 | 126 | Lastly, the `hello.py`: 127 | 128 | ```python 129 | #!/usr/bin/env python 130 | # -*- coding: utf-8 -*- 131 | 132 | from __future__ import print_function 133 | import time 134 | from flask import Flask 135 | 136 | app = Flask(__name__) 137 | 138 | @app.route("/") 139 | def hello(): 140 | return "Hello World! This is powered by Python backend." 141 | 142 | if __name__ == "__main__": 143 | print('oh hello') 144 | #time.sleep(5) 145 | app.run(host='127.0.0.1', port=5000) 146 | ``` 147 | 148 | After all the files are generated, we could simply run Electron inside bash: 149 | 150 | ```bash 151 | electron . # . as the working directory 152 | ``` 153 | 154 | A desktop application should be launched as desired. 155 | 156 | The full code could be viewed on [GitHub](https://github.com/fyears/electron-python-example). 157 | 158 | ## further thinking 159 | 160 | Electron is cool. But according to the issues in Atom Editor, the performance one of the main issue. 161 | 162 | "Everything is a website" is also cool. But well, we may ~~easily~~ reach the limitations of web technologies. 163 | 164 | That said, I believe "Electron as GUI for Python applications" is still an interesting approach about writing GUI in Python. 165 | -------------------------------------------------------------------------------- /old-post-backup/hello.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | from time import sleep 4 | import sys 5 | from flask import Flask 6 | app = Flask(__name__) 7 | 8 | @app.route("/") 9 | def hello(): 10 | return "Hello World!" 11 | 12 | if __name__ == "__main__": 13 | print('oh hello') 14 | #sleep(10) 15 | sys.stdout.flush() 16 | app.run() 17 | -------------------------------------------------------------------------------- /old-post-backup/main.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const app = electron.app; // Module to control application life. 3 | const BrowserWindow = electron.BrowserWindow; // Module to create native browser window. 4 | 5 | // Report crashes to our server. 6 | //electron.crashReporter.start(); 7 | 8 | 9 | // Keep a global reference of the window object, if you don't, the window will 10 | // be closed automatically when the javascript object is GCed. 11 | var mainWindow = null; 12 | 13 | // Quit when all windows are closed. 14 | app.on('window-all-closed', function() { 15 | //if (process.platform != 'darwin') { 16 | app.quit(); 17 | //} 18 | }); 19 | 20 | // This method will be called when Electron has done everything 21 | // initialization and ready for creating browser windows. 22 | app.on('ready', function() { 23 | // call python? 24 | var subpy = require('child_process').spawn('python', ['./hello.py']); 25 | 26 | var rq = require('request-promise'); 27 | var mainAddr = 'http://localhost:5000'; 28 | 29 | var openWindow = function(){ 30 | // Create the browser window. 31 | mainWindow = new BrowserWindow({width: 800, height: 600}); 32 | // and load the index.html of the app. 33 | // mainWindow.loadURL('file://' + __dirname + '/index.html'); 34 | mainWindow.loadURL('http://localhost:5000'); 35 | // Open the devtools. 36 | mainWindow.webContents.openDevTools(); 37 | // Emitted when the window is closed. 38 | mainWindow.on('closed', function() { 39 | // Dereference the window object, usually you would store windows 40 | // in an array if your app supports multi windows, this is the time 41 | // when you should delete the corresponding element. 42 | mainWindow = null; 43 | // kill python 44 | subpy.kill('SIGINT'); 45 | }); 46 | }; 47 | 48 | var startUp = function(){ 49 | rq(mainAddr) 50 | .then(function(htmlString){ 51 | console.log('server started!'); 52 | openWindow(); 53 | }) 54 | .catch(function(err){ 55 | //console.log('waiting for the server start...'); 56 | startUp(); 57 | }); 58 | }; 59 | 60 | // fire! 61 | startUp(); 62 | }); 63 | -------------------------------------------------------------------------------- /old-post-backup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "your-app", 3 | "version": "0.2.0", 4 | "main": "main.js", 5 | "dependencies": { 6 | "request-promise": "*", 7 | "electron-prebuilt": "*" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pretty-calculator", 3 | "version": "1.0.0", 4 | "description": "A minimal Electron and Python - based calculator ", 5 | "main": "main.js", 6 | "scripts": { 7 | "start": "electron ." 8 | }, 9 | "repository": "https://github.com/fyears/electron-python-example", 10 | "keywords": [ 11 | "Electron", 12 | "Python", 13 | "zerorpc", 14 | "demo" 15 | ], 16 | "author": "fyears", 17 | "license": "MIT", 18 | "dependencies": { 19 | "zerorpc": "git+https://github.com/0rpc/zerorpc-node.git" 20 | }, 21 | "devDependencies": { 22 | "electron": "^1.7.6", 23 | "electron-packager": "^9.0.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pycalc/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/ 3 | -------------------------------------------------------------------------------- /pycalc/api.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from calc import calc as real_calc 3 | import sys 4 | import zerorpc 5 | 6 | class CalcApi(object): 7 | def calc(self, text): 8 | """based on the input text, return the int result""" 9 | try: 10 | return real_calc(text) 11 | except Exception as e: 12 | return 0.0 13 | def echo(self, text): 14 | """echo any text""" 15 | return text 16 | 17 | def parse_port(): 18 | port = 4242 19 | try: 20 | port = int(sys.argv[1]) 21 | except Exception as e: 22 | pass 23 | return '{}'.format(port) 24 | 25 | def main(): 26 | addr = 'tcp://127.0.0.1:' + parse_port() 27 | s = zerorpc.Server(CalcApi()) 28 | s.bind(addr) 29 | print('start running on {}'.format(addr)) 30 | s.run() 31 | 32 | if __name__ == '__main__': 33 | main() 34 | -------------------------------------------------------------------------------- /pycalc/calc.py: -------------------------------------------------------------------------------- 1 | """ 2 | A calculator based on https://en.wikipedia.org/wiki/Reverse_Polish_notation 3 | """ 4 | 5 | from __future__ import print_function 6 | 7 | 8 | def getPrec(c): 9 | if c in "+-": 10 | return 1 11 | if c in "*/": 12 | return 2 13 | if c in "^": 14 | return 3 15 | return 0 16 | 17 | def getAssoc(c): 18 | if c in "+-*/": 19 | return "LEFT" 20 | if c in "^": 21 | return "RIGHT" 22 | return "LEFT" 23 | 24 | def getBin(op, a, b): 25 | if op == '+': 26 | return a + b 27 | if op == '-': 28 | return a - b 29 | if op == '*': 30 | return a * b 31 | if op == '/': 32 | return a / b 33 | if op == '^': 34 | return a ** b 35 | return 0 36 | 37 | def calc(s): 38 | numStk = [] 39 | opStk = [] 40 | i = 0 41 | isUnary = True 42 | while (i < len(s)): 43 | while (i < len(s) and s[i] == ' '): 44 | i += 1 45 | if (i >= len(s)): 46 | break 47 | if (s[i].isdigit()): 48 | num = '' 49 | while (i < len(s) and (s[i].isdigit() or s[i] == '.')): 50 | num += s[i] 51 | i += 1 52 | numStk.append(float(num)) 53 | isUnary = False 54 | continue 55 | 56 | if (s[i] in "+-*/^"): 57 | if isUnary: 58 | opStk.append('#') 59 | else: 60 | while (len(opStk) > 0): 61 | if ((getAssoc(s[i]) == "LEFT" and getPrec(s[i]) <= getPrec(opStk[-1])) or 62 | (getAssoc(s[i]) == "RIGHT" and getPrec(s[i]) < getPrec(opStk[-1]))): 63 | op = opStk.pop() 64 | if op == '#': 65 | numStk.append(-numStk.pop()) 66 | else: 67 | b = numStk.pop() 68 | a = numStk.pop() 69 | numStk.append(getBin(op, a, b)) 70 | continue 71 | break 72 | opStk.append(s[i]) 73 | isUnary = True 74 | elif (s[i] == '('): 75 | opStk.append(s[i]) 76 | isUnary = True 77 | else: 78 | while (len(opStk) > 0): 79 | op = opStk.pop() 80 | if (op == '('): 81 | break 82 | if op == '#': 83 | numStk.append(-numStk.pop()) 84 | else: 85 | b = numStk.pop() 86 | a = numStk.pop() 87 | numStk.append(getBin(op, a, b)) 88 | i += 1 89 | 90 | while (len(opStk) > 0): 91 | op = opStk.pop() 92 | if op == '#': 93 | numStk.append(-numStk.pop()) 94 | else: 95 | b = numStk.pop() 96 | a = numStk.pop() 97 | numStk.append(getBin(op, a, b)) 98 | 99 | return numStk.pop() 100 | 101 | 102 | if __name__ == '__main__': 103 | ss = [ 104 | "1 + 2 * 3 / 4 - 5 + - 6", # -8.5 105 | "10 + ( - 1 ) ^ 4", # 11 106 | "10 + - 1 ^ 4", # 9 107 | "10 + - - 1 ^ 4", # 11 108 | "10 + - ( - 1 ^ 4 )", # 11 109 | "5 * ( 10 - 9 )", # 5 110 | "1 + 2 * 3", # 7 111 | "4 ^ 3 ^ 2", # 262144 112 | "4 ^ - 3", # 0.015625 113 | "4 ^ ( - 3 )", # 0.015625 114 | ] 115 | for s in ss: 116 | res = calc(s) 117 | print('{} = {}'.format(res, s)) 118 | -------------------------------------------------------------------------------- /pycalc/requirements.txt: -------------------------------------------------------------------------------- 1 | zerorpc 2 | pyzmq 3 | future 4 | msgpack-python 5 | gevent 6 | pyinstaller 7 | pypiwin32 8 | -------------------------------------------------------------------------------- /renderer.js: -------------------------------------------------------------------------------- 1 | const zerorpc = require("zerorpc") 2 | let client = new zerorpc.Client() 3 | 4 | client.connect("tcp://127.0.0.1:4242") 5 | 6 | client.invoke("echo", "server ready", (error, res) => { 7 | if(error || res !== 'server ready') { 8 | console.error(error) 9 | } else { 10 | console.log("server is ready") 11 | } 12 | }) 13 | 14 | let formula = document.querySelector('#formula') 15 | let result = document.querySelector('#result') 16 | 17 | formula.addEventListener('input', () => { 18 | client.invoke("calc", formula.value, (error, res) => { 19 | if(error) { 20 | console.error(error) 21 | } else { 22 | result.textContent = res 23 | } 24 | }) 25 | }) 26 | 27 | formula.dispatchEvent(new Event('input')) 28 | --------------------------------------------------------------------------------