python-3.xpyqtpyqt5qtwebengineqtwebview

How to use QtWebEngine createWindow in PyQt5


I am trying to make a window that contains a QWebEngineView. Now I want the browser to be able to handle the create window or _blank type triggers, or specifically to open a URL in a new window when required. In the below code, when there is requirement to create a window by the browser, createwindow() is called, but that doesn't open the window. Please help me with the correct way to open a new window by the browser when required in the below case.

import json
import sys
import os
import time
import json
import sys
import platform

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from PyQt5.QtWebEngineWidgets import QWebEngineSettings as QWebSettings
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtCore import QUrl


from main_dash import Ui_MainWindow


class MainDashWindow(QMainWindow):
    socketSignal = QtCore.pyqtSignal(object)  # must be defined in class level

    def __init__(self):
        QMainWindow.__init__(self)
        self.ui = Ui_MainWindow()
        # self.ui.setupUi(self)
        # ui = Ui_MainWindow()
        self.isMax = 0

        self.ui.setupUi(self)

    def re_setup(self):
        self.page = WebEnginePage2()
        self.page.Notifications = True
        self.ui.full_content_container.hide()
        self.page.createWindow = True

        self.page.settings().setAttribute(QWebSettings.JavascriptEnabled, True)
        self.page.settings().setAttribute(QWebSettings.JavascriptCanOpenWindows, True)
        self.page.settings().setAttribute(
            QWebSettings.JavascriptCanAccessClipboard, True)

        # self.full_content_container is the webengineview in the mainUi file
        self.ui.full_content_container.setPage(self.page)
        # self.ui.full_content_container.setContextMenuPolicy(Qt.NoContextMenu)

        url6 = "...../icons_nec/ui/index.html"

        self.ui.full_content_container.setUrl(QtCore.QUrl(url6))
        self.ui.full_content_container.loadFinished.connect(
            self.on_load_finished)

        ########################################################################
        self.show()
        ## ==> END ##

    def get_path(self, filename):
        if hasattr(sys, "_MEIPASS"):
            return f'{os.path.join(sys._MEIPASS, filename)}'
        else:
            return f'{filename}'

    def on_load_finished(self):
        self.ui.full_content_container.show()


class WebEnginePage2(QWebEnginePage):
    def __init__(self, *args, **kwargs):
        QWebEnginePage.__init__(self, *args, **kwargs)
        self.featurePermissionRequested.connect(
            self.onFeaturePermissionRequested)

    def onFeaturePermissionRequested(self, url, feature):
        self.setFeaturePermission(
            url, feature, QWebEnginePage.PermissionGrantedByUser)

    def createWindow(self,
                    wintype: QWebEnginePage.WebWindowType) -> QWebEngineView:
        """Called by Qt when a page wants to create a new tab or window.

        In case the user wants to open a resource in a new tab, we use the
        createWindow handling of the main page to achieve that.

        See WebEngineView.createWindow for details.
        """
        return self.page().inspectedPage().view().createWindow(wintype)
    # Store external windows.
    external_windows = []

    def acceptNavigationRequest(self, url,  _type, isMainFrame):
        print("in navigation")
        if _type == QWebEnginePage.NavigationTypeLinkClicked:
            w = QWebEngineView()
            w.setUrl(url)
            w.show()
            print("link detected")

            # Keep reference to external window, so it isn't cleared up.
            self.external_windows.append(w)
            return False
            # QDesktopServices.openUrl(url)
        # elif _type == QWebEnginePage.NavigationType
        return super().acceptNavigationRequest(url,  _type, isMainFrame)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainDashWindow()
    window.re_setup()

    sys.exit(app.exec_())

Here is the Ui_MainWindow file, which is being imported above. I need to figure out with such a scenario how can I implement createWindow() if required by websites.

