想定対象
- PythonでGUIアプリを作ろうとしている
- Pythonの良い感じのGUIライブラリを探している
- デザイン工程をWebデザイナーに丸投げしたい
!!この記事は旅の始まりなのでタイトルに書いたような夢のライブラリは見つけられていません。急ぎの場合はまわれ右です!!
きっかけ
よくあることですが、先日、納期直前にデザインの大幅な変更が行われました。
気付けば稼働時間が自己ベストを更新していました(´;ω;`)
現在GUIアプリ開発は他のライブラリはダサいのでKivyを使っています。
Kivyのメリット
- クロスプラットフォーム
- ロジック(.py)とデザイン(.kv)を分けられる
- デフォルトの素材がかっこいい(大事)
しかしKivyにもデメリットはあります。
- 学習難度が高い + 日本語の情報が貧弱
- .pyファイルと.kvファイル間の通信をちゃんと設計しないと死ぬ
- .pyファイル内にkvを書く方法もあるが、そうするとデザインの修正のたびに実行して確認する必要がある
- .kvファイル内での文法エラーがfalse-negativeになることがある
- widgetへの参照がデフォルトでは弱い参照(強参照をwidgetごとに組み込む必要あり)
学習難度が高いということは致命的で、初見のプログラマがソフト作るのに時間が掛かるということであり、どうせ時間がかかるならCで良いじゃんということになります。Python開発で求められていることは実装速度です。
Kivyはロジックファイルとデザインファイルは別れていますが、つなぎこみが複雑なので本当の意味でロジックとデザインが分離してるとは言えません。
そこで新たに導入するライブラリを探します。譲れない条件は次の2つです。
- クロスプラットフォーム(Win, Mac, iOS, Android)
- デザインとロジックの分離
そこでHTMLですよ
HTMLならデザイナーでも書ける!!
もうね、天才かと思いました。
なんで今まで気づかなかったのか。。
webview的なやつでローカルのHTML読み込んで表示してやれば
ロジック書く人もデザイナーも本拠地で戦えるのです
まずはそういったライブラリを探した
2つありました。
flexxはサンプルコードを見る限り、一般的なPythonのGUIライブラリみたいなことをしてたことと、READMEに「Firefoxインストール推奨」とあったので没。
一方pywebviewは特に悪そうな点はなさそうだったので少しコードを書いてみました。
pywebviewでGUIアプリ
環境
OS: MacOS 10.13.2
Python3.6.4
OS によってインストールのpipコマンドが若干違う
Macの場合:
pip install 'pywebview[cocoa]'
※ クオテーション付けないと[``]
を認識してくれない
まずは生のHTMLが読めるか
※webviewのハンドルがメインスレッドを占領する(?)のでthreadingが必要
import time
import webview
import threading
ev = threading.Event()
def load_html():
html = ''
for i in range(10):
ev.wait()
ev.clear()
html += '<h1>This is dynamically loaded HTML:'+str(i)+'</h1>'
webview.load_html(html)
time.sleep(1)
ev.set()
html += 'Done'
webview.load_html(html)
def main():
t = threading.Thread(target=load_html)
t.start()
ev.set()
if __name__ == '__main__':
main()
webview.create_window('Load HTML Example')
いけた。
pywebviewでJSが読めるか
せっかくなのでjQueryとReactも使ってみました
※どちらもあまり触れないので変なとこあればご指摘ください。。
import time
import webview
import threading
ev_html = threading.Event()
ev_js = threading.Event()
def load_js():
ev_js.wait()
ret = webview.evaluate_js('''
// simple javascript
document.getElementById('test_js').innerHTML = 'Javascript Ok';
// jQuery
$('#test_jq').text('jQuery Ok!!');
// React
var reactElement = React.createElement('div', {}, "React Ok!!!");
React.render(reactElement, document.getElementById('test_re'));
''')
def load_html():
html = ''
ev_html.wait()
ev_html.clear()
# jQuery読み込み
html += '<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>'
# React読み込み
html += '<script src="http://fb.me/react-0.13.3.js"></script>'
html += '<script src="http://fb.me/JSXTransformer-0.13.3.js"></script>'
# html
html += '<h1>This is dynamically loaded HTML with Javascript!:</h1>'
html += '<div id="test_js"></div><div id="test_jq"></div><div id="test_re"></div>'
webview.load_html(html)
time.sleep(1)
ev_html.set()
ev_js.set()
def main():
t1 = threading.Thread(target=load_html)
t2 = threading.Thread(target=load_js)
t1.start()
t2.start()
ev_html.set()
if __name__ == '__main__':
main()
webview.create_window(title='Load JS Example')
いけた模様。
pywebviewでPython→JS通信
現実的な用途としてはボタンとか入力ボックスを作ってその内容を判定したいのでそれらしいものを作りました。
※JS内の中括弧{``}
が二重になってるのは.format()
で出力するためです。速度的なハンデが大きいのでにデバッグ用の書き方です。
import time
import datetime
import webview
import threading
ev_html = threading.Event()
ev_js = threading.Event()
def load_js():
#ev_js.wait()
#ev_js.clear()
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
ret = webview.evaluate_js('''
// jQuery
function jsClick(){{
$('#test_jq').text('{now_jq}');
}}
// React
function reClick(){{
var reactElement = React.createElement('div', {{}}, "{now_re}");
React.render(reactElement, document.getElementById('test_re'));
}}
'''.format(now_jq=now, now_re=now))
#ev_js.set()
def load_html():
html = ''
ev_html.wait()
ev_html.clear()
# jQuery読み込み
html += '<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>'
# React読み込み
html += '<script src="http://fb.me/react-0.13.3.js"></script>'
html += '<script src="http://fb.me/JSXTransformer-0.13.3.js"></script>'
# html
html += '<h1>This is dynamically loaded HTML with Javascript!:</h1>'
html += '<div><input type="button" onClick="jsClick()" value="click jQuery button"><div id="test_jq"></div></input></div>'
html += '<div><input type="button" onClick="reClick()" value="click React button"><div id="test_re"></div></input></div>'
webview.load_html(html)
ev_html.set()
ev_js.set()
def main():
1 ↲
t1 = threading.Thread(target=load_html)
t2 = threading.Thread(target=load_js)
t1.start()
t2.start()
ev_html.set()
if __name__ == '__main__':
main()
webview.create_window(title='Load JS Example')
いけた。。
いけた???
ダメです(´・ω・`)
この例を見て分かるように、JSの実行が1回で終わっています(例では時刻が同じ)。
解決策として
- 定刻監視スレッドを立てて評価しまくる
- PhantomJSなどヘッドレスブラウザを立てる
など、考えましたが危険な香りが漂います。
ちなみにload_js()
内のret
は
type: <objective-c class WebScriptObject at 0x.....>
value: <WebScriptObject: 0x....>
で、Objective-C内の型のようです。
ここの組み込みがまだということでしょう。
良い解決策が見つからない場合は自前で組み込むことも視野に。。
pywebviewでJS→Python通信
実は先ほどの問題と本質は同じでcreate_window
の第二引数js_api
が定義されてないんですよね。サンプルコードもWIPです
webview.create_window(title, url='', js_api=None, width=800, height=600, resizable=True, fullscreen=False, min_size=(200, 100)), strings={}, confirm_quit=False, background_color='#FFF', debug=False, text_select=False)
おそらくこの引数を軸に通信することになりそうです。
まとめ
- Javascriptを使わず、I/O処理の無い範囲(笑)なら現状で全然使える
- Python <=> Javascript の通信問題を解決する必要がある
今後やること
- READMEに書いてあるFlaskやBottleで組んでみる→JSを使わない方向で
- (僕にとって)未知なReactでPython通信できないか探る
- 他のOSでのバージョンを試す
- どうしてもJSが必要ならラッパーを作る
- PyInstallerやpy2exeでfreezeできるかの検証
尻切れでごめんなさい(´;ω;`)