使用Python建立WebUI
Python與HTML5如果能夠強強聯合,那麼一定能夠產生功能非常強大的應用。本系列文章將講述如何實現javascript和Python的互相呼叫。本文是系列第一篇,講述如何在javascript中呼叫Python。===============================================================================================================================================================
一、關於WebUI
廣義的WebUI是指一切基於瀏覽器的人機互動介面。與一般的『網站』相比,WebUI更側重於『獲取物理性質的資訊』以及『做出控制』,換句話說,WebUI可以認為是傳統桌面UI的升級。
WebUI是圖形介面應用程式的發展趨勢,例如,知名的Visual Studio Code,PyCharm等。網際網路革命造就了大量的基於HTML5/javascript技術的表現方法,同時造就了一大批『前端工程師』,利用這些儲備,開發像網頁一樣的內容豐富、功能強大、美觀大方的桌面應用程式也就變成一件自然而然的事情了。
WebUI的關鍵,在於HTML5/javascript如何打破瀏覽器的『隔離』,能夠更『底層』的訪問計算機本身。目前有很多技術能夠做到這一點,本系列為大家介紹的是基於PyQt的方法。
目前,PyQt已經放棄WebKit,轉而使用基於Chromium 架構的瀏覽器引擎。Chromium 與WebKit相比有很多好處,不過鑑於瀏覽器大戰不是本文主要討論的內容,因此在此不詳細展開。
二、安裝PyQt
本系列文章使用最新的PyQt5.11(基於Qt5.11)。它支援的最低的Python版本是3.5。安裝很簡單。加-U選項能夠讓你得到最新版的PyQt。
pip3 install pyqt5 -U
三、基本原理
我們使用PyQt中的QWebEngine,它是Chromium 的一個包裝。與cefpython3等競爭框架相比,PyQt的QWebEngine開發難度更低,並且能夠借力PyQt的其他功能。
下圖展示了使用QWebEngine實現javascript與Python互動的原理:

藍色的部分為Python實現,把想實現的功能寫一個類,例化為MyObject這樣一個物件。然後使用QWebChannel將這個物件註冊到QWebEnginePage裡就可以了。在javascript一端,使用Qt提供的qwebchannel.js可以得到一個javascript版的QWebChannel,使用它的objects成員就能得到MyObject的一個『影子』,它攜帶有Python之中MyObject一樣的函式。
四、一個例子
下面用一個例子進行說明。這個例子要實現3個功能:在WebUI中能夠把內容輸出到終端;能夠改變視窗的標題;能夠直接寫一個檔案。
我們首先構造實現這些功能的MyObject
#MyObjectCls.py from PyQt5.QtCore import * class MyObjectCls(QObject): sigSetParentWindowTitle = pyqtSignal(str) def __init__(self,parent=None): QObject.__init__(self,parent) @pyqtSlot(str) def consolePrint(self,msg): print(msg) @pyqtSlot(str) def setParentWindowTitle(self,msg): self.sigSetParentWindowTitle.emit(msg) @pyqtSlot(str,str) def saveFile(self,content,fileName): with open(str(fileName),"w") as fp: fp.write(str(content))
開發要點:
- 必須繼承QObject並寫好init函式
- 凡是『匯出』給javascript的函式,需要加pyqtSlot修飾器。
- 與其他QObject互動仍然可以使用pyqtSignal
接下來完成PyQt應用程式的其他部分,包括QWebEngine的容器等。這個一旦寫好,接下來如果有功能升級的需要的話,需改動的部分很少。
#webui.py from PyQt5.QtCore import * from PyQt5.QtWidgets import * from PyQt5.QtWebEngine import * from PyQt5.QtWebChannel import * from PyQt5.QtWebEngineWidgets import * from MyObjectCls import MyObjectCls class MainWin(QWebEngineView): def __init__(self,main_entry): QWebEngineView.__init__(self) self.__channel = QWebChannel(self.page()) self.__my_object = MyObjectCls(self) self.__channel.registerObject('MyObject',self.__my_object) self.page().setWebChannel(self.__channel) self.__my_object.sigSetParentWindowTitle.connect(self.setWindowTitle) self.page().load(QUrl.fromLocalFile(main_entry)) if __name__ == '__main__': import sys,os app = QApplication(sys.argv) main_entry = os.path.realpath(os.path.dirname(__file__) + "/content/index.html") w = MainWin(main_entry) w.resize(400,500) w.show() app.exec_()
為了驗證webui.py的功能,在其所在目錄建立content目錄,在content目錄建立index.html,寫一個最基本的HTML5網頁:
<!DOCTYPE html> <html> HELLO! </html>
執行的結果應該是這樣的:

