用 Electron 作為 Python 的 GUI 界面

該文章原文為《Electron as GUI of Python Applications》, 本站保留譯文版權。

這篇文章展示了如何用 Electron 作為 Python 應用的 GUI 部分(是我之前文章的一個更新)。前後端之間的通信通過 zerorpc 來實現。完整的代碼發布在 Github。

原文及爭論

注意

這篇文章是我幾年前所寫的一篇文章的更新版本。如果你沒有讀過那篇文章,你也沒有必要去讀。

爭論

我沒有想到之前那篇文章吸引了那麼多的讀者。有些人還將它發布在 Haker News 和 Reddit 上。同樣也有很多對它的批評(criticisms)。針對這些爭論,我想分享一下我的回復。

你不知道 Tkinter, GTK, QT(PySide 和 PyQT), wxPython, Kivy, thrust, ...?

當然,至少我知道它們的存在,而且試過它們中間的幾個。我仍然認為 QT 是它們中間最好的一個。另外,pyotherside 也是一個活躍的 Python 綁定。我只是在這裡提供另外一種「Web技術驅動」的方法。

...還有 cefpython。

或多或少,相比起 Electron,它處在更底層的位置。例如,PySide 就是基於它的。

我可以直接用 Javascript 來寫!

是的。除非有些庫——例如 numpy——JS 里沒有。另外,我們關注的是如何用 Electron / Javascript / web 技術來改善Python應用。

我可以用 QT WebEngine。

那就去試試吧。不過既然你都用了「web 引擎」,幹嘛不給 Electron 一個機會呢?

你有兩個運行時!

是的。一個 Javascript 的,一個 Python 的。沒辦法,Python 和 Javascript 都是動態語言,很多時候都需要運行時的支持。

架構和選項

在之前的文章中,我展示了一個架構的示例:通過 Python 構建一個本地伺服器,而Electron 則作為一個本地瀏覽器。

start
|
V
+------------+
| | start
| +-------------> +-------------------+
| electron | sub process | |
| | | python web server |
| (basically | http | |
| browser) | <-----------> | (business logic) |
| | communication | |
| | | (all html/css/js) |
| | | |
+------------+ +-------------------+

這是一個不太優雅(not-so-efficient)的解決辦法。

讓我們重新思考一下我們的核心需求:我們有一個Python應用,和一個Node.js應用(Electron)。如何將它們結合起來,並讓它們能夠彼此通信?

我們事實上需要一種跨進程通信(IPC, interprocess Communication)機制。這是無法避免的,除非 Python 和 javascript 互相可以外部調用。

HTTP 只是流行的IPC方式中的一種,而且僅僅是在寫上篇文章的時候第一個跑到我腦子裡而已。

我們有更多的選擇。

我們可以(而且應該)使用 socket。然後,在此基礎上,我們需要一個抽象的消息層(messaging layer),可以用 ZeroMq 部署,因為它是最好的消息庫(messaging libraries)之一。更進一步,我們需要再原始數據之上定義一些 schema,這可以由zerorpc實現。

(幸運的是,zerorpc符合我們的需求,因為它支持 Python 和 Node.js。如果需要支持更多語言,你可以看看 gRPC。)

因此,再這篇文章中,我將展示一個用 zerorpc 通信的例子,這個例子將比我之前展示的方法更高效。

start
|
V
+--------------------+
| | start
| electron +-------------> +------------------+
| | sub process | |
| (browser) | | python server |
| | | |
| (all html/css/js) | | (business logic) |
| | zerorpc | |
| (node.js runtime, | <-----------> | (zeromq server) |
| zeromq client) | communication | |
| | | |
+--------------------+ +------------------+

準備

注意,這個示例可以成功在以下環境運行:Windows 10,Python 3.6,Electron 1.7,Node.js v6。

我們需要用到:python 應用,pip,node,npm,並能通過命令行使用。為了使用zerorpc,我們還需要C/C++編譯器(cc 和 c++ 的命令行工具,以及 Windows 上的 MSVC)。

這個project的架構是:

.
|-- index.html
|-- main.js
|-- package.json
|-- renderer.js
|
|-- pycalc
| |-- api.py
| |-- calc.py
| `-- requirements.txt
|
|-- LICENSE
`-- README.md

