[Python] EelをつかってHTML/CSS/JavaScriptでGUIを構築

# はじめに

* PythonでGUIのアプリを作りたい。
* TkinterやKiviなどは試したけど、凝ったレイアウトを作ろうとして挫折した。
* GUIをHTML/CSS/JavaScriptのWEB技術で作りたい。でも、Electronを使うのは腰が重い。

そんなときピッタリなPythonのGUIライブラリが[Eel](https://github.com/ChrisKnott/Eel)です。
Eelは、とてもシンプルなElectronライクなGUIライブラリで、NumpyやPandasなどデータ処理などに秀でているPythonと、D3.jsなど描画に優れているJavaScriptをつなぎ合わせたアプリを、いとも簡単に構築することができます。

以下、[公式GitHubのReadme](https://github.com/ChrisKnott/Eel)に沿って、説明します。

* 実行環境
    * Windows 10
    * Python3.6
    * Eel 0.9.7

# インストール

```pip install eel```しましょう。

# Hello World

ルートに`main.py`を作成し、WEB関係のファイルは`web`ディレクトリの下に置きます。

```
main.py
web/
  main.html
  css/
  js/
```

わずか3行のスクリプトを書き、

```python:main.py
import eel
eel.init("web")
eel.start("main.html")
```

適当なHTMLを書いて`python main.py`と実行すると

```html:main.html
<html>
<head>
    <meta charset="UTF-8">
    <title>Eel</title>
</head>
<body>
    Hello World!!
</body>
</html>
```
これだけでHelloWorldが完成です。
![hello.png](https://qiita-image-store.s3.amazonaws.com/0/61468/01baf2eb-edd4-aa31-7325-8e26771c829b.png)


# PythonからJavaScript関数を呼ぶ

`eel.js`を読み込んだうえで、Pythonから呼びたい関数を`eel.expose(js_function)`します。

```html:main.html
<html>
<head>
    <meta charset="UTF-8">
    <title>Eel</title>
</head>
<body>
    <div id="target"></div>
    <script type="text/javascript" src="/eel.js"></script>
    <script type="text/javascript">
        eel.expose(js_function)
        function js_function(values) {
            var s = ""
            for (var i = 0; i < values.length; i++) { s += values[i] + "<br>" }
            document.getElementById("target").innerHTML = s
        }
    </script>
</body>
</html>
```

`eel.js_function()`で`expose`されたJavaScript関数を呼ぶことができます。

```python:main.py
import eel
import numpy as np

eel.init("web")
eel.js_function(np.random.rand(4).tolist()) # JSON serializableでないとダメ
eel.start("main.html")
```

## JavaScriptの関数の戻り値をPythonで取得
JavaScriptの関数の戻り値をPythonで取得するには、Callbackと同期待ちの2種類の方法があります。

### Callbackを設定
1つは、JavaScript関数が終了された後に実行されるCallback関数を設定する方法です。

```javascript
eel.expose(js_function);
function js_function(args) {
  return "hogehoge";
}
```

```python
eel.js_function(args)(lambda val: print(val + " from JavaScript"))
```

### 同期待ち
もう1つは、空のカッコをつけて、JavaScript関数の終了を待つ方法です。`eel.start()`の前に呼ぶとフリーズしてしまいます。

```python
val = eel.js_function(args)()
```


# JavaScriptからPython関数を呼ぶ

JavaScriptから呼びたいPython関数を、`@eel.expose`でデコレートします。

```python:main.py
import eel

@eel.expose
def python_function(val):
    print(val + " from JavaScript")

eel.init("web")
eel.start("main.html")
```

`eel.js`を読み込んだうえで、`eel.python_function(args)`でPython関数を呼ぶことができます。
JavaScriptの配列はPythonの`list`に、連想配列は`dict`へと変換されます。

```html:main.html
<html>
<head>
    <meta charset="UTF-8">
    <title>Eel</title>
</head>
<body>
    <script type="text/javascript" src="/eel.js"></script>
    <script type="text/javascript">
        eel.python_function("aa")
    </script>
</body>
</html>
```

## Pythonの関数の戻り値をJavaScriptで取得

`async` `await` を使います。

```python
@eel.expose
def python_function2():
    return "Hello"
```

```javascript
async function run() {
    let val = await eel.python_function2()();
    console.log(val + " from Python")
}
run();
```


# 起動時のオプション設定

```python
web_app_options = {
    "mode": "chrome-app",  # chromeのアプリケーションモードで起動
                           # "chrome" とすると、通常のchromeで起動
    "port": 8080,
    "chromeFlags": [
            "--start-fullscreen",  # フルスクリーンで起動 他のChromeが起動していると機能しない？
            # "--window-size=800,600",
            # "--window-position=0,0",
    ]
}
eel.start("main.html", options=web_app_options)
```

# マルチスレッド

`eel.sleep()`や、`eel.spawn()`を使うことで、簡単にマルチスレッド化できます。下の例は、

* WEBサーバをホストするスレッド
* `my_other_thread`
* メインスレッド

の3つが走るスクリプトです。

```python
import eel
eel.init("web")
def my_other_thread():
    while True:
        print("I'm a thread")
        eel.sleep(1.0) # time.sleep()でなくて、eel.sleep()を使うこと
eel.spawn(my_other_thread)
eel.start("main.html", block=False) # block=Falseとしないと、ここで止まってしまう
while True:
    print("I'm a main loop")
    eel.sleep(1.0)
```

# 動作サンプル ( Smoothie によるリアルタイムなグラフ)

Pythonでグラフといえばmatplotlibですが、ここでは、JavaScriptのライブラリ[Smoothie Charts](http://smoothiecharts.org/)を使ってリアルタイムなグラフを描画してみます。

```python:main.py
import random
import time
import eel

eel.init("web")
web_app_options = {"chromeFlags": ["--window-size=420,200"]}
eel.start("main.html", options=web_app_options, block=False)

while True:
    eel.push_data([
        {"time": time.time() * 1000, "value": random.random()}, # JSのgetTime()と同じにするためミリ秒に直す
    ])
    eel.sleep(0.2)
```
```html:main.html
<html>
<head>
    <meta charset="UTF-8">
    <title>Chart</title>
</head>
<body>
    <div id="parent" style="height: 100%;">
        <canvas id="mycanvas"></canvas>
    </div>
    <script type="text/javascript" src="/eel.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/smoothie/1.34.0/smoothie.min.js"></script>
    <script type="text/javascript">
        // canvasの高さと幅を画面に合わせる
        var parent = document.getElementById("parent")
        var canvas = document.getElementById("mycanvas")
        canvas.width = parent.clientWidth
        canvas.height = parent.clientHeight

        var smoothie = new SmoothieChart();
        smoothie.streamTo(canvas);
        var series = new TimeSeries();
        smoothie.addTimeSeries(series);

        eel.expose(push_data)
        function push_data(values) {
            for (var i = 0; i < values.length; i++) {
                series.append(values[i]["time"], values[i]["value"]);
            }
        }
    </script>
</body>
</html>
```

![chart.png](https://qiita-image-store.s3.amazonaws.com/0/61468/1aa17e38-f847-32c9-5298-04295140b1af.png)


# バイナリ化

1. `pip install pyinstaller`をして、[PyInstaller](https://www.pyinstaller.org/)をインストールする
2. `main.py`のディレクトリで`python -m eel main.py web`と実行すると、必要な引数を加えてPyInstallerが実行されます
3. すると、`dist`ディレクトリ以下に実行ファイルが出来上がります

* PyInstallerのオプションを引数に加えることもできます
    * `--onefile` ひとつのファイルにまとめる
    * `--noconsole` コンソールを表示させない
    * `--exclude [モジュール名]` 不要に取り込まれてしまったモジュールを取り除く