首发于DustTech
Electron+Python界面开发(通过zerorpc)

Electron+Python界面开发(通过zerorpc)

Python 开发GUI要么太繁琐要么太丑,而前端技术恰巧是最适合做漂亮UI的。所以考虑将Python和前端技术结合,通过进程通信和前端框架交流,打包成一个完整的桌面APP。教程分成两种实现方式,一个是zerorpc进程通信一个是http通信。

这篇教程介绍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 |                  |
|                    |               |                  |
+--------------------+               +------------------+



Electron基础


详情见官方文档:electronjs.org/docs


如果你可以建一个网站,你就可以建一个桌面应用程序。 Electron 是一个使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架,它负责比较难搞的部分,你只需把精力放在你的应用的核心上即可。



有很多有名的App是用Electeon开发的,如:Skype和GitHub以及著名编辑器Atom,所以这个框架在水平上是被认可的。



Electron 可以让你使用纯 JavaScript 调用丰富的原生(操作系统) APIs 来创造桌面应用。 你可以把它看作一个专注于桌面应用的 Node. js 的变体,而不是 Web 服务器。

这不意味着 Electron 是某个图形用户界面(GUI)库的 JavaScript 版本。 相反,Electron 使用 web 页面作为它的 GUI,所以你能把它看作成一个被 JavaScript 控制的,精简版的 Chromium 浏览器。


安装


为你的新Electron应用创建一个新的空文件夹。 打开你的命令行工具,然后从该文件夹运行npm init。将package.json修改一下。


{
    "name": "your-app",
    "version": "0.1.0",
    "main": "main.js",
    "scripts": {
      "start": "electron ."
    }
 }



现在,您需要安装electron。 我们推荐的安装方法是把它作为您 app 中的开发依赖项,这使您可以在不同的 app 中使用不同的 Electron 版本。 在您的app所在文件夹中运行下面的命令:


npm install --save-dev electron



使用


electron模块包含了Electron提供的所有API和功能,引入方法和普通Node.js模块一样:


const electron = require('electron')



electron 模块所提供的功能都是通过命名空间暴露出来的。 比如说: electron.app负责管理Electron 应用程序的生命周期, electron.BrowserWindow类负责创建窗口。 下面是一个简单的main.js文件,它将在应用程序准备就绪后打开一个窗口:


const {app, BrowserWindow} = require('electron')

  function createWindow () {   
    // 创建浏览器窗口
    win = new BrowserWindow({width: 800, height: 600})

    // 然后加载应用的 index.html。
    win.loadFile('index.html')
  }

  app.on('ready', createWindow)



您应当在 main.js 中创建窗口,并处理程序中可能遇到的所有系统事件。 下面我们将完善上述例子,添加以下功能:打开开发者工具、处理窗口关闭事件、在macOS用户点击dock上图标时重建窗口,添加后,main. js 就像下面这样:


const {app, BrowserWindow} = require('electron')

  // Keep a global reference of the window object, if you don't, the window will
  // be closed automatically when the JavaScript object is garbage collected.
  let win

  function createWindow () {
    // 创建浏览器窗口。
    win = new BrowserWindow({width: 800, height: 600})

    // 然后加载应用的 index.html。
    win.loadFile('index.html')

    // 打开开发者工具
    win.webContents.openDevTools()

    // 当 window 被关闭,这个事件会被触发。
    win.on('closed', () => {
      // 取消引用 window 对象,如果你的应用支持多窗口的话,
      // 通常会把多个 window 对象存放在一个数组里面,
      // 与此同时,你应该删除相应的元素。
      win = null
    })
  }

  // Electron 会在初始化后并准备
  // 创建浏览器窗口时,调用这个函数。
  // 部分 API 在 ready 事件触发后才能使用。
  app.on('ready', createWindow)

  // 当全部窗口关闭时退出。
  app.on('window-all-closed', () => {
    // 在 macOS 上,除非用户用 Cmd + Q 确定地退出,
    // 否则绝大部分应用及其菜单栏会保持激活。
    if (process.platform !== 'darwin') {
      app.quit()
    }
  })

  app.on('activate', () => {
    // 在macOS上,当单击dock图标并且没有其他窗口打开时,
    // 通常在应用程序中重新创建一个窗口。
    if (win === null) {
      createWindow()
    }
  })

  // 在这个文件中,你可以续写应用剩下主进程代码。
  // 也可以拆分成几个文件,然后用 require 导入。