正如上圖所示,Python應用被放置在一個子文件夾(subfolder)里。在這個例子中,Python 應用 pycalc/calc.py 提供以下功能:calc(text),可以接受一段文本,例如 1 + 1/2,並返回結果,例如1.5。pycalc/api.py就是我們的目標。

index.html, main.js, package.json 和 renderer.js 都是從 electron-quick-start 修改而來。

Python的部分

首先,我們已經可以運行Python應用,那麼Python環境應該是沒問題的。我強烈建議你們使用 virtualenv 來開發 Python 應用。

試著安裝 zerorpc 和 pyinstaller。

pip install zerorpc
pip install pyinstaller

如果成功的話,上述命令應該不會出現問題。否則請到網上尋找解決辦法(guide)。

Node.js / Electron 的部分

第二步,試著配置 Node.js 和 Electron 環境。我假設 node 和 npm 已經能通過 command line 使用,並且是最新版本的。

我們需要配置 package.json, 特別是 main 這個入口:

{
"name": "pretty-calculator",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"dependencies": {
"zerorpc": "git+https://github.com/fyears/zerorpc-node.git"
},
"devDependencies": {
"electron": "^1.7.6",
"electron-packager": "^9.0.1"
}
}

清除緩存:

# On Linux / OS X
# clean caches, very important!!!!!
rm -rf ~/.node-gyp
rm -rf ~/.electron-gyp
rm -rf ./node_modules
# On Window PowerShell (not cmd.exe!!!)
# clean caches, very important!!!!!
Remove-Item "$($env:USERPROFILE).node-gyp" -Force -Recurse -ErrorAction Ignore
Remove-Item "$($env:USERPROFILE).electron-gyp" -Force -Recurse -ErrorAction Ignore
Remove-Item .
ode_modules -Force -Recurse -ErrorAction Ignore

(譯者註:如果在 Windows 中執行上述命令,提示 Ignore 不是有效的枚舉值,可以將該值更改為 SilentlyContinue 後執行。因為 Ignore 是在 Powershell 3.0 加入的。參考微軟官方文檔)

然後運行npm:

# 1.7.6 is the version of electron
# Its very important to set the electron version correctly!!!
# check out the version value in your package.json
npm install --runtime=electron --target=1.7.6

# verify the electron binary and its version by opening it
./node_modules/.bin/electron

Npm install 將會從我的復刻安裝 zerorpc-node,這樣就可以不用從源代碼編譯了。

(如果需要,在項目文件夾中添加 ./.npmrc 文件夾)

現在所有的庫都應該部署完畢了。

可選:從源碼編譯

如果上面的安裝方式出現了錯誤,即使你設置了正確的 electron 版本,我們也許只能從源碼來編譯了。

諷刺的是,為了編譯 Node.js C/C++ native code,我們必須配置 python2,不管你的 Python 應用用的是什麼版本。你可以查看官方說明。

特別是,如果你的工作環境是 Windows,用管理員許可權打開 PowerShell,運行 npm install --global --production windows-build-tools 來在 %USERPROFILE%.windows-build-toolspython27 中安裝一個獨立的 Python 2.7 以及其他所需的 VS 庫。我們只需要使用這一次。

接下來,按照上面所說的方法清理緩存。

設定好 npm 的版本,並且安裝好所需的庫。

配置環境變數:

# On Linux / OS X:

# env
export npm_config_target=1.7.6 # electron version
export npm_config_runtime=electron
export npm_config_disturl=https://atom.io/download/electron
export npm_config_build_from_source=true

# may not be necessary
#export npm_config_arch=x64
#export npm_config_target_arch=x64

npm config ls
# On Window PowerShell (not cmd.exe!!!)

$env:npm_config_target="1.7.6" # electron version
$env:npm_config_runtime="electron"
$env:npm_config_disturl="https://atom.io/download/electron"
$env:npm_config_build_from_source="true"

# may not be necessary
#$env:npm_config_arch="x64"
#$env:npm_config_target_arch="x64"

npm config ls

接下來安裝:

# in the same shell as above!!!
# because you want to make good use of the above environment variables