好的現在我們已經得到了一個瀏覽器。此時,『前端』工程師就可以繼續開發index.html了。根據之前提到的3個功能進行控制元件的建立工作。
需要說明的是,QWebChannel不依賴任何前端框架,也就是說完全可以使用喜歡的框架進行開發。我們這裡選擇了bootstrap3和jQuery。首先把框架搭好,除錯工具可以使用Chrome或者FireFox。HTML5程式碼如下:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link href="bootstrap/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div class="container"> <div class="page-header"> <small>Console Output</small> </div> <div class="input-group"> <input type="text" class="form-control" id="txt0" placeholder="Enter Text Here..."> <span class="input-group-btn"> <button class="btn btn-default" id="btn0" type="button">Go!</button> </span> </div> <div class="page-header"> <small>Set Window Title</small> </div> <div class="input-group"> <input type="text" class="form-control" id="txt1" placeholder="Enter Text Here..."> <span class="input-group-btn"> <button class="btn btn-default" id="btn1" type="button">Go!</button> </span> </div> <div class="page-header"> <small>Save A File</small> </div> <input type="text" class="form-control" id="txt3" placeholder="Enter File Name Here..."> <div class="input-group"> <input type="text" class="form-control" id="txt2" placeholder="Enter Content Here..."> <span class="input-group-btn"> <button class="btn btn-default" id="btn2" type="button">Go!</button> </span> </div> </div> <script src="jquery/jquery-1.12.4.min.js"></script> <script src="bootstrap/js/bootstrap.min.js"></script> <script src="qrc:///qtwebchannel/qwebchannel.js"></script> <script src="control.js"></script> </body> </html>
由於我們希望能夠離線執行這個應用,因此把bootstrap3、jQuery都下載了下來,供本地使用。
注意其中兩行:
<script src="qrc:///qtwebchannel/qwebchannel.js"></script> <script src="control.js"></script>
第一行引用qwebchannel.js,這個指令碼已經封裝在了Qt的動態連結庫中,因此直接使用qrc的資源引用即可得到,第二行是我們一會兒要開發的控制邏輯。
即便不在QWebEngine下,目前的這個網頁也是能夠正常渲染的,在Chrome裡的效果如下:

在Chrome裡除錯完畢,替換掉剛才那個簡單的index.html,重新執行webui.py,應該得到完全一樣的東西:

接下來寫control.js
$(document).ready(function() { new QWebChannel(qt.webChannelTransport, function(channel) { var my_object = channel.objects.MyObject; $("#btn0").click(function() { my_object.consolePrint($("#txt0").val()); }); $("#btn1").click(function() { my_object.setParentWindowTitle($("#txt1").val()); }); $("#btn2").click(function() { my_object.saveFile($("#txt2").val(),$("#txt3").val()); }); }) })
可以看到介面簡單的不能再簡單。
由於『前端』和『後端』都在本地,因此此處使用預設的webChannelTransport。由於操作是非同步的,因此在回撥函式裡才能得到有效的channel objects。這行程式碼,
var my_object = channel.objects.MyObject;
與Python裡這樣程式碼:
self.__channel.registerObject('MyObject',self.__my_object)
是一一對應的,那麼JavaScript中的my_object,與Python中的my_object就建立起了一致性。接下來只需要使用jQuery,在相應的事件響應函式裡『呼叫』Python裡的函式就可以了。MyObjectCls如同一個橋一樣,把JavaScript和Python連線了起來。
