PyWebViewでBootstrapを使用したUIを作り、PyInstallerでEXEにする

PythonのGUIをHTMLで構築するライブラリ、PyWebViewを使って、既存のHTMLやCSSを読み込み。さらにPyInstallerを使ってEXEとして使えるようにパッケージしてみます。

# 用意するもの
* Windows 10 1903 Ver
* Python 3.7.5
* PyInstaller 3.7
* PyWebView 3.0.2
* pipenv 2018.10.13
* Bootstrap(今回はBootstrap-honoka 4.1.3を利用)

なお、パッケージングの都合上、pipenvはプロジェクトフォルダに配置されていた方が都合が良いため、予め環境変数に`PIPENV_VENV_IN_PROJECT=true`を設定しておきます。

# 作業

## 必要なライブラリをインストールする
とりあえず作業を開始します。適当なフォルダを作り、pipenvでPyInstallerとPyWebViewをインストールします。

```powershell
mkdir pywebtest
cd pywebtest
pipenv install --python C:\Python36\Python.exe
pipenv install pywebview
pipenv install --dev pyinstaller
``` 
次に、最低限必要なフォルダを作成します

```powershell
mkdir html
mkdir src
```

## bootstrapを導入する
次に、Bootstrapを導入します。今回はnpmを使ってライブラリを導入します

```powershell
npm init
... // パッケージの設定は適当で良いです
npm install --save bootstrap-honoka
```

## GUI用のHTML一式を作成する
htmlフォルダには以下の三つのファイルを作成します。

* html/main.html
* html/main.css
* html/main.js

```html:html/main.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Test</title>
</head>
<body>
<h1>It works!</h1>
<button class="btn btn-primary">test button</button>
</body>
</html>
```

```css:html/main.css
body{
  padding: 12px;
}
h1::after{
  content: " with CSS";
}
```

```js:html/main.js
document.querySelector("button").addEventListener("click", function(){
  alert("It works!");
});
```

ここまでで大体次のような感じになっていると思います。

```text
pywebtest
│  package-lock.json
│  package.json
│  Pipfile
│  Pipfile.lock
│  
├─.venv
│      
├─html
│      main.css
│      main.html
│      main.js
│      　
├─node_modules
│  └─bootstrap-honoka
└─src
        ここにソースファイル
```

# GUI呼び出しのためのコードを作成する

それではGUI作成のためのコードを書いてみます。注意点としては以下の二つ

* PyWebViewは、ウィンドウを表示すると、そのスレッドを占有してしまうこと（その後に処理を行いたい場合は、スレッドを作る必要があること
* PyWebViewにはHTMLやCSSのファイルを読み込むメソッドはないので、load_html, load_css, evaluate_jsメソッドにopenしたファイルを読み込ませる必要があること

特に気をつけるのは上です。以下のような感じで読み込んでみます。

```py:src/main.py
import webview

def load_css(path):
  with open(path, mode="r", encoding="utf-8") as f:
    webview.load_css(f.read())

def load_js(path): 
  with open(path, mode="r", encoding="utf-8") as f:
    webview.evaluate_js(f.read())

def load_thread():
  load_html("html/main.html")
  load_css("html/main.css")
  load_css("node_modules/bootstrap-honoka/dist/css/bootstrap.min.css")
  load_js("html/main.js")

with open(path, mode="r", encoding="utf-8") as f:
  html = f.read()
window = webview.create_window(title="Test Application", html=html, width=640, height=320)
webview.start(load_thread, window)
```

ここまでできたら、`pipenv run`で実行します。

```powershell
pipenv run ./src/main.py
```

すると、とりあえずアプリが起動し、無事にBootstrapが読み込まれた状態で、HTMLが表示されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/14499/52886776-e625-64df-b3ca-1eee6e6a9a98.png)

なお、一件`load_html`を`load_css`のあとに呼び出せば処理が高速化されそうなものですが、これをやるとGUIの表示段階で無限ループに陥ってしまうようなので上手くいきません。必ずHTMLを読み込んでからCSSを読み込みようにします。

