Skip to main content

对于小型 HTML GUI 应用程序,具有简单的 Python/JS 互操作性

项目描述

鳗鱼

PyPI 版本 皮皮下载 Python 执照

警报总数 语言等级:JavaScript 语言等级:Python

Eel 是一个小的 Python 库,用于制作简单的类似 Electron 的离线 HTML/JS GUI 应用程序,可以完全访问 Python 功能和库。

Eel 托管一个本地网络服务器,然后让您在 Python 中注释函数,以便可以从 Javascript 调用它们,反之亦然。

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

介绍

在 Python 中制作 GUI 应用程序有多种选择,但如果您想使用 HTML/JS(例如,为了使用 jQueryUI 或 Bootstrap),那么您通常必须编写大量样板代码来与客户端通信(Javascript ) 端到服务器(Python)端。

与 Electron 最接近的 Python 等效项(据我所知)是cefpython。对于我想要的东西来说,它有点重。

Eel 不像 Electron 或 cefpython 那样成熟——它可能不适合制作像 Atom 这样的成熟应用程序——但它非常适合制作与团队内部使用的小实用程序脚本等效的 GUI。

出于某种原因,许多一流的数字运算和数学库都使用 Python(Tensorflow、Numpy、Scipy 等),但许多最好的可视化库都使用 Javascript(D3、THREE.js 等)。希望 Eel 可以轻松地将这些组合成简单的实用应用程序,以帮助您的开发。

如果您愿意,可以在Discord上加入 Eel 的用户和维护者。

安装

从 pypi 安装pip

pip install eel

要包括对 HTML 模板的支持,目前使用Jinja2

pip install eel[jinja2]

用法

目录结构

一个 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,包括你的起始页main.html,那么应用程序就这样启动了;

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

这将在默认设置 ( http://localhost:8000 )上启动一个网络服务器,并打开浏览器访问http://localhost:8000/main.html

如果安装了 Chrome 或 Chromium,则默认情况下它将以应用模式打开(带有--appcmdline 标志),无论操作系统的默认浏览器设置为什么(可以覆盖此行为)。

应用选项

其他选项可以eel.start()作为关键字参数传递。

一些选项包括应用程序所处的模式(例如“chrome”)、应用程序运行的端口、应用程序的主机名以及添加额外的命令行标志。

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

  • mode,一个字符串,指定要使用的浏览器(例如'chrome', 'electron', 'edge', 'custom')。也可以NoneFalse不开窗。默认:'chrome'
  • host,一个字符串,指定用于 Bottle 服务器的主机名。默认值'localhost':)
  • port,一个 int 指定用于 Bottle 服务器的端口。用于0自动选择端口。默认值:8000 .
  • block,一个布尔值,表示调用是否start()应该阻塞调用线程。默认:True
  • jinja_templates,一个字符串,指定用于 Jinja2 模板的文件夹,例如my_templates. 默认: None
  • cmdline_args,传递给命令以启动浏览器的字符串列表。例如,我们可能会为 Chrome 添加额外的标志;eel.start('main.html', mode='chrome-app', port=8080, cmdline_args=['--start-fullscreen', '--browser-startup-dialog']). 默认:[]
  • size,一个整数元组,以像素为单位指定主窗口的(宽度,高度)默认值:None
  • position,一个整数元组,以像素为单位指定主窗口的(左,上)默认值:None
  • geometry,一个字典,指定所有窗口的大小和位置。键应该是页面的相对路径,值应该是形式的字典{'size': (200, 100), 'position': (300, 50)}默认: {}
  • close_callback,一个 lambda 或函数,在关闭窗口的 websocket 时调用(即当用户关闭窗口时)。它应该有两个参数;一个字符串,它是刚刚关闭的页面的相对路径,以及仍然打开的其他 websocket 的列表。默认:None
  • app,一个瓶子的实例,将被使用而不是创建一个新的实例。这可用于在启动 eel 之前在实例上安装中间件,例如用于会话管理、身份验证等。

暴露功能

除了前端文件夹中的文件外,还将提供一个 Javascript 库/eel.js。您应该在任何页面中包含此内容:

<script type="text/javascript" src="/eel.js"></script>

包含这个库会创建一个eel可用于与 Python 端通信的对象。

@eel.exposePython代码中任何用这样装饰的函数......

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

...将eel在 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

公开的名称也可以通过传入第二个参数来覆盖。如果您的应用在构建期间缩小了 JavaScript,则可能需要这样做以确保可以在 Python 端解析函数:

