之前,我有一篇文章《Python GUI美化小技巧》,其中提到了兩種美化GUI的方式,嗯,兩種方式我都不太滿意。
Tkinter Designer需要學習Figma這款設計軟件如何使用,其美化GUI的原理就是用圖片去填充,從而讓Tkinter有比較美觀的樣式,抱歉,我暫時還不想學Figma。
而文中另外一個方案:TTk Bootstrap其實是更大的坑,如果你只是開發單窗口GUI軟件,TTkBootstrap挺好的,但當你有開啟一個窗口後,關閉並開啟另外一個新窗口的需求時,TTKBoostrap就是一個大坑,新的窗口會丟失掉相應的樣式。
我希望Python生態中有類似Electron的庫,讓我們可以利用HTML、CSS、JS來構建前端頁面,而後端的邏輯使用Python來實現,這樣就可以輕松構建出美觀的GUI了。
有我微信好友的朋友應該知道,上兩周我發布了視頻同步助手,可以幫助大家將視頻一鍵同步到6個不同的媒體平台,這個軟件便使用PyWebView框架開發,軟件界面如下:
PyWebView可以讓你通過HTML、CSS、JS來構建GUI的前端頁面,使用Python來實現軟件中的業務邏輯,再配合使用Pyinstaller實現軟件打包,便可以構建一個美觀的GUI應用了。
遺憾的是PyWebView資料比較少,Github的star數也不多,我實際操作下來,有些Windows平台無法打開,因精力有限,也沒有去深究為何無法打開。
本文便簡單介紹PyWebView的使用方法,相比於官方文檔,會突出其中遇到的坑和相應的解決方案,也算是視頻同步助手這個項目的技術復盤吧(嗯,變現階段失敗了,所以)。
光讀PyWebView的文檔,是比較難起步的,比較好的方式是去PyWebView的github中將源碼拉下來,其中有example目錄,提供了一下例子項目,其中要搞懂的核心邏輯是,前端JS寫的邏輯,怎麼去與Python互通。
PyWebView提供了js api和接口請求這兩種方法,先看js api。
在實例化webview時,將定義好的Api類實例作為js_api的參數傳入:
class Api():
def addItem(self, title):
print('Added item %s' % title)
def removeItem(self, item):
print('Removed item %s' % item)
def editItem(self, item):
print('Edited item %s' % item)
def toggleItem(self, item):
print('Toggled item %s' % item)
def toggleFullscreen(self):
webview.windows[0].toggle_fullscreen()
def load_page(self):
file_url = 'file:///C:/Users/admin/workplace/github/pywebview/examples/todos/assets/hello.html'
webview.windows[0].load_url(file_url)
if __name__ == '__main__':
api = Api()
# js_api 獲得 api 實例
webview.create_window('Todos magnificos', 'assets/index.html', js_api=api, min_size=(600, 450))
webview.start(debug=True)
前端通過window.pywebview.api來使用Api類中提供的方法,比如調用addItem方法:
window.pywebview.api.addItem(title)
因為我們項目在啟動時,開啟了debug模式,所以可以按F12打開浏覽器的DevTools,在控制台中,可以輕松打印出window.pywebview中各種屬性,其中的api屬性包含了Python中Api類提供的所有方法。
JS api對於一些比較復雜的需求不太好用,此時可以使用HTTP服務的形式,具體而言,利用Flask開啟一個web服務,前端的操作通過HTTP請求來調用該Web服務。
首先,基於Flask構建服務實例:
from flask import Flask
server = Flask(__name__, static_folder=gui_dir, template_folder=gui_dir)
然後再實例化webview:
# 傳入server實例,開啟HTTP服務模式
window = webview.create_window('My first pywebview application', server)
webview.start(debug=True)
前端JS通過Ajax的形式,請求Flask提供的接口,從而讓後端執行相關的邏輯,如果沒有使用Vue、React等框架時,我個人便習慣使用Jquery提供的ajax方法,並在該方法基礎上再封裝多一層。
function doAjax(url, method, data, success, fail) {
if (success === undefined) {
success = function (data) {
console.log('[success] ', data)
}
}
if (fail === undefined) {
fail = function (err) {
console.log('[fail]', err)
}
}
if (method === 'POST') {
data = JSON.stringify(data)
}
$.ajax({
url: url,
type: method,
cache: false,
dataType: "json",
headers: {
"token": window.token
},
data: data,
contentType: "application/json; charset=utf-8",
success: success,
fail: fail
})
}
在需要調用後端邏輯時,直接請求接口則可:
function get_video_settings_data(video_id) {
doAjax("/video_settings/data", "GET", {video_id: video_id}, get_video_settings_data_handler);
}
與之對應的後端接口與普通的Flask接口沒啥差異:
@server.route('/video_settings/data', methods=['GET'])
def get_video_settings_data():
video_id = request.args.get('video_id')
video_id = video_id.split('-')[-1]
data = VideoTaskSettingsModel.get_settings(video_id=video_id)
if not data:
video_settings = {}
else:
video_settings = data[0]
return jsonify({'status': 'ok', 'video_settings': video_settings})
PyWebView為了避免接口被惡意請求,其官方例子中建議在請求時,header中都帶上一個Token,用於識別當前請求,這個Token PyWebView會幫我們生成好放在window.pywebview.token中。
import webview
def verify_token(function):
@wraps(function)
def wrapper(*args, **kwargs):
token = request.headers.get('token')
# 判斷前端傳遞token是否與webview.token一致,從而校驗請求是否合法
if token == webview.token:
return function(*args, **kwargs)
else:
raise Exception('Authentication error')
return wrapper
@server.route('/video/task_list')
@verify_token
def video_task_list():
'''
視頻配置列表
:return:
'''
video_tasks = VideoTaskModel.get_all_video_task()
response = {
'status': 'ok',
'video_tasks': video_tasks
}
return jsonify(response)
在開發視頻同步助手時,為了方便,沒有使用Vue+PyWebView的形式,即我無法使用類似vue-router的東西來實現頁面變化的效果,那要怎麼弄呢?
視頻同步助手前端利用Flask的模板語法+Jquery實現主要邏輯,後端就是Flask+JS Api,沒錯,PyWebView允許你同時開啟HTTP服務與JS Api支持。
為了實現頁面的變化,我將側邊欄這些都寫到base.html中,然後通過Flask模板繼承的能力避免每個頁面中都弄個側邊欄。
當我們要訪問不同頁面時,發現PyWebView不允許你通過render_template方法獲得新頁面,PyWebView要實現頁面更新只有兩種方式,一是使用load_url方法,提供一個url,webview會去加載新的頁面,另一種是使用load_html方法,該方法會加載html文件。
遺憾的是,load_html方法加載html時,不會執行html中引入的JS,所以我們只能通過load_url方法去做。
回顧一下PyWebView HTTP服務的使用,我們利用Flask構建了web實例,但Web實例啟動後監聽的端口這些是由PyWebView隨機分配的,我們無法獲得,而load_url方法需要完整的url才能獲取。
為了解決這個問題,只能在PyWebView剛啟動時,將url記錄起來,然後再利用記錄的url拼接出完整的URL,實現頁面的切換。
PyWebView啟動後,如果開啟了HTTP服務,會默認訪問根路徑,我們在這裡記錄一下url:
@server.route('/')
def video_task_template():
"""
視頻任務
"""
set_base_url(request.base_url)
# 省略...
配合JS Api,來調用load_url方法:
class JSApi():
def load_page(self, page_name):
url = urljoin(get_base_url(), page_name)
webview.windows[0].load_url(url)
用戶點擊側邊欄時,使用JSApi提供的load_page方法:
window.token = '{
{ token }}';
function view_video_task() {
window.pywebview.api.load_page('/')
}
function view_user() {
window.pywebview.api.load_page('/user_template')
}
Flask中配到接口,通過render_template返回頁面數據:
@server.route('/user_template')
def user_template():
"""
用戶列表
"""
set_base_url(request.base_url)
return render_template('user.html', token=webview.token)
通過這種方式,利用了PyWebView的load_url方法,成功加載新頁面,實現頁面的切換,頁面中的JS也可以正常執行。
用戶需要添加自己的視頻到無感同步助手中,會發現,視頻元素無法顯示,其原因在於,視頻可能在用戶系統的任意位置,當前Flask構建的web服務因為安全沙箱的原因,無法讀取系統中任意位置的元素並返回。
我們看到Flask實例化時,需要傳輸static_folder,我們才能正常訪問存放在static_folder中的js、css文件,template_folder也是一個道理。
frontend_dir = os.path.join(root_path, 'gui', 'frontend')
server = Flask(__name__, static_folder=frontend_dir, template_folder=frontend_dir)
我們是否可以為Flask添加多個static,從而實現讓Flask可以訪問系統中任意元素的效果呢?嗯,可以的,利用Flask提供的send_from_directory方法。
from flask import request, send_from_directory
@server.route('/cdn')
def custom_static():
file_dir_path = request.args.get('file_dir_path')
filename = request.args.get('filename')
return send_from_directory(file_dir_path, filename)
前端訪問的形式為:
<div class="video-source-div">
<video class="video-source" controls="">
<source id="video-srouce-5" src="cdn?file_dir_path=C:\Users\admin\Videos\myvideos&filename=無感助手開發反思.mp4" metadata="" preload="auto" type="video/mp4"></source>
</video>
</div>
無感同步助手的業務邏輯中,有開啟新Tab頁面的需求,網上常見的解決方法為:
window.open('https://www.baidu.com', '_blank')
正常而言,這會開啟一個新的Tab,但Chrome為了避免網站惡意開啟新的Tab,默認是不允許純JS開啟新Tab的操作的。
怎麼解決?翻了大半天Chrome的文檔,才知道維護window.open無效以及解決方法。
訪問Chrome中關於彈出窗口的設置:chrome://settings/content/popups
然後將默認修改改成【網站可以發送彈出式窗口並使用重定向】則可。
PyWebView會使用系統自帶的浏覽器內核來給你的GUI程序提供前端展示,而Electron是自己打包進一個Chromium,所以PyWebView打包會小一點,但穩定性上,個人體驗下來,比Electron差上一些。
此外,Pyinstaller打包也是一個大坑,後續有空,再將Pyinstaller的各種實踐技巧補上。
我是二兩,下篇文章見。