最后,创建你想展示的 index.html


<!DOCTYPE html>
  <html>
    <head>
      <meta charset="UTF-8">
      <title>Hello World!</title>
    </head>
    <body>
      <h1>Hello World!</h1>
      We are using node <script>document.write(process.versions.node)</script>,
      Chrome <script>document.write(process.versions.chrome)</script>,
      and Electron <script>document.write(process.versions.electron)</script>.
    </body>
  </html>



启动


在创建并初始化完成 main.jsindex.htmlpackage.json之后,您就可以在当前工程的根目录执行 npm start 命令来启动刚刚编写好的Electron程序了。




Python部分


安装pip install zerorpc。在项目根目录创建文件夹py,用于存放Python相关代码。新建一个python文件,命名为api.py。敲入如下demo。


import zerorpc

class HelloRPC(object):
    def hello(self, name):
        return "Hello, %s" % name

s = zerorpc.Server(HelloRPC())
s.bind("tcp://0.0.0.0:4242")
s.run()



然后命令行里运行python api.py。再另一个终端输入zerorpc tcp://localhost:4242 hello NXB,如果得到Hello,NXB则没有问题。


Electron部分


接着之前的main.js后面写。我们首先需要node.js能够调用Python进程。


const path=require('path')

let pyProc = null
let pyPort = null

const createPyProc = () => {
  let port = '4242'
  let script = path.join(__dirname, 'py', '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)



写完后运行npm start看看能不能启动python子程序按照之前的方式测试一下能不能通信。没问题的话继续。

修改我们的主页index.html,构建一个输入框。我们希望在输入框里输入字符,下方可以动态显示Hello,XX。


<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello XX</title>
  </head>
  <body>
    <input id="name" ></input>
    <p id="result" color='black'></p>
  </body>
  <script>
    require('./render.js')
  </script>
</html>



在根目录下创建render.js用于监听输入框,将输入框的内容动态发送给python进程,并接收反回来的消息。


// renderer.js
var zerorpc = require("zerorpc");
var client = new zerorpc.Client();
client.connect("tcp://127.0.0.1:4242");

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



如果没问题的话应该是这样的效果:




打包


测试没问题之后我们需要将应用打包,因为别人电脑上不一定装了node.js或是python。首先要装个打包工具pip install pyinstaller

package.jsonscript中加入"build-python":"pyinstaller ./py/api.py --clean --distpath ./pydist"。然后运行npm run build-python编译一下。编译完了可以把根目录下生成的build文件夹和api.spec删了。如果中间报错 AttributeError: module 'enum' has no attribute 'IntFlag',就运行pip uninstall enum34把enum34删了。


This is likely caused by the package enum34. Since python 3.4 there's a standard library enummodule, so you should uninstall enum34, which is no longer compatible with the enum in the standard library since enum.IntFlag was added in python 3.6


之前子进程是通过调用python命令运行的,现在我们要换成生成的可执行程序。修改main.js


// let script = path.join(__dirname, 'py', 'api.py')
  // pyProc = require('child_process').spawn('python', [script, port])
  let script = path.join(__dirname, 'pydist', 'api','api')
  pyProc = require('child_process').execFile(script, [port])



运行npm start可以查看效果。

在根目录运行npm install electron-packager --save-dev安装Electron打包模块。然后将"pack-app": "./node_modules/.bin/electron-packager . --overwrite --ignore=py$"写入package.json的script中。

运行npm run pack-app打包程序。最后会生成可执行文件,复制到别的电脑也可以运行。


编辑于 2018-06-12 17:06