eel.expose(someFunction, "my_javascript_function");

将复杂对象作为参数传递时,请记住它们在内部被转换为 JSON 并向下发送到 websocket(一个可能丢失信息的过程)。

埃洛,世界!

查看完整示例:examples/01 - hello_world

把它组合成一个你好,世界!例如,我们有一个简短的 HTML 页面web/hello.html

<!DOCTYPE html>
<html>
  <head>
    <title>Hello, World!</title>

    <!-- Include eel.js - note this file doesn't exist in the 'web' directory -->
    <script type="text/javascript" src="/eel.js"></script>
    <script type="text/javascript">
      eel.expose(say_hello_js); // Expose this function to Python
      function say_hello_js(x) {
        console.log("Hello from " + x);
      }

      say_hello_js("Javascript World!");
      eel.say_hello_py("Javascript World!"); // Call a Python function
    </script>
  </head>

  <body>
    Hello, World!
  </body>
</html>

和一个简短的 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,我们将看到...

Hello from Python World!
Hello from Javascript World!

...在终端中,并且...

Hello from Javascript World!
Hello from Python World!

...在浏览器控制台中(按 F12 打开)。

你会注意到,在 Python 代码中,Javascript 函数在浏览器窗口启动之前就被调用了——任何像这样的早期调用都会排队,然后在 websocket 建立后发送。

返回值

虽然我们想将我们的代码视为包含单个应用程序,但 Python 解释器和浏览器窗口在不同的进程中运行。这会使它们之间的来回通信有点混乱,特别是如果我们总是必须明确地值从一侧发送到另一侧。

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

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

回调

当你调用一个暴露的函数时,你可以在之后立即传递一个回调函数。当函数在另一端完成执行时,此回调将自动与返回值异步调用。

例如,如果我们在 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))

(反之亦然)。

同步返回

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

要同步检索返回值,只需不向第二组括号传递任何内容。所以在 Python 中我们会这样写:

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

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

在 Javascript 中,该语言不允许我们在等待回调时进行阻塞,除非在函数await内部使用async。因此,来自 Javascript 端的等效代码将是:

async function run() {
  // Inside a function marked 'async' we can use the 'await' keyword.

  let n = await eel.py_random()(); // Must prefix call with 'await', otherwise it's the same syntax
  console.log("Got this from Python: " + n);
}

run();

异步 Python

Eel 建立在 Bottle 和 Gevent 之上,它们提供了一个类似于 Javascript 的异步事件循环。很多 Python 的标准库都隐含地假设只有一个执行线程——为了解决这个问题,Gevent 可以“猴子补丁”许多标准模块,例如time.当您调用时,此猴子修补程序会自动完成import eel. 如果你需要猴子补丁,你应该在你之前import gevent.monkey打电话。猴子补丁可能会干扰调试器之类的东西,因此除非必要,否则应避免使用。gevent.monkey.patch_all() import eel

在大多数情况下,您应该避免使用time.sleep(),而是使用gevent. 为方便起见,两个最常用的 gevent 方法,sleep()直接spawn()从 Eel 提供(以节省导入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. 用于服务 web 文件夹的 Eel 的内部线程
  2. my_other_thread方法,重复打印“我是线程”
  3. 将卡在最终while循环中的主 Python 线程,反复打印“我是主循环”

使用 PyInstaller 构建可分发的二进制文件

如果你想将你的应用程序打包成一个可以在没有安装 Python 解释器的计算机上运行的程序,你应该使用PyInstaller

  1. 使用所需的 Python 版本和最少的必要 Python 包配置 virtualenv
  2. 安装 PyInstallerpip install PyInstaller
  3. 在您应用的文件夹中,运行python -m eel [your_main_script] [your_web_folder](例如,您可以运行python -m eel hello.py web
  4. 这将创建一个新文件夹dist/
  5. 可以传递有效的 PyInstaller 标志,例如排除带有标志的模块:--exclude module_name。例如,您可以运行python -m eel file_access.py web --exclude win32com --exclude numpy --exclude cryptography
  6. 当您的应用程序正常运行感到高兴时,添加--onefile --noconsole标志以构建单个可执行文件

有关更多选项,请参阅 PyInstaller的文档。

微软边缘

对于 Windows 10 用户,默认情况下会安装 Microsoft Edge ( eel.start(.., mode='edge')),如果未安装首选浏览器,这是一个有用的后备选项。请参阅示例:

项目详情