程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式師世界 >> 編程語言 >  >> 更多編程語言 >> Python

使用Python開發美觀GUI

編輯:Python

之前,我有一篇文章《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。

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類提供的所有方法。

HTTP服務

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>

JS無法開啟新Tab

無感同步助手的業務邏輯中,有開啟新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的各種實踐技巧補上。

我是二兩,下篇文章見。


  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式師世界 All Rights Reserved