diff options
Diffstat (limited to 'examples/webchannel')
-rw-r--r-- | examples/webchannel/standalone/core.py | 26 | ||||
-rw-r--r-- | examples/webchannel/standalone/dialog.py | 33 | ||||
-rw-r--r-- | examples/webchannel/standalone/dialog.ui | 48 | ||||
-rw-r--r-- | examples/webchannel/standalone/doc/standalone.png | bin | 0 -> 47586 bytes | |||
-rw-r--r-- | examples/webchannel/standalone/doc/standalone.rst | 8 | ||||
-rw-r--r-- | examples/webchannel/standalone/index.html | 79 | ||||
-rw-r--r-- | examples/webchannel/standalone/main.py | 63 | ||||
-rw-r--r-- | examples/webchannel/standalone/standalone.pyproject | 4 | ||||
-rw-r--r-- | examples/webchannel/standalone/ui_dialog.py | 57 | ||||
-rw-r--r-- | examples/webchannel/standalone/websocketclientwrapper.py | 36 | ||||
-rw-r--r-- | examples/webchannel/standalone/websockettransport.py | 52 |
11 files changed, 406 insertions, 0 deletions
diff --git a/examples/webchannel/standalone/core.py b/examples/webchannel/standalone/core.py new file mode 100644 index 000000000..987a4ee1a --- /dev/null +++ b/examples/webchannel/standalone/core.py @@ -0,0 +1,26 @@ +# Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com> +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +from __future__ import annotations + + +from PySide6.QtCore import QObject, Signal, Slot + + +class Core(QObject): + """An instance of this class gets published over the WebChannel and is then + accessible to HTML clients.""" + sendText = Signal(str) + + def __init__(self, dialog, parent=None): + super().__init__(parent) + self._dialog = dialog + self._dialog.send_text.connect(self._emit_send_text) + + @Slot(str) + def _emit_send_text(self, text): + self.sendText.emit(text) + + @Slot(str) + def receiveText(self, text): + self._dialog.display_message(f"Received message: {text}") diff --git a/examples/webchannel/standalone/dialog.py b/examples/webchannel/standalone/dialog.py new file mode 100644 index 000000000..27cef86a7 --- /dev/null +++ b/examples/webchannel/standalone/dialog.py @@ -0,0 +1,33 @@ +# Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com> +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +from __future__ import annotations + + +from PySide6.QtCore import Signal, Slot +from PySide6.QtWidgets import QDialog +from ui_dialog import Ui_Dialog + + +class Dialog(QDialog): + send_text = Signal(str) + + def __init__(self, parent=None): + super().__init__(parent) + self._ui = Ui_Dialog() + self._ui.setupUi(self) + self._ui.send.clicked.connect(self.clicked) + self._ui.input.returnPressed.connect(self._ui.send.animateClick) + + @Slot(str) + def display_message(self, message): + self._ui.output.appendPlainText(message) + + @Slot() + def clicked(self): + text = self._ui.input.text() + if not text: + return + self.send_text.emit(text) + self.display_message(f"Sent message: {text}") + self._ui.input.clear() diff --git a/examples/webchannel/standalone/dialog.ui b/examples/webchannel/standalone/dialog.ui new file mode 100644 index 000000000..056a3f587 --- /dev/null +++ b/examples/webchannel/standalone/dialog.ui @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>Dialog</class> + <widget class="QDialog" name="Dialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Dialog</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="0"> + <widget class="QLineEdit" name="input"> + <property name="placeholderText"> + <string>Message Contents</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="send"> + <property name="text"> + <string>Send</string> + </property> + </widget> + </item> + <item row="0" column="0" colspan="2"> + <widget class="QPlainTextEdit" name="output"> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="plainText"> + <string notr="true">Initializing WebChannel...</string> + </property> + <property name="backgroundVisible"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/examples/webchannel/standalone/doc/standalone.png b/examples/webchannel/standalone/doc/standalone.png Binary files differnew file mode 100644 index 000000000..972b0fbbf --- /dev/null +++ b/examples/webchannel/standalone/doc/standalone.png diff --git a/examples/webchannel/standalone/doc/standalone.rst b/examples/webchannel/standalone/doc/standalone.rst new file mode 100644 index 000000000..f22f32c23 --- /dev/null +++ b/examples/webchannel/standalone/doc/standalone.rst @@ -0,0 +1,8 @@ +WebChannel Standalone Example +============================= + +A simple chat between a server and a remote client running in a browser. + +.. image:: standalone.png + :width: 400 + :alt: WebChannel Standalone Screenshot diff --git a/examples/webchannel/standalone/index.html b/examples/webchannel/standalone/index.html new file mode 100644 index 000000000..7c042cd0c --- /dev/null +++ b/examples/webchannel/standalone/index.html @@ -0,0 +1,79 @@ +<!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"> + //BEGIN SETUP + function output(message) { + var output = document.getElementById("output"); + output.innerHTML = output.innerHTML + message + "\n"; + } + window.onload = function() { + if (location.search != "") + var baseUrl = (/[?&]webChannelBaseUrl=([A-Za-z0-9\-:/\.]+)/.exec(location.search)[1]); + else + var baseUrl = "ws://localhost:12345"; + + output("Connecting to WebSocket server at " + baseUrl + "."); + var socket = new WebSocket(baseUrl); + + socket.onclose = function() { + console.error("web channel closed"); + }; + socket.onerror = function(error) { + console.error("web channel error: " + error); + }; + socket.onopen = function() { + output("WebSocket connected, setting up QWebChannel."); + new QWebChannel(socket, function(channel) { + // make core object accessible globally + window.core = channel.objects.core; + + document.getElementById("send").onclick = function() { + var input = document.getElementById("input"); + var text = input.value; + if (!text) { + return; + } + + output("Sent message: " + text); + input.value = ""; + core.receiveText(text); + } + + core.sendText.connect(function(message) { + output("Received message: " + message); + }); + + core.receiveText("Client connected, ready to send/receive messages!"); + output("Connected to WebChannel, ready to send/receive messages!"); + }); + } + } + //END SETUP + </script> + <style type="text/css"> + html { + height: 100%; + width: 100%; + } + #input { + width: 400px; + margin: 0 10px 0 0; + } + #send { + width: 90px; + margin: 0; + } + #output { + width: 500px; + height: 300px; + } + </style> + </head> + <body> + <textarea id="output"></textarea><br /> + <input id="input" /><input type="submit" id="send" value="Send" onclick="javascript:click();" /> + </body> +</html> diff --git a/examples/webchannel/standalone/main.py b/examples/webchannel/standalone/main.py new file mode 100644 index 000000000..8459b1a92 --- /dev/null +++ b/examples/webchannel/standalone/main.py @@ -0,0 +1,63 @@ +# Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com> +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +from __future__ import annotations + + +import os +import sys + +from PySide6.QtWidgets import QApplication +from PySide6.QtGui import QDesktopServices +from PySide6.QtNetwork import QHostAddress, QSslSocket +from PySide6.QtCore import (QFile, QFileInfo, QUrl) +from PySide6.QtWebChannel import QWebChannel +from PySide6.QtWebSockets import QWebSocketServer + +from dialog import Dialog +from core import Core +from websocketclientwrapper import WebSocketClientWrapper + + +if __name__ == '__main__': + app = QApplication(sys.argv) + if not QSslSocket.supportsSsl(): + print('The example requires SSL support.') + sys.exit(-1) + cur_dir = os.path.dirname(os.path.abspath(__file__)) + js_file_info = QFileInfo(f"{cur_dir}/qwebchannel.js") + if not js_file_info.exists(): + QFile.copy(":/qtwebchannel/qwebchannel.js", + js_file_info.absoluteFilePath()) + + # setup the QWebSocketServer + server = QWebSocketServer("QWebChannel Standalone Example Server", + QWebSocketServer.NonSecureMode) + if not server.listen(QHostAddress.LocalHost, 12345): + print("Failed to open web socket server.") + sys.exit(-1) + + # wrap WebSocket clients in QWebChannelAbstractTransport objects + client_wrapper = WebSocketClientWrapper(server) + + # setup the channel + channel = QWebChannel() + client_wrapper.client_connected.connect(channel.connectTo) + + # setup the UI + dialog = Dialog() + + # setup the core and publish it to the QWebChannel + core = Core(dialog) + channel.registerObject("core", core) + + # open a browser window with the client HTML page + url = QUrl.fromLocalFile(f"{cur_dir}/index.html") + QDesktopServices.openUrl(url) + + display_url = url.toDisplayString() + message = f"Initialization complete, opening browser at {display_url}." + dialog.display_message(message) + dialog.show() + + sys.exit(app.exec()) diff --git a/examples/webchannel/standalone/standalone.pyproject b/examples/webchannel/standalone/standalone.pyproject new file mode 100644 index 000000000..b4fcdfa8e --- /dev/null +++ b/examples/webchannel/standalone/standalone.pyproject @@ -0,0 +1,4 @@ +{ + "files": ["main.py", "core.py", "dialog.py", "websocketclientwrapper.py", + "websockettransport.py", "dialog.ui", "index.html"] +} diff --git a/examples/webchannel/standalone/ui_dialog.py b/examples/webchannel/standalone/ui_dialog.py new file mode 100644 index 000000000..36c2fe400 --- /dev/null +++ b/examples/webchannel/standalone/ui_dialog.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'dialog.ui' +## +## Created by: Qt User Interface Compiler version 6.7.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, + QFont, QFontDatabase, QGradient, QIcon, + QImage, QKeySequence, QLinearGradient, QPainter, + QPalette, QPixmap, QRadialGradient, QTransform) +from PySide6.QtWidgets import (QApplication, QDialog, QGridLayout, QLineEdit, + QPlainTextEdit, QPushButton, QSizePolicy, QWidget) + +class Ui_Dialog(object): + def setupUi(self, Dialog): + if not Dialog.objectName(): + Dialog.setObjectName(u"Dialog") + Dialog.resize(400, 300) + self.gridLayout = QGridLayout(Dialog) + self.gridLayout.setObjectName(u"gridLayout") + self.input = QLineEdit(Dialog) + self.input.setObjectName(u"input") + + self.gridLayout.addWidget(self.input, 1, 0, 1, 1) + + self.send = QPushButton(Dialog) + self.send.setObjectName(u"send") + + self.gridLayout.addWidget(self.send, 1, 1, 1, 1) + + self.output = QPlainTextEdit(Dialog) + self.output.setObjectName(u"output") + self.output.setReadOnly(True) + self.output.setPlainText(u"Initializing WebChannel...") + self.output.setBackgroundVisible(False) + + self.gridLayout.addWidget(self.output, 0, 0, 1, 2) + + + self.retranslateUi(Dialog) + + QMetaObject.connectSlotsByName(Dialog) + # setupUi + + def retranslateUi(self, Dialog): + Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Dialog", None)) + self.input.setPlaceholderText(QCoreApplication.translate("Dialog", u"Message Contents", None)) + self.send.setText(QCoreApplication.translate("Dialog", u"Send", None)) + # retranslateUi + diff --git a/examples/webchannel/standalone/websocketclientwrapper.py b/examples/webchannel/standalone/websocketclientwrapper.py new file mode 100644 index 000000000..a14672f25 --- /dev/null +++ b/examples/webchannel/standalone/websocketclientwrapper.py @@ -0,0 +1,36 @@ +# Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com> +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +from __future__ import annotations + +from PySide6.QtCore import QObject, Signal, Slot + +from websockettransport import WebSocketTransport + + +class WebSocketClientWrapper(QObject): + """Wraps connected QWebSockets clients in WebSocketTransport objects. + + This code is all that is required to connect incoming WebSockets to + the WebChannel. Any kind of remote JavaScript client that supports + WebSockets can thus receive messages and access the published objects. + """ + client_connected = Signal(WebSocketTransport) + + def __init__(self, server, parent=None): + """Construct the client wrapper with the given parent. All clients + connecting to the QWebSocketServer will be automatically wrapped + in WebSocketTransport objects.""" + super().__init__(parent) + self._server = server + self._server.newConnection.connect(self.handle_new_connection) + self._transports = [] + + @Slot() + def handle_new_connection(self): + """Wrap an incoming WebSocket connection in a WebSocketTransport + object.""" + socket = self._server.nextPendingConnection() + transport = WebSocketTransport(socket) + self._transports.append(transport) + self.client_connected.emit(transport) diff --git a/examples/webchannel/standalone/websockettransport.py b/examples/webchannel/standalone/websockettransport.py new file mode 100644 index 000000000..96e9b822a --- /dev/null +++ b/examples/webchannel/standalone/websockettransport.py @@ -0,0 +1,52 @@ +# Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com> +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +from __future__ import annotations + +from PySide6.QtWebChannel import QWebChannelAbstractTransport +from PySide6.QtCore import QByteArray, QJsonDocument, Slot + + +class WebSocketTransport(QWebChannelAbstractTransport): + """QWebChannelAbstractSocket implementation using a QWebSocket internally. + + The transport delegates all messages received over the QWebSocket over + its textMessageReceived signal. Analogously, all calls to + sendTextMessage will be sent over the QWebSocket to the remote client. + """ + + def __init__(self, socket): + """Construct the transport object and wrap the given socket. + The socket is also set as the parent of the transport object.""" + super().__init__(socket) + self._socket = socket + self._socket.textMessageReceived.connect(self.text_message_received) + self._socket.disconnected.connect(self._disconnected) + + def __del__(self): + """Destroys the WebSocketTransport.""" + self._socket.deleteLater() + + def _disconnected(self): + self.deleteLater() + + def sendMessage(self, message): + """Serialize the JSON message and send it as a text message via the + WebSocket to the client.""" + doc = QJsonDocument(message) + json_message = str(doc.toJson(QJsonDocument.Compact), "utf-8") + self._socket.sendTextMessage(json_message) + + @Slot(str) + def text_message_received(self, message_data_in): + """Deserialize the stringified JSON messageData and emit + messageReceived.""" + message_data = QByteArray(bytes(message_data_in, encoding='utf8')) + message = QJsonDocument.fromJson(message_data) + if message.isNull(): + print("Failed to parse text message as JSON object:", message_data) + return + if not message.isObject(): + print("Received JSON message that is not an object: ", message_data) + return + self.messageReceived.emit(message.object(), self) |