CallEachWithJs

This commit is contained in:
Irony 2021-12-16 02:48:35 +08:00
parent 48ef9a5b88
commit 4aa9ad36fc
9 changed files with 794 additions and 0 deletions

View file

@ -0,0 +1,61 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Created on 2021/12/15
@author: Irony
@site: https://pyqt.site , https://github.com/PyQt5
@email: 892768447@qq.com
@file: CallEachWithJs.py
@description: 与JS之间的互相调用
"""
import os
from PyQt5.QtCore import QUrl, pyqtSlot
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import (QApplication, QLineEdit, QPushButton, QVBoxLayout,
QWidget)
from Lib.WebChannelObject import WebChannelObject
class Window(QWidget):
def __init__(self, *args, **kwargs):
super(Window, self).__init__(*args, **kwargs)
self.m_obj = WebChannelObject(self)
# 注册该窗口,可以访问该窗口的属性,槽函数,信号
# https://doc.qt.io/qt-5/qwidget.html#properties
# https://doc.qt.io/qt-5/qwidget.html#signals
# https://doc.qt.io/qt-5/qwidget.html#public-slots
self.m_obj.registerObject('qtwindow', self)
self.m_obj.start()
layout = QVBoxLayout(self)
self.editTitle = QLineEdit(self, placeholderText='输入标题')
layout.addWidget(self.editTitle)
layout.addWidget(QPushButton('修改标题', self, clicked=self.onChangeTitle))
QDesktopServices.openUrl(
QUrl.fromLocalFile(
os.path.join(os.path.dirname(sys.argv[0] or __file__),
'Data/CallEachWithJs.html')))
def onChangeTitle(self):
self.setWindowTitle(self.editTitle.text())
# ------- 把非槽函数通过pyqtSlot重新暴露 -------
@pyqtSlot(int, int)
def resize(self, width, height):
super().resize(width, height)
if __name__ == '__main__':
import cgitb
import sys
cgitb.enable(format='text')
app = QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())

View file

@ -0,0 +1,76 @@
<!DOCTYPE html>
<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">
function appendText(message) {
// 添加输出到页面
var textArea = document.getElementById("textArea");
output.innerHTML = output.innerHTML + message + "\n";
}
window.onload = function () {
// 创建websocket
var socket = new WebSocket("ws://localhost:12345");
socket.onclose = function () {
appendText("WebSocket连接关闭");
};
socket.onerror = function () {
appendText("WebSocket连接发生错误");
};
socket.onopen = function () {
appendText("WebSocket连接成功");
// 创建webchannel
window.channel = new QWebChannel(socket, function (channel) {
// 重点:把注册的对象全部设为全局,注册了多少个就设置多少个
window.WebChannelObject = channel.objects.WebChannelObject;
window.qtwindow = channel.objects.qtwindow;
// 绑定窗口标题变化信号
window.qtwindow.windowTitleChanged.connect(function (title) {
appendText('标题变化:' + title);
});
});
};
}
</script>
</head>
<body>
<span>输出:</span><br /><textarea id="output" style="width:400px;height:200px;"></textarea><br /><br />
<input type="button" value="设置属性int= 100" onclick="javascript: window.WebChannelObject.intValue = 100;" />
<input type="button" value="获取属性int"
onclick="javascript: appendText('intValue:' + window.WebChannelObject.intValue);" /><br /><br />
<input type="button" value="设置属性float= 99.9" onclick="javascript: window.WebChannelObject.floatValue = 99.9;" />
<input type="button" value="获取属性float"
onclick="javascript: appendText('floatValue:' + window.WebChannelObject.floatValue);" /><br /><br />
<input type="button" value="设置属性str= Irony" onclick="javascript: window.WebChannelObject.strValue = 'Irony';" />
<input type="button" value="获取属性str"
onclick="javascript: appendText('strValue:' + window.WebChannelObject.strValue);" /><br /><br />
<input type="button" value="设置属性bool= true" onclick="javascript: window.WebChannelObject.boolValue = true;" />
<input type="button" value="获取属性bool"
onclick="javascript: appendText('boolValue:' + window.WebChannelObject.boolValue);" /><br /><br />
<!-- <input type="button" value="设置属性list= [1, '2', false]"
onclick="javascript: window.WebChannelObject.listValue = [1, '2', false];" />
<input type="button" value="获取属性list"
onclick="javascript: appendText('listValue:' + window.WebChannelObject.listValue);" /><br /><br />
<input type="button" value="设置属性map= {'key1': 1, 'key2': '2', 'key3': false}"
onclick="javascript: window.WebChannelObject.mapValue = {'key1': 1, 'key2': '2', 'key3': false};" />
<input type="button" value="获取属性map"
onclick="javascript: appendText('mapValue:' + window.WebChannelObject.mapValue);" /><br /><br /> -->
<input type="button" value="调用加法testAdd(1, 2)"
onclick="javascript: window.WebChannelObject.testAdd(1, 2, function(result) { appendText('testAdd ret:' + result); });" /><br /><br />
<input type="button" value="设置窗口大小resize(400, 400)" onclick="javascript: window.qtwindow.resize(400, 400);" />
</body>
</html>

View file

@ -0,0 +1,448 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Copyright (C) 2016 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:LGPL$
** 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.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $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)
{
message.data.forEach(data => {
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 (const objectName of Object.keys(data)) {
new QObject(objectName, data[objectName], channel);
}
// now unwrap properties, which might reference other registered objects
for (const objectName of Object.keys(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
return response.map(qobj => object.unwrapQObject(qobj))
}
if (!(response instanceof Object))
return response;
if (!response["__QObject*__"] || response.id === undefined) {
var jObj = {};
for (const propName of Object.keys(response)) {
jObj[propName] = object.unwrapQObject(response[propName]);
}
return jObj;
}
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
Object.keys(qObject).forEach(name => delete qObject[name]);
}
});
// here we are already initialized, and thus must directly unwrap the properties
qObject.unwrapProperties();
return qObject;
}
this.unwrapProperties = function()
{
for (const propertyIdx of Object.keys(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);
// only required for "pure" signals, handled separately for properties in propertyUpdate
if (isPropertyNotifySignal)
return;
// also note that we always get notified about the destroyed signal
if (signalName === "destroyed" || signalName === "destroyed()" || signalName === "destroyed(QObject*)")
return;
// and otherwise we only need to be connected only once
if (object.__objectSignals__[signalIndex].length == 1) {
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 (const propertyIndex of Object.keys(propertyMap)) {
var propertyValue = propertyMap[propertyIndex];
object.__propertyCache__[propertyIndex] = this.unwrapQObject(propertyValue);
}
for (const signalName of Object.keys(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, this.unwrapQObject(signalArgs));
}
function addMethod(methodData)
{
var methodName = methodData[0];
var methodIdx = methodData[1];
// Fully specified methods are invoked by id, others by name for host-side overload resolution
var invokedMethod = methodName[methodName.length - 1] === ')' ? methodIdx : methodName
object[methodName] = function() {
var args = [];
var callback;
var errCallback;
for (var i = 0; i < arguments.length; ++i) {
var argument = arguments[i];
if (typeof argument === "function")
callback = argument;
else if (argument instanceof QObject && webChannel.objects[argument.__id__] !== undefined)
args.push({
"id": argument.__id__
});
else
args.push(argument);
}
var result;
// during test, webChannel.exec synchronously calls the callback
// therefore, the promise must be constucted before calling
// webChannel.exec to ensure the callback is set up
if (!callback && (typeof(Promise) === 'function')) {
result = new Promise(function(resolve, reject) {
callback = resolve;
errCallback = reject;
});
}
webChannel.exec({
"type": QWebChannelMessageTypes.invokeMethod,
"object": object.__id__,
"method": invokedMethod,
"args": args
}, function(response) {
if (response !== undefined) {
var result = object.unwrapQObject(response);
if (callback) {
(callback)(result);
}
} else if (errCallback) {
(errCallback)();
}
});
return 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;
var valueToSend = value;
if (valueToSend instanceof QObject && webChannel.objects[valueToSend.__id__] !== undefined)
valueToSend = { "id": valueToSend.__id__ };
webChannel.exec({
"type": QWebChannelMessageTypes.setProperty,
"object": object.__id__,
"property": propertyIndex,
"value": valueToSend
});
}
});
}
// ----------------------------------------------------------------------
data.methods.forEach(addMethod);
data.properties.forEach(bindGetterSetter);
data.signals.forEach(function(signal) { addSignal(signal, false); });
Object.assign(object, data.enums);
}
//required for use with nodejs
if (typeof module === 'object') {
module.exports = {
QWebChannel: QWebChannel
};
}

View file

@ -0,0 +1,195 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Created on 2021/12/15
@author: Irony
@site: https://pyqt.site , https://github.com/PyQt5
@email: 892768447@qq.com
@file: WebChannelObject.py
@description: 交互对象需要继承QObject并暴露接口
"""
from PyQt5.QtCore import (QJsonDocument, QJsonParseError, QObject,
pyqtProperty, pyqtSlot)
from PyQt5.QtNetwork import QHostAddress
from PyQt5.QtWebChannel import QWebChannel, QWebChannelAbstractTransport
from PyQt5.QtWebSockets import QWebSocketServer
class WebSocketTransport(QWebChannelAbstractTransport):
def __init__(self, socket, *args, **kwargs):
super(WebSocketTransport, self).__init__(*args, **kwargs)
self.m_socket = socket
self.m_socket.textMessageReceived.connect(self.textMessageReceived)
self.m_socket.disconnected.connect(self.deleteLater)
def sendMessage(self, message):
print('sendMessage:', message)
self.m_socket.sendTextMessage(
QJsonDocument(message).toJson(QJsonDocument.Compact).data().decode(
'utf-8', errors='ignore'))
def textMessageReceived(self, message):
print('textMessageReceived:', message)
error = QJsonParseError()
json = QJsonDocument.fromJson(message.encode('utf-8', errors='ignore'),
error)
if error.error:
print('Failed to parse message:{}, Error is:{}'.format(
message, error.errorString()))
return
if not json.isObject():
print('Received JSON message that is not an object:{}'.format(
message))
return
self.messageReceived.emit(json.object(), self)
class WebChannelObject(QObject):
def __init__(self, *args, **kwargs):
super(WebChannelObject, self).__init__(*args, **kwargs)
# 内部属性供外部调用
self._intValue = 0
self._floatValue = 0.0
self._boolValue = False
self._strValue = ''
# 设置数组或者字典有一定问题
# self._listValue = []
# self._mapValue = {}
# webchannel对象
self.m_webchannel = QWebChannel(self)
# 这里默认注册自己,这里使用了类名作为名称
self.registerObject(self.__class__.__name__, self)
# websocket服务
self.m_clients = {}
self.m_server = QWebSocketServer(self.__class__.__name__,
QWebSocketServer.NonSecureMode, self)
def registerObject(self, name, obj):
"""注册对象
@param name: 名称
@type name: str
@param obj: 对象
@type obj: QObject
"""
self.m_webchannel.registerObject(name, obj)
def registerObjects(self, objects):
"""注册多个对象
@param objects: 对象列表
@type objects: list
"""
for name, obj in objects:
self.registerObject(name, obj)
def deregisterObject(self, obj):
"""注销对象
@param obj: 对象
@type obj: QObject
"""
self.m_webchannel.deregisterObject(obj)
def deregisterObjects(self, objects):
"""注销多个对象
@param objects: 对象列表
@type objects: list
"""
for obj in objects:
self.deregisterObject(obj)
def start(self, port=12345):
"""启动服务
@param port: 端口
@type port: int
"""
if not self.m_server.listen(QHostAddress.Any, port):
raise Exception(
'Failed to create WebSocket server on port {}'.format(port))
print('WebSocket server listening on port {}'.format(port))
# 新连接信号
self.m_server.newConnection.connect(self._handleNewConnection)
def stop(self):
"""停止服务"""
self.m_server.close()
def _handleNewConnection(self):
"""新连接"""
socket = self.m_server.nextPendingConnection()
print('New WebSocket connection from {}'.format(
socket.peerAddress().toString()))
# 连接关闭信号
socket.disconnected.connect(self._handleDisconnected)
transport = WebSocketTransport(socket)
self.m_clients[socket] = transport
self.m_webchannel.connectTo(transport)
def _handleDisconnected(self):
"""连接关闭"""
socket = self.sender()
print('WebSocket connection from {} closed'.format(
socket.peerAddress()))
if socket in self.m_clients:
self.m_clients.pop(socket)
socket.deleteLater()
# ------- 下面是注册属性的方法 -------
@pyqtProperty(int)
def intValue(self):
return self._intValue
@intValue.setter
def intValue(self, value):
self._intValue = value
@pyqtProperty(float)
def floatValue(self):
return self._floatValue
@floatValue.setter
def floatValue(self, value):
self._floatValue = value
@pyqtProperty(bool)
def boolValue(self):
return self._boolValue
@boolValue.setter
def boolValue(self, value):
self._boolValue = value
@pyqtProperty(str)
def strValue(self):
return self._strValue
@strValue.setter
def strValue(self, value):
self._strValue = value
# @pyqtProperty(list)
# def listValue(self):
# return self._listValue
# @listValue.setter
# def listValue(self, value):
# self._listValue = value
# @pyqtProperty(dict)
# def mapValue(self):
# return self._mapValue
# @mapValue.setter
# def mapValue(self, value):
# self._mapValue = value
# ------- 下面是注册函数的方法 -------
# ------- 如果有返回值一定要注明 result=返回类型 -------
@pyqtSlot(int, int, result=int)
def testAdd(self, a, b):
return a + b

View file

0
QWebChannel/README.en.md Normal file
View file

12
QWebChannel/README.md Normal file
View file

@ -0,0 +1,12 @@
# QWebChannel
- 目录
- [和Js互相调用](#1和Js互相调用)
## 1、和Js互相调用
[运行 CallEachWithJs.py](CallEachWithJs.py)
通过`qwebchannel.js`和`QWebChannel.registerObject`通过中间件`WebSocket`进行对象和Javascript的交互类似于json rpc
该方法类似与`QWebEngineView`中的例子同时该demo也适用与nodejs。
![CallEachWithJs](ScreenShot/CallEachWithJs.gif)

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 KiB

View file

@ -146,6 +146,8 @@ https://pyqt.site 论坛是专门针对PyQt5学习和提升开设的网站
- [拦截请求内容](QWebEngineView/BlockRequestData.py)
- [浏览器下载文件](Test/partner_625781186/6.QWebEngineView下载文件)
- [打印网页](Test/partner_625781186/17_打印预览qwebengineview)
- [QWebChannel](QWebChannel)
- [和Js互相调用](QWebChannel/CallEachWithJs.py)
- [QThread](QThread)
- [继承QThread](QThread/InheritQThread.py)