Eel:利用 HTML 技术构建 Python GUI

官方 Github: ChrisKnott/Eel

Eel 是一个轻量的 Python 库,用于制作简单的类似于 Electron(但是比它更轻量) 的离线 HTML/JS GUI 应用程序,并具有对 Python 功能(capabilities)和库的完全访问权限。

Eel 托管一个本地 Web 服务器,然后允许您使用 Python 注释函数(annotate functions),以便可以从 JavaScript 调用它们,反之亦然。

Eel 旨在消除编写简短的 GUI 应用程序的麻烦。如果您熟悉 Python 和 Web 开发,则可以跳到 示例,该示例从给定文件夹中选择随机文件名(这在浏览器中是不可能的)。

1 使用方法

一个 Eel 应用程序将分为由各种Web技术文件(.html, .js, .css)组成的前端和由各种 Python 脚本组成的后端。

所有前端文件都应放在一个目录中(如果需要,可以将它们进一步分成多个文件夹)。

my_python_script.py     <-- Python scripts
other_python_module.py
static_web_folder/      <-- Web folder
  main_page.html
  css/
    style.css
  img/
    logo.png

假设您将所有前端文件都放在一个名为 web 的目录中,包括起始页 index.html,则该应用程序将按以下方式启动:

import eel
eel.init('web')
eel.start('index.html')

这将在默认设置(http://localhost:8000)的地址上启动网络服务器,并打开浏览器到 http://localhost:8000/index.html。

如果安装了 Chrome 或 Chromium,则默认情况下,无论操作系统的默认浏览器设置为什么,它都将在“应用程序模式”(App Mode)下打开(带有 --app 的命令行标志)(可以覆写此行为)。

2 App 选项

可以将其他选项作为关键字参数传递给 eel.start()。一些选项包括应用程序所处的模式(例如'chrome'),应用程序运行的端口,应用程序的主机名以及添加其他命令行标志。

从 Eel v0.12.0开始,以下选项可用于 start()

  • mode, a string specifying what browser to use (e.g. 'chrome', 'electron', 'edge', 'custom'). Can also be None or False to not open a window. Default: 'chrome'
  • host, a string specifying what hostname to use for the Bottle server. Default: 'localhost')
  • port, an int specifying what port to use for the Bottle server. Use 0 for port to be picked automatically. Default: 8000.
  • block, a bool saying whether or not the call to start() should block the calling thread. Default: True
  • jinja_templates, a string specifying a folder to use for Jinja2 templates, e.g. my_templates. Default: None
  • cmdline_args, a list of strings to pass to the command to start the browser. For example, we might add extra flags for Chrome; eel.start('main.html', mode='chrome-app', port=8080, cmdline_args=['--start-fullscreen', '--browser-startup-dialog']). Default: []
  • size, a tuple of ints specifying the (width, height) of the main window in pixels Default: None
  • position, a tuple of ints specifying the (left, top) of the main window in pixels Default: None
  • geometry, a dictionary specifying the size and position for all windows. The keys should be the relative path of the page, and the values should be a dictionary of the form {'size': (200, 100), 'position': (300, 50)}. Default: {}
  • close_callback, a lambda or function that is called when a websocket to a window closes (i.e. when the user closes the window). It should take two arguments; a string which is the relative path of the page that just closed, and a list of other websockets that are still open. Default: None
  • app, an instance of Bottle which will be used rather than creating a fresh one. This can be used to install middleware on the
    instance before starting eel, e.g. for session management, authentication, etc.

3 暴露函数

除了前端文件夹中的文件之外,还将在 /eel.js 中提供 JavaScript 库。您应该在任何页面中包括以下内容:


Python 代码中的任何函数可以用 @eel.expose 装饰:

@eel.expose
def my_python_function(a, b):
    print(a, b, a + b)

而 Python 中的代码可以直接在 JavaScript 中像下面的方式调用:

console.log("Calling Python...");
eel.my_python_function(1, 2); // This calls the Python function that was decorated

类似的任何 JavaScript 函数也可以这样暴露:

eel.expose(my_javascript_function);
function my_javascript_function(a, b, c, d) {
  if (a < b) {
    console.log(c * d);
  }
}

然后在 Python 中调用:

print('Calling Javascript...')
eel.my_javascript_function(1, 2, 3, 4)  # This calls the Javascript function

exposed 的名称也可以通过传入第二个参数来覆盖。如果您的应用在构建过程中缩小了 JavaScript,则可能有必要确保在 Python 端可以解析函数:

eel.expose(someFunction, "my_javascript_function");

当将复杂对象作为参数传递时,请记住,在内部将它们转换为 JSON 并通过websocket 发送(该过程可能会丢失信息)。

下面看一个实例:

4 Hello World!

创建一个 HTML 文件 web/hello.html



  
    Hello, World!

    
    
    
  

  
    Hello, World!
  

和一个简单的 Python 脚本 hello.py:

import eel

# Set web files folder and optionally specify which file types to check for eel.expose()
#   *Default allowed_extensions are: ['.js', '.html', '.txt', '.htm', '.xhtml']
eel.init('web', allowed_extensions=['.js', '.html'])

@eel.expose                         # Expose this function to Javascript
def say_hello_py(x):
    print('Hello from %s' % x)

