Edited at

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

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


用意するもの


  • Windows 10 1809 Ver

  • Python 3.6.6

  • PyInstaller 3.7

  • PyWebView 2.2.1 

  • pipenv 2018.10.13

  • Bootstrap(今回はBootstrap-honoka 4.1.3を利用)

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


作業


必要なライブラリをインストールする

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

mkdir pywebtest

cd pywebtest
pipenv install --python C:\Python36\Python.exe
pipenv install pywebview
pipenv install --dev pyinstaller

次に、最低限必要なフォルダを作成します

mkdir html

mkdir src


bootstrapを導入する

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

npm init

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


GUI用のHTML一式を作成する

htmlフォルダには以下の三つのファイルを作成します。


  • html/main.html

  • html/main.css

  • html/main.js


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>


html/main.css

body{

padding: 12px;
}
h1::after{
content: " with CSS";
}


html/main.js

document.querySelector("button").addEventListener("click", function(){

alert("It works!");
});

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

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したファイルを読み込ませる必要があること

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


src/main.py

import webview

import threading

ev = threading.Event()

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

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")

threading.Thread(target=load_thread).start()

webview.create_window(title="Test Application", width=640, height=320)


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

pipenv run ./src/main.py

すると、とりあえずアプリが起動し、無事にBootstrapが読み込まれた状態で、HTMLが表示されます。

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


PyInstallerで実行ファイルを作る

それではPyInstallerを使ってみようと思います。以前と同様、ビルド用の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が作成されたら、実行してみます。

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

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

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

ただ、そこまで無理しなければアプリのGUIでは一応良いんじゃないでしょうか。ファイルやフォルダを開くダイアログを表示するなどのメソッドも完備していますし。