【python】小規模GUIアプリをHTML/CSS/JSで作成できるEelはいかがですか？

# はじめに
pythonでGUIアプリを作成したい際に選ばれるのはこれらでしょうか。
- **Tkinter**
    - 触った経験なし。よく記事で目にする。
- **PySimpleGUI**
    - 簡単に作成できる。UIが古いイメージ。
- **Flet**
    - 簡単に作成でき、UIもモダンなイメージ。

`Tkinter`は触ったことがないので分かりませんが、`Flet`はコントロールと呼ばれる部品を配置しながらGUIアプリを構築していきます。（PySimpleGUIも同じイメージです）
簡単に作成できる反面、デザインの融通に限界があります。

反面、今回ご紹介する**Eel**は、フロント部分を HTML/CSS/JS を使用して作成できるため、デザインの幅がぐっと広がります。

# 本記事の方向性
`Eel`でできることを網羅的に知りたい、という方には向いていません。
詳細なメソッドや構成に関しては[公式ドキュメント](https://github.com/python-eel/Eel)や[こちらの記事](https://qiita.com/inoory/items/f431c581332c8d500a3b)がおすすめです。

本記事では、ページ遷移やベーステンプレート等の基本的な処理を交えながら、簡単なGUIアプリを作成し、なんとなく理解した気分になれるのを目標としています。

# ひな型をつくる
アプリケーションを作成するために、まずはひな型を作成していきます。

## 仮想環境作成、アクティベート
`venv_eel_test`という仮想環境を作成します。

```zsh
python -m venv venv_eel_test
call venv_eel_test/Scripts/activate
```

## ライブラリインストール
必要なライブラリをインストールしていきます。
2023/12/18時点で動作確認を行ったバージョンを記載しております。

```zsh
pip install Eel==0.16.0
pip install jinja2==3.1.2
```

## ディレクトリ構成
今回の構成は以下です。
※`style.css`は一応配置しておりますが、本記事では触れません。

```
main.py
web/
    css/
        style.css
    js/
        main.js
    templates/
        base.html
        index.html
```

それぞれ、以下のように編集していきます。


まずはベースのテンプレートです。
```base.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>{% block title %}{% endblock title %}</title>
        <link rel="stylesheet" href="../css/style.css" type="text/css">
    </head>
    <body>
        
        {% block content %}{% endblock content %}

        <script type="text/javascript" src="/eel.js"></script>
        <script type="text/javascript" src="../js/main.js"></script>
    </body>
</html>
```

`base.html`に埋め込む形でトップページを作成します。
```index.html
{% extends "base.html" %}

{% block title %}Eel GUI App{% endblock title %}

{% block content %}
<h3>Hello, Eel</h3>
{% endblock content %}
```

今回は小規模なのでテンプレを作成するほどのものではないかと思いますが、何かと楽なので採用しております。

最後はアプリのトリガーとなる`main.py`を修正します。
```main.py
import eel
from jinja2 import Environment, FileSystemLoader


eel.init('web', allowed_extensions=['.js', '.html'])

env = Environment(loader=FileSystemLoader('web'))

@eel.expose
def render_template(template_name, **context):
    template = env.get_template(template_name)
    return template.render(context)

eel.start('templates/index.html', jinja_templates='templates', mode='chrome', size=(900, 600))
```

ここまででひな型は完了です。
cmdで `python main.py` を実行すると、アプリが起動されます。

![top.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3337466/44cb27aa-d82a-2e6e-2afe-2deb27f952be.png)

# 機能を追加していく

ここまでで作成したひな型に、以下のような機能を追加していきます。
- ユーザーがフォーム入力
- 「作成」ボタンを押下すると、「フォームの値.txt」というテキストファイルが作成される。
- 「ファイル一覧ページ」リンクを押下すると、作成したファイル名一覧が表示されるページに遷移する。

トップページイメージ。
![TOP.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3337466/4e787ff4-90c1-c9ee-ee98-b8cc8a790697.png)

ファイル一覧ページイメージ
![ファイル一覧.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3337466/b751a357-7421-1b27-5288-cb562a073967.png)

先ほどのひな型から、以下のようにディレクトリ構成を変更します。

```
main.py
src/
    filehelper.py    <- 追加
web/
    css/
        style.css
    js/
        main.js
        file_list.js    <- 追加
    templates/
        base.html
        index.html
        file_list.html    <- 追加
```

まずトップページを以下のように編集します。

```index.html
{% extends "base.html" %}

{% block title %}Eel GUI App{% endblock title %}

{% block content %}
<h3>Hello, Eel</h3>

<nav>
    <p id="nav__file-list">ファイル一覧ページ</p>
</nav>

<!-- ファイル作成フォーム -->
<div>
    <input id="filename" type="text">
    <button id="btn__create-file">作成</button>
</div>
{% endblock content %}
```

続いて、以下の関数を`filehelper.py`に定義します。
- ファイルを作成する
- ファイル名一覧を読み取り、返す

```filehelper.py
import os
import eel

BASE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')


@eel.expose
def create_file(
    filename: str
) -> None:
    """
    filesフォルダ内に空のテキストファイルを作成する
    """
    if not os.path.exists(os.path.join(BASE_DIR, 'files')):
        os.mkdir(os.path.join(BASE_DIR, 'files'))
    
    filename += '.txt' if not filename.endswith('.txt') else ''
    with open(os.path.join(BASE_DIR, 'files', filename), 'w') as f:
        pass

@eel.expose
def read_files() -> list:
    """
    filesフォルダ内のファイル名一覧を取得する
    """
    filename_list = os.listdir(os.path.join(BASE_DIR, 'files'))
    return filename_list
```

ここで重要なのが `@eel.expose`デコレータです。
これを関数の前に記述することで、javascript側から上記の関数を呼び出すことが出来ます。

さらに、作成した `filehelper.py` を `main.py` でも読み込んであげます。

```main.py
# ... 略 ...

from src import filehelper

# ... 略 ...
```

続いて、「作成」ボタンを押下した際に `create_file` 関数を呼び出してテキストファイルを作成する処理を、`main.js` に記述します。

```main.js
// 「作成」ボタン押下時にcreate_file関数を起動する
const addCreateFileFunc = () => {
    const filenameForm = document.getElementById('filename');
    const btnCreateFile = document.getElementById('btn__create-file');

    if (btnCreateFile !== null) {
        btnCreateFile.addEventListener('click', async () => {
            let filename = filenameForm.value;
    
            await eel.create_file(filename)((res => {
                console.log(`${filename} の作成が完了しました。`);
            }));
        });
    }
}

window.addEventListener('DOMContentLoaded', function(){
    addCreateFileFunc();
});
```

ここでキモなのは以下の部分です。

```main.js
await eel.create_file(filename)((res => {
    console.log(`${filename} の作成が完了しました。`);
}));
```
このような記述方法で、先ほどpythonに定義した関数を呼び出すことが出来ます。
javascriptからpython側の関数を、もしくはpythonからjavascript側の関数を呼び出す方法は[公式GitHub](https://github.com/python-eel/Eel)をご参照ください。

これで、「作成」ボタンを押下した際にファイルを作成する処理を実装できました。

次は、トップページの「ファイル一覧ページ」を押下した際にページ遷移する機能を追加していきます。
再度 `main.js` に修正を加えます。

```main.js
const addCreateFileFunc = () => {
    // ... 略 ...
}

const link = (target) => {
    window.location.href = target;
}

window.addEventListener('DOMContentLoaded', function(){
    addCreateFileFunc();

    // ページ遷移設定
    const navFileList = document.getElementById('nav__file-list');
    if (navFileList !== null) {
        navFileList.addEventListener('click', function () {
            link('file_list.html');
        });
    }
});

```

`window.location.href = 'テンプレート名'`でページ遷移することができます。
このままでは遷移先のページがないので、`file_list.html`を以下のように修正します。

```file_list.html
{% extends "base.html" %}

{% block title %}Eel GUI App{% endblock title %}

{% block content %}
<h3>File List</h3>

<div id="wr__file-list"></div>

<script type="text/javascript" src="../js/file_list.js"></script>
{% endblock content %}

```

`<div id="wr__file-list"></div>` 内に、取得したファイル名を追加していきます。

ファイル名を取得する処理は、先ほど `filehelper.py`に定義しましたので、その関数をjavascript側から呼び出す処理を `file_list.js`に記述していきます。

```file_list.js
const displayFileList = async () => {
    await eel.read_files()((res) => {
        const wrFileList = document.getElementById('wr__file-list');
        const fragment = document.createDocumentFragment();
        res.forEach(filename => {
            let $p = document.createElement('p');
            $p.textContent = filename;

            fragment.appendChild($p);
        });

        wrFileList.appendChild(fragment);
    });
}

window.addEventListener('DOMContentLoaded', function(){
    displayFileList();
});
```

ファイルを作成するときと違うのは、呼び出したpython側の関数から戻り値がある点です。
`res`内に返り値が格納されていますので、要素の数だけループ処理を行い、pタグの作成 ⇒ 追加を行っています。

これらの処理を、ページ遷移しDOMの読み込みが終了した段階で実行しています。
結果、ページ遷移すると、ファイル名一覧がページに表示されます。

![スクリーンショット 2023-12-18 163729.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3337466/231cd59a-cd95-cf14-85ff-07aa38c5d724.png)

以上で終了です。
お疲れ様です、最後までご覧いただきありがとうございました。

# さいごに
株式会社ジールでは、初期費用が不要で運用・保守の手間もかからず、ノーコード・ローコードですぐに手元データを分析可能なオールインワン型データ活用プラットフォーム「ZEUSCloud」を提供しております。
ご興味がある方は是非下記のリンクをご覧ください：
https://www.zdh.co.jp/products-services/cloud-data/zeuscloud/?utm_source=qiita&utm_medium=referral&utm_campaign=qiita_zeuscloud_content-area