say_hello_py('Python World!')
eel.say_hello_js('Python World!')   # Call a Javascript function

eel.start('hello.html')             # Start (this blocks and enters loop)

如果我们运行 Python 脚本(python hello.py),则会打开一个浏览器窗口,显示hello.html

Eel:利用 HTML 技术构建 Python GUI_第1张图片

并且在终端将看到:

Hello from Python World!
Hello from Javascript World!

5 返回值

尽管我们想将代码视为由单个应用程序组成,但 Python 解释器和浏览器窗口在单独的进程中运行。这可能会使它们之间来回通信变得一团糟,尤其是当我们总是必须显式地将值从一侧发送到另一侧时。

Eel 支持从应用程序另一端检索返回值的两种方法,这有助于使代码简洁。

为了防止在 Python 端永久挂起,已设定尝试从 JavaScript 端检索值的超时时间,默认为 10000 毫秒(10秒)。可以通过 _js_result_timeout 参数更改为 eel.init
JavaScript 端没有相应的超时。

5.1 回调

调用 exposed 函数时,可以立即传递回调函数。当函数在另一侧执行完毕时,将自动使用返回值自动调用此回调。

例如,如果我们在 JavaScript 中定义并暴露了以下函数:

eel.expose(js_random);
function js_random() {
  return Math.random();
}

然后在 Python 中,我们可以像这样从 JavaScript 端检索随机值:

def print_num(n):
    print('Got this from Javascript:', n)

# Call Javascript function, and pass explicit callback function
eel.js_random()(print_num)

# Do the same with an inline lambda as callback
eel.js_random()(lambda n: print('Got this from Javascript:', n))

反过来也一样。

5.2 同步返回

在大多数情况下,对另一端的调用是为了快速检索某些数据,例如小部件的状态或输入字段的内容。在这些情况下,同步几毫秒然后继续执行代码比将整个过程分解成回调更方便。

要同步获取返回值,只需将任何内容都传递给第二组括号即可。因此,在 Python 中,我们将编写:

n = eel.js_random()()  # This immediately returns the value
print('Got this from Javascript:', n)

您只能在浏览器窗口启动后(调用 eel.start() 之后)执行同步返回,否则显然会挂起调用。

在 JavaScript 中,该语言不允许我们在等待回调时进行阻塞,除非通过从 async 函数内部使用 await。 因此,JavaScript 方面的等效代码为:

async function run() {
  // Inside a function marked 'async' we can use the 'await' keyword.
  // Must prefix call with 'await', otherwise it's the same syntax
  let n = await eel.py_random()(); 
  console.log("Got this from Python: " + n);
}

run();

6 异步 Python

Eel基于 Bottle 和 Gevent 构建,它们提供了类似于 JavaScript 的异步事件循环。许多Python的标准库都隐式地假设只有一个执行线程-为了处理这个问题,Gevent 使用“monkey patch”的许多标准模块,例如 time。如果您需要猴子补丁,则应该 import gevent.monkey 并调用gevent.monkey.patch_all() 之后,再 import eel。猴子修补会干扰调试器之类的东西,因此除非有必要,否则应避免。

在大多数情况下,应该避免使用 time.sleep() 而使用 gevent 提供的版本。为了方便起见,直接从 Eel 提供了两个最常用的 gevent 方法sleep()spawn()(同样也节省了导入 time 和/或 gevent 的时间)。

例如:

import eel
eel.init('web')

def my_other_thread():
    while True:
        print("I'm a thread")
        eel.sleep(1.0)                  # Use eel.sleep(), not time.sleep()

eel.spawn(my_other_thread)

eel.start('main.html', block=False)     # Don't block on this call

while True:
    print("I'm a main loop")
    eel.sleep(1.0)                      # Use eel.sleep(), not time.sleep()

然后,我们将运行三个“线程”(greenlets):

  1. Eel 的内部线程用于服务Web文件夹
  2. my_other_thread 方法,重复打印 "I'm a thread"
  3. 主 Python 线程(将停留在最终的 while 循环中)重复打印 "I'm a main loop"

7 打包二进制文件

如果你想让用户下载你的软件使用, 而用户没有安装python, 你最好将你的程序打包成二进制可执行文件, 那么最好使用 PyInstaller

在你的 app 根目录下执行下面的命令:

python -m eel [your_main_script] [your_web_folder]

这将创建文件夹 dist/,如果你想要创建单文件程序,你需要使用 --onefile 参数, 如果不想程序运行的时候有一个黑色命令窗口, 你可以使用 --noconsole 参数。也可以运行像 python -m eel file_access.py web --exclude win32com --exclude numpy --exclude cryptography,排除一些包。更多信息见 PyInstaller 文档。

8 Microsoft Edge

对于 Windows 10 用户,默认情况下会安装 Microsoft Edge(eel.start(.., mode='edge')),如果未安装首选浏览器,则会进行有用的回退。请参阅示例:

  • A Hello World example using Microsoft Edge: examples/01 - hello_world-Edge/
  • Example implementing browser-fallbacks: examples/07 - CreateReactApp/eel_CRA.py

你可能感兴趣的:(Eel:利用 HTML 技术构建 Python GUI)