# PyInstallerで実行ファイルを作る
それではPyInstallerを使ってみようと思います。[以前](https://qiita.com/TakamiChie/items/8dba8459343db898b335)と同様、ビルド用のPowerShellスクリプトを書いてビルドを行います。

```powershell:create_exe.ps1
# ディレクトリの用意
if (Test-Path dist) {
  Remove-Item dist -Recurse | Out-Null
}
New-Item dist -ItemType Directory | Out-Null
# windllを捜索
Write-Host "> Searching WinDLL"
$windll = "c:\windows\WinSxS\x86_microsoft-windows-m..namespace-downlevel_*"
$windllpath = "."
if(Test-Path $windll){
  $item = (Get-ChildItem $windll | Sort-Object LastWriteTime)[0]
  $windllpath = '"c:\windows\WinSxS\{0}"' -f $item.Name
}
# pyinstaller
Write-Host "> Creation EXE"
pyinstaller `
  --name TestAPP `
  --windowed `
  --path $windllpath `
  --path C:/Windows/System32/downlevel `
  --specpath ./dist/ `
  --distpath ./dist/dist `
  --workpath ./dist/build `
  --add-data "../html;html" `
  --add-binary "../.venv/Lib/site-packages/webview/lib;webview/lib" `
  src\main.py
Write-Host "> Complete"
```
ここで気をつけるのは

* add-binaryオプションでPyWebViewのDLLをパッケージに含めておくこと(パスも合わせておくこと)
* add-dataオプションでhtmlファイルを配置しておくこと

無事exeが作成されたら、実行してみます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/14499/459f1357-d659-915c-0cae-2058292a1298.png)

単独のアプリとして実行できました。

同じようなHTMLでGUIを実現するライブラリは、eelというライブラリもあるのですが、そちらは既存のChromeをそのまま使う（拡張機能がインストールされていたら、それらも使える状態）ようになっていることや、そもそもPyInstallerでパッケージするとうまく動かないことから、採用できませんでした。

PyWebViewは ~~EdgeHTMLを使うため~~ 残念ながらTrident(IE11)を使います(いちおうEdgeHTMLへの移行予定自体はあるようですが)。PyWebViewの引数JSAPIで指定したPythonオブジェクトの戻り値などはIE11が対応していないPromiseオブジェクトですので、かなり従来のHTMLコーディングとは異なるアプローチが必要になるでしょう。

ただ、そこまで無理しなければアプリのGUIでは一応良いんじゃないでしょうか。[ファイルやフォルダを開くダイアログを表示する](https://pywebview.flowrl.com/guide/api.html#create-file-dialog)などのメソッドも完備していますし。

# 2019/10/22追記：CEFPythonを使う

PyWebViewでCEFというものが使えるようになりました。これを使えば、ブラウザエンジンをChromiumにすることができます(いちおうMSHTML(Edge)にも対応しているようですが、どうせ今後はこちらもChromiumになってしまうので…)。

こちらを使う場合は、まず`cefpython3`と`pythonnet`という二つのモジュールを追加します。

```powershell
pipenv install cefpython3
pipenv install pythonnet
```

PyInstallerでEXEを作るとき、`cefpython3`ディレクトリに入っているDLLが必要になるため、pyinstallerの呼び出しコードに、次のコマンドを追加します。

```powershell:create_exe.ps1(pyinstallerのオプションに追加)
  --add-binary "../.venv/Lib/site-packages/cefpython3/;." `
```

で、最後にPyWebViewの公式にも書かれているように、`webview.start()`に`gui="cef"`という引数を付け加えます。

```powershell:main.py
# …
window = webview.create_window(title="Test Application", width=640, height=320)
webview.start(load_thread, window, gui="cef")
```

また、`webview.start()`メソッドの引数に`debug="true"`をつけると開発者モードのウィンドウまで出せるようになります。これで開発が相当やりやすくなる。

## CEFPythonの開発者ウィンドウで、JavaScriptのトレースを行う

CEFPythonを使った場合、HTMLはdata-uriの形式でChromiumに読み込まれています。そのため、この状態でコンソールからJavaScriptを追っていくのは難しい。

そんなときは、JavaScriptの冒頭で、何か適当に`console.log`しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14499/e271de31-0b8c-e385-f32a-e2560f7c35e0.png)

すると、右側に行番号が表示されるので、そこからJavaScriptのソースを見ることができます。