Opening links in a new window with QWebEngineView
Redirect links to a separate floating browser window

It's quite a common practice to use QWebEngineView as a documentation (or document) browser in PyQt5 applications as it allows the documentation to be created using familiar tools. You can build HTML documentation and bundle it with your application (or host them remotely) then allow your users to browse them within the app.

However, this raises an issue when the document contains links to external resources -- how should these links be handled? It's a weird user experience if your users, through clicking a series of links, can end up on Google or Facebook inside your application. One way to avoid this is to enforce opening of certain links in external windows or browsers, ensuring your documentation browser remains just that.

In this quick tutorial we'll look at how to implement custom link handling in your Qt browser and use this to redirect clicked links to different windows or the user's desktop browser.

The skeleton web browser code is shown below. We'll modify this to add the open in new window behavior.

python
from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineView

import os
import sys


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.browser = QWebEngineView()
        self.browser.setUrl(QUrl("https://www.mfitzp.com"))
        self.setCentralWidget(self.browser)


app = QApplication(sys.argv)
window = MainWindow()
window.show()

app.exec_()
python
from PySide2.QtCore import QUrl
from PySide2.QtWidgets import QMainWindow, QApplication
from PySide2.QtWebEngineWidgets import QWebEngineView

import os
import sys


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.browser = QWebEngineView()
        self.browser.setUrl(QUrl("https://www.mfitzp.com"))
        self.setCentralWidget(self.browser)


app = QApplication(sys.argv)
window = MainWindow()
window.show()

app.exec_()

This creates our basic browser window and navigates to the LearnPyQt homepage. All links will open within the same browser window (normal browser behavior).

The basic browser The basic browser window.

To override the default navigation behavior we need to create a customized QWebEnginePage class. This is the Qt class which handles viewing (and editing) of web documents, such as web pages within the QWebEngineView.