from PyQt5 import QtWebEngineWidgets
import all_icons_rc
from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(1280, 720)
        MainWindow.setMinimumSize(QtCore.QSize(1280, 720))
        MainWindow.setMaximumSize(QtCore.QSize(1920, 1080))
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout_2.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout_2.setSpacing(0)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.Header = QtWidgets.QFrame(self.centralwidget)
        self.Header.setMinimumSize(QtCore.QSize(0, 40))
        self.Header.setMaximumSize(QtCore.QSize(16777215, 50))
        self.Header.setStyleSheet("background-color: rgb(33, 37, 41);")
        self.Header.setFrameShape(QtWidgets.QFrame.NoFrame)
        self.Header.setFrameShadow(QtWidgets.QFrame.Raised)
        self.Header.setObjectName("Header")
        self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.Header)
        self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout_4.setSpacing(0)
        self.horizontalLayout_4.setObjectName("horizontalLayout_4")
        self.frame_2 = QtWidgets.QFrame(self.Header)
        self.frame_2.setStyleSheet("")
        self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised)
        self.frame_2.setObjectName("frame_2")
        self.horizontalLayout_4.addWidget(self.frame_2)
        self.frame = QtWidgets.QFrame(self.Header)
        self.frame.setMaximumSize(QtCore.QSize(150, 16777215))
        self.frame.setStyleSheet("/*background-color: rgb(85, 255, 0);*/")
        self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
        self.frame.setObjectName("frame")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.frame)
        self.horizontalLayout.setSpacing(15)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.minimize_btn = QtWidgets.QPushButton(self.frame)
        self.minimize_btn.setText("")
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap(
            ":/icons/Icons/icons8_macos_minimize_50px.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.minimize_btn.setIcon(icon)
        self.minimize_btn.setIconSize(QtCore.QSize(30, 30))
        self.minimize_btn.setFlat(True)
        self.minimize_btn.setObjectName("minimize_btn")
        self.horizontalLayout.addWidget(self.minimize_btn)
        self.maximize_btn = QtWidgets.QPushButton(self.frame)
        self.maximize_btn.setText("")
        icon1 = QtGui.QIcon()
        icon1.addPixmap(QtGui.QPixmap(
            ":/icons/Icons/icons8_maximize_window_50px.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.maximize_btn.setIcon(icon1)
        self.maximize_btn.setIconSize(QtCore.QSize(30, 30))
        self.maximize_btn.setFlat(True)
        self.maximize_btn.setObjectName("maximize_btn")
        self.horizontalLayout.addWidget(self.maximize_btn)
        self.close_btn = QtWidgets.QPushButton(self.frame)
        self.close_btn.setText("")
        icon2 = QtGui.QIcon()
        icon2.addPixmap(QtGui.QPixmap(
            ":/icons/Icons/icons8_Close_50px_2.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.close_btn.setIcon(icon2)
        self.close_btn.setIconSize(QtCore.QSize(30, 30))
        self.close_btn.setFlat(True)
        self.close_btn.setObjectName("close_btn")
        self.horizontalLayout.addWidget(self.close_btn)
        self.horizontalLayout_4.addWidget(self.frame)
        self.verticalLayout_2.addWidget(self.Header)
        self.body = QtWidgets.QFrame(self.centralwidget)
        self.body.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.body.setFrameShadow(QtWidgets.QFrame.Raised)
        self.body.setLineWidth(0)
        self.body.setObjectName("body")
        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.body)
        self.verticalLayout_3.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout_3.setSpacing(0)
        self.verticalLayout_3.setObjectName("verticalLayout_3")
        self.full_content_container = QtWebEngineWidgets.QWebEngineView(
            self.body)#<----This is webengineview
        self.full_content_container.setStyleSheet("background-color: rgb(85, 255, 255);\n"
                                                "border:none;")
       
        self.full_content_container.setObjectName("full_content_container")
        self.verticalLayout_3.addWidget(self.full_content_container)
        self.verticalLayout_2.addWidget(self.body)
        MainWindow.setCentralWidget(self.centralwidget)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))

For now, just Ignore the icons.

index.html file snippet which is being loaded in webengineview for testing

<!DOCTYPE html>
<html>
<body>

<h1>The a target attribute</h1>

<p>Open link in a new window or tab: <a href="https://pathor.in" target="_blank">Visit PathOr!</a></p>

</body>
</html>


Solution

  • UPDATE:

    Below is an implementaion based on your example code. Everything should work as expected if you completely replace your WebEnginePage2 class with this one:

    class WebEnginePage2(QWebEnginePage):
        _windows = {}
    
        @classmethod
        def newWindow(cls):
            window = QWebEngineView()
            window.setObjectName(f'window-{id(window)}')
            window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
            window.destroyed.connect(
                lambda window: cls._windows.pop(window.objectName(), None))
            window.setPage(cls(window))
            cls._windows[window.objectName()] = window
            return window
    
        def __init__(self, *args, **kwargs):
            QWebEnginePage.__init__(self, *args, **kwargs)
            self.featurePermissionRequested.connect(
                self.onFeaturePermissionRequested)
            self.geometryChangeRequested.connect(self.handleGeometryChange)
    
        def handleGeometryChange(self, rect):
            view = self.view()
            window = QtGui.QWindow.fromWinId(view.winId())
            if window is not None:
                rect = rect.marginsRemoved(window.frameMargins())
            view.resize(rect.size())
            view.show()
    
        def createWindow(self, mode):
            window = self.newWindow()
            if mode != QtWebEngineWidgets.QWebEnginePage.WebDialog:
                window.resize(800, 600)
                window.show()
            return window.page()
    
        def onFeaturePermissionRequested(self, url, feature):
            self.setFeaturePermission(
                url, feature, QWebEnginePage.PermissionGrantedByUser)
    

    You need to create a new instance of the browser window and keep a reference to it in a window list. It's also important to ensure the window is given an appropriate size, otherwise it won't be visible. For windows opened with javascript, the geometryChangeRequested signal can be used to set the requested size, otherwise a default should be used.

    Below is a simple demo that implements the basic features. Hopefully it should be obvious how to adapt this to your own application:

    from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets
    
    class Browser(QtWebEngineWidgets.QWebEngineView):
        _windows = set()
    
        @classmethod
        def _removeWindow(cls, window):
            cls._windows.discard(window)
    
        @classmethod
        def newWindow(cls):
            window = cls()
            cls._windows.add(window)
            return window
    
        def __init__(self, parent=None):
            super().__init__(parent)
            self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
            self.page().geometryChangeRequested.connect(self.handleGeometryChange)
            self.page().titleChanged.connect(self.setWindowTitle)
    
        def closeEvent(self, event):
            self._removeWindow(self)
            event.accept()
    
        def handleGeometryChange(self, rect):
            window = QtGui.QWindow.fromWinId(self.winId())
            if window is not None:
                rect = rect.marginsRemoved(window.frameMargins())
            self.resize(rect.size())
            self.setFocus()
            self.show()
    
        def createWindow(self, mode):
            window = self.newWindow()
            if mode != QtWebEngineWidgets.QWebEnginePage.WebDialog:
                window.resize(800, 600)
                window.show()
            return window
    
    html = """
    <html><head><title>Test Page</title>
    <script type="text/javascript"><!--
    var count = 0
    var url = 'https://www.google.com'
    function newWindow() {
        count += 1
        window.open(url, 'Test' + count, 'width=640,height=480');
    }
    --></script>
    </head>
    <body>
    <input type="button" value="New Window" onclick="newWindow()" />
    <p><a href="https://www.google.com" target="_blank">Blank</a></p>
    </body>
    </html>"""
    
    if __name__ == '__main__':
    
        import sys
        app = QtWidgets.QApplication(sys.argv)
        browser = Browser()
        browser.setHtml(html)
        browser.setGeometry(600, 100, 400, 200)
        browser.show()
        sys.exit(app.exec_())