# install everything based on the package.json
npm install

# verify the electron binary and its version by opening it
./node_modules/.bin/electron

(如果需要,在項目文件夾中添加 ./.npmrc 文件夾)

核心功能

Python 部分

我們要再 Python 端建立一個 ZeroMQ 伺服器。

將 calc.py 放到 pycalc/ 文件夾中。然後在這個路徑下再創建一個 pycalc/api.py。查看 zerorpc-python 作為參考。

from __future__ import print_function
from calc import calc as real_calc
import sys
import zerorpc

class CalcApi(object):
def calc(self, text):
"""based on the input text, return the int result"""
try:
return real_calc(text)
except Exception as e:
return 0.0
def echo(self, text):
"""echo any text"""
return text

def parse_port():
return 4242

def main():
addr = tcp://127.0.0.1: + parse_port()
s = zerorpc.Server(CalcApi())
s.bind(addr)
print(start running on {}.format(addr))
s.run()

if __name__ == __main__:
main()

為了測試,在終端運行 python pycalc/api.py。然後打開另一個終端,運行下面的命令,查看結果:

zerorpc tcp://localhost:4242 calc "1 + 1"
## connecting to "tcp://localhost:4242"
## 2.0

結束調試後,記得終止 Python 程序。

事實上,這只是另外一個伺服器,通過構建於 TCP 之上的 zeromq 進行通信,而不是構建於 HTTP 之上的傳統 web 伺服器。

Node.js / Electron 部分

基本思想:在主進程(main process)中,引用 Python 作為子進程(child process),並構建窗口。在渲染進程中,使用 Node.js 運行時和 zerorpc 庫來 Python 子進程通信。所有的 HTML / Javascript / CSS 都由 Electron 管理,而不是 Python web 伺服器(像我之前那個例子里一樣)。

在 main.js 中,這些是默認的一些初始代碼,沒什麼特別的:

// main.js

const electron = require(electron)
const app = electron.app
const BrowserWindow = electron.BrowserWindow
const path = require(path)

let mainWindow = null
const createWindow = () => {
mainWindow = new BrowserWindow({width: 800, height: 600})
mainWindow.loadURL(require(url).format({
pathname: path.join(__dirname, index.html),
protocol: file:,
slashes: true
}))
mainWindow.webContents.openDevTools()
mainWindow.on(closed, () => {
mainWindow = null
})
}
app.on(ready, createWindow)
app.on(window-all-closed, () => {
if (process.platform !== darwin) {
app.quit()
}
})
app.on(activate, () => {
if (mainWindow === null) {
createWindow()
}
})

我們要加上一些代碼來運行 Python 子進程:

// add these to the end or middle of main.js

let pyProc = null
let pyPort = null

const selectPort = () => {
pyPort = 4242
return pyPort
}

const createPyProc = () => {
let port = + selectPort()
let script = path.join(__dirname, pycalc, api.py)
pyProc = require(child_process).spawn(python, [script, port])
if (pyProc != null) {
console.log(child process success)
}
}

const exitPyProc = () => {
pyProc.kill()
pyProc = null
pyPort = null
}

app.on(ready, createPyProc)
app.on(will-quit, exitPyProc)

在 index.html 中,我們有一個 <input> 來接收用戶輸入,還有一個 <div> 負責輸出:

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello Calculator!</title>
</head>
<body>
<h1>Hello Calculator!</h1>
<p>Input something like <code>1 + 1</code>.</p>
<p>This calculator supports <code>+-*/^()</code>,
whitespaces, and integers and floating numbers.</p>
<input id="formula" value="1 + 2.0 * 3.1 / (4 ^ 5.6)"></input>
<div id="result"></div>
</body>
<script>
require(./renderer.js)
</script>
</html>

在 renderer.js 中,我們有一些用來初始化 zerorpc 客戶端的代碼,以及查看輸入變化的代碼。當用戶輸入一些公式的時候,JS 會發送這些文字到 Python 後端,並返回計算結果。

// renderer.js

const zerorpc = require("zerorpc")
let client = new zerorpc.Client()
client.connect("tcp://127.0.0.1:4242")