By creating a custom QWebEnginePage class we are able to intercept the .acceptNavigationRequest signal and implement custom behavior. For example, we can decline links to block navigation, alter the links to navigate somewhere else, or (as we're doing here) open up custom viewers. In our specific case we will both decline the default behavior, by returning Falseand implement our own, by opening a new window.

For our new windows, we create a custom web engine view (the same type we have in the main window), set the URL and then display the window. Note that we need to keep a reference to the created window (in the external_windows list) so it isn’t destroyed on exiting this method.

For everything else, we pass through to the handler on the parent class with super().acceptNavigationRequest().

python
from PyQt5.QtWebEngineWidgets import QWebEnginePage

class CustomWebEnginePage(QWebEnginePage):
    """ Custom WebEnginePage to customize how we handle link navigation """
    # Store external windows.
    external_windows = []

    def acceptNavigationRequest(self, url,  _type, isMainFrame):
        if _type == QWebEnginePage.NavigationTypeLinkClicked:
            w = QWebEngineView()
            w.setUrl(url)
            w.show()

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

To use our custom page class we need to set it on the browser with .setPage(). After this, any navigation is sent through the custom page instance and our acceptNavigationRequest handler.

python
        self.browser = QWebEngineView()
        self.browser.setPage(CustomWebEnginePage(self))
        self.browser.setUrl(QUrl("http://google.com"))

The full working example is shown below.

python
from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage

import os
import sys


class CustomWebEnginePage(QWebEnginePage):
    """ Custom WebEnginePage to customize how we handle link navigation """
    # Store external windows.
    external_windows = []

    def acceptNavigationRequest(self, url,  _type, isMainFrame):
        if _type == QWebEnginePage.NavigationTypeLinkClicked:
            w = QWebEngineView()
            w.setUrl(url)
            w.show()

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


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.browser = QWebEngineView()
        self.browser.setPage(CustomWebEnginePage(self))
        self.browser.setUrl(QUrl("https://www.mfitzp.com"))
        self.setCentralWidget(self.browser)


app = QApplication(sys.argv)
window = MainWindow()
window.show()

app.exec_()
python
from PySide2.QtCore import QUrl
from PySide2.QtWidgets import QMainWindow, QApplication
from PySide2.QtWebEngineWidgets import QWebEngineView, QWebEnginePage

import os
import sys


class CustomWebEnginePage(QWebEnginePage):
    """ Custom WebEnginePage to customize how we handle link navigation """
    # Store external windows.
    external_windows = []

    def acceptNavigationRequest(self, url,  _type, isMainFrame):
        if _type == QWebEnginePage.NavigationTypeLinkClicked:
            w = QWebEngineView()
            w.setUrl(url)
            w.show()

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


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.browser = QWebEngineView()
        self.browser.setPage(CustomWebEnginePage(self))
        self.browser.setUrl(QUrl("https://www.mfitzp.com"))
        self.setCentralWidget(self.browser)


app = QApplication(sys.argv)
window = MainWindow()
window.show()

app.exec_()

If you run this example and click on a link in the page, a new window will be opened for every link you click. You can continue to navigate within those external windows as normal -- they use the standard QWebEnginePage class, so don't have our open in new window behavior.

Links are opened in a separate window Links are opened in a new window.

Conditionally popping up a new window

Sometimes you only want "external" links to be popped up into a separate window -- navigation within your documentation should stay within the documentation browser window, and only external links (not from your documentation) popped up into a separate window.

Since we have access to the URL being navigated this is pretty straightforward. We can just compare that URL to some pattern (here we're using the hostname via url.host()), and choose to either pop a new window, or pass it to the default handler.

python
class CustomWebEnginePage(QWebEnginePage):
    """ Custom WebEnginePage to customize how we handle link navigation """
    # Store external windows.
    external_windows = []

    def acceptNavigationRequest(self, url,  _type, isMainFrame):
        if (_type == QWebEnginePage.NavigationTypeLinkClicked and
            url.host() != 'www.mfitzp.com'):
            # Pop up external links into a new window.
            w = QWebEngineView()
            w.setUrl(url)
            w.show()

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

Navigating around the LearnPyQt site now stays within the main browser, but external links (such as to the forum) are popped out into a separate window.

External window popped up for external links The external window is popped up for external links only.

Reusing an external window

In this first example we're creating a new window for every link, rather than creating a new window if none exists and then sending all subsequent link clicks to that same window. To get this second behavior we just need to hold a single reference to the external window and check if it exists before creating a new one.

python
class WebEnginePage(QWebEnginePage):
    # Store second window.
    external_window = None

    def acceptNavigationRequest(self, url,  _type, isMainFrame):
        print(url, _type, isMainFrame)
        if _type == QWebEnginePage.NavigationTypeLinkClicked:
            if not self.external_window:
                self.external_window = QWebEngineView()

            self.external_window.setUrl(url)
            self.external_window.show()
            return False

        return super().acceptNavigationRequest(url,  _type, isMainFrame)

Putting this into our example gives us the following complete example.

python
from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage

import sys


class CustomWebEnginePage(QWebEnginePage):
    # Store second window.
    external_window = None

    def acceptNavigationRequest(self, url,  _type, isMainFrame):
        print(url, _type, isMainFrame)
        if _type == QWebEnginePage.NavigationTypeLinkClicked:
            if not self.external_window:
                self.external_window = QWebEngineView()

            self.external_window.setUrl(url)
            self.external_window.show()
            return False

        return super().acceptNavigationRequest(url,  _type, isMainFrame)


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.browser = QWebEngineView()
        self.browser.setPage(CustomWebEnginePage(self))
        self.browser.setUrl(QUrl("https://www.mfitzp.com"))
        self.setCentralWidget(self.browser)


app = QApplication(sys.argv)
window = MainWindow()
window.show()

app.exec_()
python
from PySide2.QtCore import QUrl
from PySide2.QtWidgets import QMainWindow, QApplication
from PySide2.QtWebEngineWidgets import QWebEngineView, QWebEnginePage

import sys


class CustomWebEnginePage(QWebEnginePage):
    # Store second window.
    external_window = None

    def acceptNavigationRequest(self, url,  _type, isMainFrame):
        print(url, _type, isMainFrame)
        if _type == QWebEnginePage.NavigationTypeLinkClicked:
            if not self.external_window:
                self.external_window = QWebEngineView()

            self.external_window.setUrl(url)
            self.external_window.show()
            return False

        return super().acceptNavigationRequest(url,  _type, isMainFrame)


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.browser = QWebEngineView()
        self.browser.setPage(CustomWebEnginePage(self))
        self.browser.setUrl(QUrl("https://www.mfitzp.com"))
        self.setCentralWidget(self.browser)


app = QApplication(sys.argv)
window = MainWindow()
window.show()

app.exec_()

When clicking on a link in the browser window a new window is created. You can browse in that window as normal, but if you click a new link in the parent window, the current page will be replaced with that link.

You might also want to consider popping up external links in the users default browser. This allows them to bookmark the links, or browse as normally, rather than in the restricted browser view your app provides.

For this we don't need to create a window, we can just send the url to the system default handler. This is accomplished by passing the url to the QDesktopServices.openUrl() method. The full working example is shown below.

python
from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from PyQt5.QtGui import QDesktopServices

import os
import sys


class CustomWebEnginePage(QWebEnginePage):
    """ Custom WebEnginePage to customize how we handle link navigation """
    # Store external windows.
    external_windows = []

    def acceptNavigationRequest(self, url,  _type, isMainFrame):
        if (_type == QWebEnginePage.NavigationTypeLinkClicked and
            url.host() != 'www.mfitzp.com'):
            # Send the URL to the system default URL handler.
            QDesktopServices.openUrl(url)
            return False
        return super().acceptNavigationRequest(url,  _type, isMainFrame)


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.browser = QWebEngineView()
        self.browser.setPage(CustomWebEnginePage(self))
        self.browser.setUrl(QUrl("https://www.mfitzp.com"))
        self.setCentralWidget(self.browser)


app = QApplication(sys.argv)
window = MainWindow()
window.show()

app.exec_()
python
from PySide2.QtCore import QUrl
from PySide2.QtWidgets import QMainWindow, QApplication
from PySide2.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from PySide2.QtGui import QDesktopServices

import os
import sys


class CustomWebEnginePage(QWebEnginePage):
    """ Custom WebEnginePage to customize how we handle link navigation """
    # Store external windows.
    external_windows = []

    def acceptNavigationRequest(self, url,  _type, isMainFrame):
        if (_type == QWebEnginePage.NavigationTypeLinkClicked and
            url.host() != 'www.mfitzp.com'):
            # Send the URL to the system default URL handler.
            QDesktopServices.openUrl(url)
            return False
        return super().acceptNavigationRequest(url,  _type, isMainFrame)


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.browser = QWebEngineView()
        self.browser.setPage(CustomWebEnginePage(self))
        self.browser.setUrl(QUrl("https://www.mfitzp.com"))
        self.setCentralWidget(self.browser)


app = QApplication(sys.argv)
window = MainWindow()
window.show()

app.exec_()

If you run this and click on any external links, you'll see them open in your system's default browser window (here showing Chrome).

Link opened in default browser window

Continue reading

Creating additional windows  pyqt

In an earlier tutorial we've already covered how to open dialog windows. These are special windows which (by default) grab the focus of the user, and run their own event loop, effectively blocking the execution of the rest of your app. However, quite often you will want to open a … More

Join the Community

I start with thanks to Martin Fitzpatrick for excellent explanation and code for running QRunnable objects within QThreadPool. My problem is that I have three separate processes where one of those is taking the results of the two as arguments. Of course I could combine them into on big function but as the result are stored in variable accessible for all the processes it is not necessary to run all of three processes every time.

So I start with Martin code that is used as class definition for QRunnable object and signals

class Worker(QtCore.QRunnable):
'''
Worker thread

Inherits from QRunnable to handler worker thread setup, signals and wrap-up.

:param callback: The function callback to run on this worker thread. Supplied args and
             kwargs will be passed through to the runner.
:type callback: function
:param args: Arguments to pass to the callback function

Then I am using a function which executes functions passed to that as arguments

    def exe_worker_run(self, WorkerPool, function, arguments):
        Worker = thread.Worker(function, arguments)
        Worker.signal.started.connect(self.sig_thread_start)
        Worker.signal.error.connect(self.sig_thread_error)
        Worker.signal.result.connect(self.sig_thread_result)
        Worker.signal.finished.connect(self.sig_thread_finish)
        WorkerPool.start(Worker)

The results are intercepted by the following function (of course it is simplified)

    def sig_thread_result(self, result):

Then the “function” argument in the exe_worker_run is the name of the function that performs the real job. Again the simplified version of that function will look like

    def exe_languages_load(self, arguments, callback_progress, callback_data):
        try:
            result = (sql query to database)
        except Exception as ex:
            session.rollback()
            print(ex)
        finally:
            session.close()
            return result

The problem is that the results of two functions which are then arguments for third are not emitted before the execution of third function is started. What should I do to allow sequential execution of all three functions in a way which will postpone the execution of third until the results of first and second are emitted and assigned to respective dataframes?