前言
几天前写了一个备份的小工具,用Python写个脚本,也就花个一天的时间搞定,给客户用了一下,对功能很满意,但对界面不满足,想要一个图形界面来管理;
一个备份脚本,有必要整成这样子吗?没办法,谁让是上帝的要求呢,就研究一下;先后找了python的tkinter、pyqt,尝试着画一些界面,虽然功能可以实现,但界面很难看;恰好,在查看pyqt的API文档的时候 ,发现了QWebView组件,据介绍可以实现一个浏览器,并且浏览器中的JS可以与Python代码进行交互。忽然眼前一亮,我不正是我想要的吗!
抽几天时间把工具写完了,目前运行良好;想写篇博客做个记录,也想给需要进行此类开发的朋友做个示例,就把这个工具最核心的内容做了一个DEMO。现在,就把这个小DEMO的开发过程做个说明;
功能简介
这个小DEMO用于显示计算机上的一些信息,显示内容不是主要的,主要为了体现如何用python+HTML开发桌面应用程序。
1.使用HTML开发一个界面; 2.在页面上显示主机信息; 3.实现功能时,体现JS调用Python和Python调用JS;
最终实现的主界面如下:
准备材料
开发工具:Visual Studio Code
开发语言:python 2.7/python 3.7
界面工具包:PyQT4/PyQT5
目录结构
说明:
demoUI.py : python主体程序 ,主要实现窗口创建,加载index.html页面,python与JS的交互;
views/index.html : 主要进行数据展示,及与python交互的js程序
demoUI.py开发
引入的包
from PyQt4 import QtCore from PyQt4 import QtGui from PyQt4 import QtWebKit from PyQt4 import QtNetwork #处理中文问题 import sys,json from PyQt4.Qt import QObject reload(sys) sys.setdefaultencoding('utf8')
前四行代码,主要是引入Qt相关的包,这里使用的PyQt4;
后三行代码,主要是为了处理中文问题,可以忽略,但遇到中文乱码的时候,想起来这句话,把它加上就行了
DemoWin类
class DemoWin(QtWebKit.QWebView): def __init__(self): QtWebKit.QWebView.__init__(self) self.resize(800, 600) self.setUrl(QtCore.QUrl('views/index.html')) self.show() mainFrame = self.page().mainFrame() winobj = WinObj(mainFrame) mainFrame.javaScriptWindowObjectCleared.connect(lambda: mainFrame.addToJavaScriptWindowObject(QtCore.QString('WinObj'), winobj))
DemoWin是整个应用的核心类,主要实现窗体创建,关联与JS交互的槽函数等;
class DemoWin(QtWebKit.QWebView):
像标题所说,我们将使用WebKit作为页面展示,所以这里的主页面是以QtWebKit.QWebView作为基类
QtWebKit.QWebView.init(self) self.resize(800, 600) self.setUrl(QtCore.QUrl(‘views/index.html’)) self.show()
设置窗口大小为800*600,并且加载index.html进行显示
mainFrame = self.page().mainFrame()
获取网页主框架,按照QT官方文档解释:QWebFrame代表一个网页框架,每一个QWebFrame至少包含一个主框架,使用QWebFrame.mainFrame()获取。
这是进行后续各类操作的基础,类似于JS中只有获取到网页的dom对象,才可以对其中的元素操作一样;
winobj = WinObj(mainFrame) mainFrame.javaScriptWindowObjectCleared.connect(lambda: mainFrame.addToJavaScriptWindowObject(QtCore.QString(‘WinObj’), winobj)) ##js调用python
这段代码是关键中的关键!
WinObj类:是封装后用于js调用的槽函数类,后续再详细介绍
addToJavaScriptWindowObject类:第一个参数是对象在javascript里的名字, 可以自由命名, 第二个参数是对应的QObject实例指针。 这样在javascript里就可以直接访问WinObj对象拉, 是不是看上去超级简单?但是这个函数的调用时机是有讲究的,按照官方文档的推荐,是需要在javaScriptWindowObjectCleared信号的槽里调用,才有了以上的代码;里面用了lambda表达式,纯粹是为了减少一个槽函数的定义,你如果觉得不好看或不喜欢,完全可以定义一个槽函数;
WinObj类
下面来重点介绍WinObj类,类型定义如下:
class WinObj(QtCore.QObject): def __init__(self,mainFrame): super(WinObj,self).__init__() self.mainFrame = mainFrame @QtCore.pyqtSlot(result="QString") def getInfo(self): import socket,platform hostname = socket.gethostname() try: ip = socket.gethostbyname(hostname) except: ip = '' list_info = platform.uname() sys_name = list_info[0] + list_info[2] cpu_name = list_info[5] dic_info = {"hostname":hostname,"ip":ip,"sys_name":sys_name, \ "cpu_name":cpu_name} #调用js函数,实现回调 self.mainFrame.evaluateJavaScript('%s(%s)' % ('onGetInfo',json.dumps(dic_info))) return json.dumps(dic_info)
该类封装了JS可直接调用的方法,有一些区别于普通类的地方要注意;
class WinObj(QtCore.QObject):
该类必须继承自QObject,而不能是object。
def __init__(self,mainFrame): super(WinObj,self).__init__() self.mainFrame = mainFrame
构造函数将mainFrame传进来,主要用于调用js函数(后面将有示例介绍)
@QtCore.pyqtSlot(result="QString") def getInfo(self): import socket,platform hostname = socket.gethostname() ip = socket.gethostbyname(hostname) list_info = platform.uname() sys_name = list_info[0] + list_info[2] cpu_name = list_info[5] dic_info = {"hostname":hostname,"ip":ip,"sys_name":sys_name, \ "cpu_name":cpu_name} #调用js函数,实现回调 self.mainFrame.evaluateJavaScript('%s(%s)' % ('onGetInfo',json.dumps(dic_info))) return json.dumps(dic_info)
getInfo()实现了一个供JS调用的方法,有几下几点要注意:
(1)@QtCore.pyqtSlot(result=”QString”) 用于将python方法转换为供js用的函数,括号里写明数据类型及返回类型;如果没有声明result,则不能返回数据;
(2)self.mainFrame.evaluateJavaScript(‘%s(%s)’ % (‘onGetInfo’,json.dumps(dic_info)))
调用页面中用JS声明的onGetInfo函数(这里仅作一个示例,将查询到的数据进行回调返回)
demoUI.py完整代码下:
#coding=utf-8 ''' Created on 2017年11月3日 @author: Administrator ''' from PyQt4 import QtCore from PyQt4 import QtGui from PyQt4 import QtWebKit from PyQt4 import QtNetwork #处理中文问题 import sys,json from PyQt4.Qt import QObject reload(sys) sys.setdefaultencoding('utf8') class DemoWin(QtWebKit.QWebView): def __init__(self): QtWebKit.QWebView.__init__(self) self.resize(800, 600) self.setUrl(QtCore.QUrl('views/index.html')) self.show() mainFrame = self.page().mainFrame() winobj = WinObj(mainFrame) mainFrame.javaScriptWindowObjectCleared.connect(lambda: mainFrame.addToJavaScriptWindowObject(QtCore.QString('WinObj'), winobj)) ##js调用python class WinObj(QtCore.QObject): def __init__(self,mainFrame): super(WinObj,self).__init__() self.mainFrame = mainFrame @QtCore.pyqtSlot(result="QString") def getInfo(self): import socket,platform hostname = socket.gethostname() try: ip = socket.gethostbyname(hostname) except: ip = '' list_info = platform.uname() sys_name = list_info[0] + list_info[2] cpu_name = list_info[5] dic_info = {"hostname":hostname,"ip":ip,"sys_name":sys_name, \ "cpu_name":cpu_name} #调用js函数,实现回调 self.mainFrame.evaluateJavaScript('%s(%s)' % ('onGetInfo',json.dumps(dic_info))) return json.dumps(dic_info) if __name__ == '__main__': app = QtGui.QApplication(sys.argv) demoWin = DemoWin() sys.exit(app.exec_())
index.html
<!DOCTYPE script PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <script type="text/javascript" src='http://www.mobibrw.com/wp-content/uploads/2020/05/vue.min_.1.0.11.js'></script> </head> <body> <div id='app'> <input type='button' onclick='getInfo()' value="获取机器信息"> <H3 >主机名:{{config.hostname}} </H3> <H3 >IP:{{config.ip}} </H3> <H3 >操作系统:{{config.sys_name}} </H3> <H3 >CPU:{{config.cpu_name}} </H3> <H3>以下为python回调信息:</H3> <label id='info'></label> </div> </body> </html> <script type="text/javascript"> var vm = new Vue({ el: '#app', data:{ config:{} }, created:function(){ this.config = JSON.parse(window.WinObj.getInfo()) } }); function getInfo() { info = JSON.parse(window.WinObj.getInfo()) alert(info) } //python回调的js函数 function onGetInfo(info) { document.getElementById('info').innerText=JSON.stringify(info) } </script>
Python 3.7/PyQT5下需要安装 pyQt5
$ python3 -m pip install --upgrade pip $ pip3 install SIP $ pip3 install pyQt5 $ pip3 install --upgrade PyQt5 $ pip3 install PyQtWebEngine
Python 3.7/PyQT5下的代码(注意,Python 2.7/PyQT4是的交互是同步调用的,但是Python 3.7/PyQT5通过QtWebChannel交互是异步的)如下:
#coding=utf-8 from PyQt5 import QtCore from PyQt5 import QtGui from PyQt5 import QtWidgets from PyQt5 import QtWebEngineWidgets from PyQt5 import QtNetwork from PyQt5 import QtWebChannel #处理中文问题 import sys,json from PyQt5.Qt import QObject #reload(sys) #sys.setdefaultencoding('utf8') # 调试窗口配置 # 如果不想自己创建调试窗口,可以使用Chrome连接这个地址进行调试 import os DEBUG_PORT = '5588' DEBUG_URL = 'http://127.0.0.1:%s' % DEBUG_PORT os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = DEBUG_PORT def qt_message_handler(mode, context, message): if mode == QtCore.QtInfoMsg: mode = 'INFO' elif mode == QtCore.QtWarningMsg: mode = 'WARNING' elif mode == QtCore.QtCriticalMsg: mode = 'CRITICAL' elif mode == QtCore.QtFatalMsg: mode = 'FATAL' else: mode = 'DEBUG' print('qt_message_handler: line: %d, func: %s(), file: %s' % ( context.line, context.function, context.file)) print(' %s: %s\n' % (mode, message)) QtCore.qInstallMessageHandler(qt_message_handler) class DemoWin(QtWebEngineWidgets.QWebEngineView): def __init__(self): QtWebEngineWidgets.QWebEngineView.__init__(self) self.setWindowTitle('Web页面中的JavaScript与 QWebEngineView交互例子') self.resize(800, 600) self.settings().setAttribute(QtWebEngineWidgets.QWebEngineSettings.PluginsEnabled, True) self.settings().setAttribute(QtWebEngineWidgets.QWebEngineSettings.JavascriptEnabled, True) url = QtCore.QUrl('file:///' + QtCore.QFileInfo('views/index.html').absoluteFilePath()) #url = QtCore.QUrl("https://www.baidu.com") self.setUrl(url) self.show() self.winobj = WinObj(self.page()) self.channel = QtWebChannel.QWebChannel() self.channel.registerObject("con", self.winobj) self.page().setWebChannel(self.channel) #网页调试窗口 self.inspector = QtWebEngineWidgets.QWebEngineView() self.inspector.setWindowTitle('Web Inspector') self.inspector.load(QtCore.QUrl(DEBUG_URL)) self.loadFinished.connect(self.handleHtmlLoaded) def handleHtmlLoaded(self, ok): if ok: self.page().setDevToolsPage(self.inspector.page()) self.inspector.show() class WinObj(QtCore.QObject): def __init__(self, page): super(WinObj,self).__init__() self.page = page @QtCore.pyqtSlot(result="QString") def getInfo(self): import socket,platform hostname = socket.gethostname() try: ip = socket.gethostbyname(hostname) except: ip = '' list_info = platform.uname() sys_name = list_info[0] + list_info[2] cpu_name = list_info[5] dic_info = {"hostname":hostname,"ip":ip,"sys_name":sys_name, \ "cpu_name":cpu_name} #调用js函数,实现回调 self.page.runJavaScript('%s(%s)' % ('onGetInfo',json.dumps(dic_info))) js = json.dumps(dic_info) return js if __name__ == '__main__': app = QtWidgets.QApplication(sys.argv) demoWin = DemoWin() sys.exit(app.exec())
index.html
<!DOCTYPE script PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <script type="text/javascript" src="qwebchannel.js"></script> <script type="text/javascript" src='http://www.mobibrw.com/wp-content/uploads/2020/05/vue.min_.1.0.11.js'></script> </head> <body> <div id='app'> <input type='button' onclick='getInfo()' value="获取机器信息"> <H3 >主机名:{{config.hostname}} </H3> <H3 >IP:{{config.ip}} </H3> <H3 >操作系统:{{config.sys_name}} </H3> <H3 >CPU:{{config.cpu_name}} </H3> <H3>以下为python回调信息:</H3> <label id='info'></label> </div> </body> </html> <script type="text/javascript"> new QWebChannel( qt.webChannelTransport, function(channel) { window.con = channel.objects.con; window.vm = new Vue({ el: '#app', data: { config:{} }, created:function() { window.con.getInfo(function(info) { window.vm.config = JSON.parse(info); }); } }); }); function getInfo() { window.con.getInfo(); } //python回调的js函数 function onGetInfo(info) { document.getElementById('info').innerText=JSON.stringify(info) } </script>
/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com> ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtWebChannel module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:BSD$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** BSD License Usage ** Alternatively, you may use this file under the terms of the BSD license ** as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of The Qt Company Ltd nor the names of its ** contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ "use strict"; var QWebChannelMessageTypes = { signal: 1, propertyUpdate: 2, init: 3, idle: 4, debug: 5, invokeMethod: 6, connectToSignal: 7, disconnectFromSignal: 8, setProperty: 9, response: 10, }; var QWebChannel = function(transport, initCallback) { if (typeof transport !== "object" || typeof transport.send !== "function") { console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." + " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send)); return; } var channel = this; this.transport = transport; this.send = function(data) { if (typeof(data) !== "string") { data = JSON.stringify(data); } channel.transport.send(data); } this.transport.onmessage = function(message) { var data = message.data; if (typeof data === "string") { data = JSON.parse(data); } switch (data.type) { case QWebChannelMessageTypes.signal: channel.handleSignal(data); break; case QWebChannelMessageTypes.response: channel.handleResponse(data); break; case QWebChannelMessageTypes.propertyUpdate: channel.handlePropertyUpdate(data); break; default: console.error("invalid message received:", message.data); break; } } this.execCallbacks = {}; this.execId = 0; this.exec = function(data, callback) { if (!callback) { // if no callback is given, send directly channel.send(data); return; } if (channel.execId === Number.MAX_VALUE) { // wrap channel.execId = Number.MIN_VALUE; } if (data.hasOwnProperty("id")) { console.error("Cannot exec message with property id: " + JSON.stringify(data)); return; } data.id = channel.execId++; channel.execCallbacks[data.id] = callback; channel.send(data); }; this.objects = {}; this.handleSignal = function(message) { var object = channel.objects[message.object]; if (object) { object.signalEmitted(message.signal, message.args); } else { console.warn("Unhandled signal: " + message.object + "::" + message.signal); } } this.handleResponse = function(message) { if (!message.hasOwnProperty("id")) { console.error("Invalid response message received: ", JSON.stringify(message)); return; } channel.execCallbacks[message.id](message.data); delete channel.execCallbacks[message.id]; } this.handlePropertyUpdate = function(message) { for (var i in message.data) { var data = message.data[i]; var object = channel.objects[data.object]; if (object) { object.propertyUpdate(data.signals, data.properties); } else { console.warn("Unhandled property update: " + data.object + "::" + data.signal); } } channel.exec({type: QWebChannelMessageTypes.idle}); } this.debug = function(message) { channel.send({type: QWebChannelMessageTypes.debug, data: message}); }; channel.exec({type: QWebChannelMessageTypes.init}, function(data) { for (var objectName in data) { var object = new QObject(objectName, data[objectName], channel); } // now unwrap properties, which might reference other registered objects for (var objectName in channel.objects) { channel.objects[objectName].unwrapProperties(); } if (initCallback) { initCallback(channel); } channel.exec({type: QWebChannelMessageTypes.idle}); }); }; function QObject(name, data, webChannel) { this.__id__ = name; webChannel.objects[name] = this; // List of callbacks that get invoked upon signal emission this.__objectSignals__ = {}; // Cache of all properties, updated when a notify signal is emitted this.__propertyCache__ = {}; var object = this; // ---------------------------------------------------------------------- this.unwrapQObject = function(response) { if (response instanceof Array) { // support list of objects var ret = new Array(response.length); for (var i = 0; i < response.length; ++i) { ret[i] = object.unwrapQObject(response[i]); } return ret; } if (!response || !response["__QObject*__"] || response.id === undefined) { return response; } var objectId = response.id; if (webChannel.objects[objectId]) return webChannel.objects[objectId]; if (!response.data) { console.error("Cannot unwrap unknown QObject " + objectId + " without data."); return; } var qObject = new QObject( objectId, response.data, webChannel ); qObject.destroyed.connect(function() { if (webChannel.objects[objectId] === qObject) { delete webChannel.objects[objectId]; // reset the now deleted QObject to an empty {} object // just assigning {} though would not have the desired effect, but the // below also ensures all external references will see the empty map // NOTE: this detour is necessary to workaround QTBUG-40021 var propertyNames = []; for (var propertyName in qObject) { propertyNames.push(propertyName); } for (var idx in propertyNames) { delete qObject[propertyNames[idx]]; } } }); // here we are already initialized, and thus must directly unwrap the properties qObject.unwrapProperties(); return qObject; } this.unwrapProperties = function() { for (var propertyIdx in object.__propertyCache__) { object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]); } } function addSignal(signalData, isPropertyNotifySignal) { var signalName = signalData[0]; var signalIndex = signalData[1]; object[signalName] = { connect: function(callback) { if (typeof(callback) !== "function") { console.error("Bad callback given to connect to signal " + signalName); return; } object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; object.__objectSignals__[signalIndex].push(callback); if (!isPropertyNotifySignal && signalName !== "destroyed") { // only required for "pure" signals, handled separately for properties in propertyUpdate // also note that we always get notified about the destroyed signal webChannel.exec({ type: QWebChannelMessageTypes.connectToSignal, object: object.__id__, signal: signalIndex }); } }, disconnect: function(callback) { if (typeof(callback) !== "function") { console.error("Bad callback given to disconnect from signal " + signalName); return; } object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; var idx = object.__objectSignals__[signalIndex].indexOf(callback); if (idx === -1) { console.error("Cannot find connection of signal " + signalName + " to " + callback.name); return; } object.__objectSignals__[signalIndex].splice(idx, 1); if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { // only required for "pure" signals, handled separately for properties in propertyUpdate webChannel.exec({ type: QWebChannelMessageTypes.disconnectFromSignal, object: object.__id__, signal: signalIndex }); } } }; } /** * Invokes all callbacks for the given signalname. Also works for property notify callbacks. */ function invokeSignalCallbacks(signalName, signalArgs) { var connections = object.__objectSignals__[signalName]; if (connections) { connections.forEach(function(callback) { callback.apply(callback, signalArgs); }); } } this.propertyUpdate = function(signals, propertyMap) { // update property cache for (var propertyIndex in propertyMap) { var propertyValue = propertyMap[propertyIndex]; object.__propertyCache__[propertyIndex] = propertyValue; } for (var signalName in signals) { // Invoke all callbacks, as signalEmitted() does not. This ensures the // property cache is updated before the callbacks are invoked. invokeSignalCallbacks(signalName, signals[signalName]); } } this.signalEmitted = function(signalName, signalArgs) { invokeSignalCallbacks(signalName, signalArgs); } function addMethod(methodData) { var methodName = methodData[0]; var methodIdx = methodData[1]; object[methodName] = function() { var args = []; var callback; for (var i = 0; i < arguments.length; ++i) { if (typeof arguments[i] === "function") callback = arguments[i]; else args.push(arguments[i]); } webChannel.exec({ "type": QWebChannelMessageTypes.invokeMethod, "object": object.__id__, "method": methodIdx, "args": args }, function(response) { if (response !== undefined) { var result = object.unwrapQObject(response); if (callback) { (callback)(result); } } }); }; } function bindGetterSetter(propertyInfo) { var propertyIndex = propertyInfo[0]; var propertyName = propertyInfo[1]; var notifySignalData = propertyInfo[2]; // initialize property cache with current value // NOTE: if this is an object, it is not directly unwrapped as it might // reference other QObject that we do not know yet object.__propertyCache__[propertyIndex] = propertyInfo[3]; if (notifySignalData) { if (notifySignalData[0] === 1) { // signal name is optimized away, reconstruct the actual name notifySignalData[0] = propertyName + "Changed"; } addSignal(notifySignalData, true); } Object.defineProperty(object, propertyName, { configurable: true, get: function () { var propertyValue = object.__propertyCache__[propertyIndex]; if (propertyValue === undefined) { // This shouldn't happen console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); } return propertyValue; }, set: function(value) { if (value === undefined) { console.warn("Property setter for " + propertyName + " called with undefined value!"); return; } object.__propertyCache__[propertyIndex] = value; webChannel.exec({ "type": QWebChannelMessageTypes.setProperty, "object": object.__id__, "property": propertyIndex, "value": value }); } }); } // ---------------------------------------------------------------------- data.methods.forEach(addMethod); data.properties.forEach(bindGetterSetter); data.signals.forEach(function(signal) { addSignal(signal, false); }); for (var name in data.enums) { object[name] = data.enums[name]; } } //required for use with nodejs if (typeof module === 'object') { module.exports = { QWebChannel: QWebChannel }; }
参考链接
- Python+WebKit+HTML开发桌面应用程序
- pyqt5加载网页的简单使用
- Porting from Qt WebKit to Qt WebEngine
- PyQt5 QWebChannel实现python与Javascript双向通信
- Qt的QWebChannel和JS、HTML通信/交互驱动百度地图
- qwebchannel.js Example File
- PyQt5高级界面控件之QWebEngineView(十三)
- QWebEngineView加载本地html三种方法
- Redirect qDebug output to file with PyQt5
- 基于 QWebChannel 的前端通信方案
- qwebengineview与js相互调用(js调用c++部分)
- PyQt - JavaScript 交互
- Qt WebChannel Examples
- What is the alternative to QWebInspector in Qt WebEngine?