let formula = document.querySelector(#formula)
let result = document.querySelector(#result)
formula.addEventListener(input, () => {
client.invoke("calc", formula.value, (error, res) => {
if(error) {
console.error(error)
} else {
result.textContent = res
}
})
})
formula.dispatchEvent(new Event(input))

運行

運行下面的命令,神奇的事發生了:

./node_modules/.bin/electron .

Awesome!

如果出現錯誤,類似動態鏈接錯誤,試著清除緩存,然後重新安裝庫:

rm -rf node_modules
rm -rf ~/.node-gyp ~/.electron-gyp

npm install

打包

有人問我應該怎麼打包。這很簡單:運用如何打包 Python 應用以及 Electron 應用的知識。

Python 部分

使用 PyInstaller。

在終端運行以下命令:

pyinstaller pycalc/api.py --distpath pycalcdist

rm -rf build/
rm -rf api.spec

如果一切正常的話,會生成 pycalcdist/api/ 文件夾,包含了一個 exe 執行文件。這是完全獨立的 Python exe,可以移植到其它地方。

注意:你必須生成這個獨立的 exe 執行文件,因為其它電腦上可能沒有運行這些代碼所需的 python 環境和所需的庫。直接把 Python 代碼複製到別的地方是不行的。

Node.js / Electron 部分

這有點難,因為我們將 Python 打包成了 exe。

在上面的代碼中,我寫過:

// part of main.js
let script = path.join(__dirname, pycalc, api.py)
pyProc = require(child_process).spawn(python, [script, port])

然而,當我們把 Python 打包後,我們就不能再對 Python 代碼使用 spawn 方法。取而代之的是,我們必須使用 execFile 方法來運行 exe 文件。

Electron 不知道應用是否已經被分發出去了,所以在 mail.js 里,我加上了這段函數:

// main.js

const PY_DIST_FOLDER = pycalcdist
const PY_FOLDER = pycalc
const PY_MODULE = api // without .py suffix

const guessPackaged = () => {
const fullPath = path.join(__dirname, PY_DIST_FOLDER)
return require(fs).existsSync(fullPath)
}

const getScriptPath = () => {
if (!guessPackaged()) {
return path.join(__dirname, PY_FOLDER, PY_MODULE + .py)
}
if (process.platform === win32) {
return path.join(__dirname, PY_DIST_FOLDER, PY_MODULE, PY_MODULE + .exe)
}
return path.join(__dirname, PY_DIST_FOLDER, PY_MODULE, PY_MODULE)
}

然後修改 createPyProc 方法:

// main.js
// the improved version
const createPyProc = () => {
let script = getScriptPath()
let port = + selectPort()

if (guessPackaged()) {
pyProc = require(child_process).execFile(script, [port])
} else {
pyProc = require(child_process).spawn(python, [script, port])
}

if (pyProc != null) {
//console.log(pyProc)
console.log(child process success on port + port)
}
}

關鍵點是,檢查 *dist 文件夾是否被生成。如果生成了,那麼說明我們在生產模式下,那麼就直接運行 exe 文件;如果沒有,我們就需要對 Python 代碼用 spawn 方法,在 Python shell 裡面運行。

最後,運行 Electron-Packager 來生成最終的應用。我們需要排除一些文件夾,例如,pycalc/ 已經不需要了。應用的名稱,平台等等需要在 package.json 裡面配置。請參閱相關文檔。

# we need to make sure we have bundled the latest Python code
# before running the below command!
# Or, actually, we could bundle the Python executable later,
# and copy the output into the correct distributable Electron folder...

./node_modules/.bin/electron-packager . --overwrite --ignore="pycalc$" --ignore=".venv" --ignore="old-post-backup"
## Packaging app for platform win32 x64 using electron v1.7.6
## Wrote new app to ./pretty-calculator-win32-x64

最後,我們就得到了最終生成的應用!對我來說,結果在 ./pretty-calculator-win32-x64 裡面。在我的電腦上,大小是 170MB 左右。我也試著壓縮了一下,生成的 .7z 文件是 43.3MB 左右。

將最後生成的文件移植到其它電腦上看看結果吧!


Swiity?

swiity.com圖標
推薦閱讀:

TAG:GUI設計 | Python | Electron |