[[ activeDiscount.description ]] I'm giving a [[ activeDiscount.discount ]]% discount on my books with the code [[ couponCode ]].

The Qt6 editions of my books are now available, supporting Python 3 with PyQt6 and PySide6.

Read the free tutorial below or unlock the video

Unlock video course
This video is unlocked by your membership.

Tabbed web browsing
Use signal redirection to add a multi-tab interface

In the previous parts of this tutorial we built our own custom web browser using PyQt5 widgets. Starting from a basic app skeleton we've extended it to add support a simple UI, help dialogs and file operations. However, one big feature is missing -- tabbed browsing.

Tabbed browsing was a revolution when it first arrived, but is now an expected feature of web browsers. Being able to keep multiple documents open in the same window makes it easier to keep track of things as you work. In this tutorial we'll take our existing browser application and implement tabbed browsing in it.

This is a little tricky, making use of signal redirection to add additional data about the current tab to Qt's built-in signals. If you get confused, take a look back at that tutorial.

Mozarella Ashbadger (Tabbed)

The full source code for Mozzarella Ashbadger is available in the 15 minute apps repository. You can download/clone to get a working copy, then install requirements using:

pip3 install -r requirements.txt

You can then run Mozzarella Ashbadger with:

python3 browser_tabbed.py

Read on for a walkthrough of how to convert the existing browser code to support tabbed browsing.

Creating a QTabWidget

Adding a tabbed interface to our browser is simple using a QTabWidget. This provides a simple container for multiple widgets (in our case QWebEngineView widgets) with a built-in tabbed interface for switching between them.

Two customisations we use here are .setDocumentMode(True) which provides a Safari-like interface on Mac, and .setTabsClosable(True) which allows the user to close the tabs in the application.

We also connect QTabWidget signals tabBarDoubleClicked, currentChanged and tabCloseRequested to custom slot methods to handle these behaviours.

class MainWindow(QMainWindow):

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

        self.tabs = QTabWidget()
        self.tabs.tabBarDoubleClicked.connect( self.tab_open_doubleclick )
        self.tabs.currentChanged.connect( self.current_tab_changed )
        self.tabs.tabCloseRequested.connect( self.close_current_tab )


The three slot methods accept an i (index) parameter which indicates which tab the signal resulted from (in order).

We use a double-click on an empty space in the tab bar (represented by an index of -1 to trigger creation of a new tab. For removing a tab, we use the index directly to remove the widget (and so the tab), with a simple check to ensure there are at least 2 tabs — closing the last tab would leave you unable to open a new one.

The current_tab_changed handler uses a self.tabs.currentWidget() construct to access the widget (QWebEngineView browser) of the currently active tab, and then uses this to get the URL of the current page. This same construct is used throughout the source for the tabbed browser, as a simple way to interact with the current browser view.

    def tab_open_doubleclick(self, i):
        if i == -1: # No tab under the click

    def current_tab_changed(self, i):
        qurl = self.tabs.currentWidget().url()
        self.update_urlbar( qurl, self.tabs.currentWidget() )
        self.update_title( self.tabs.currentWidget() )

    def close_current_tab(self, i):
        if self.tabs.count() < 2:


The code for adding a new tab is is follows:

    def add_new_tab(self, qurl=None, label="Blank"):

        if qurl is None:
            qurl = QUrl('')

        browser = QWebEngineView()
        browser.setUrl( qurl )
        i = self.tabs.addTab(browser, label)


Signal & Slot changes

While the setup of the QTabWidget and associated signals is simple, things get a little trickier in the browser slot methods.

Whereas before we had a single QWebEngineView now there are multiple views, all with their own signals. If signals for hidden tabs are handled things will get all mixed up. For example, the slot handling a loadCompleted signal must check that the source view is in a visible tab and only act if it is.

We can do this using a little trick for sending additional data with signals. Below is an example of doing this when creating a new QWebEngineView in the add_new_tab function.

    def add_new_tab(self, qurl=None, label="Blank"):

        if qurl is None:
            qurl = QUrl('')

        browser = QWebEngineView()
        browser.setUrl( qurl )
        i = self.tabs.addTab(browser, label)


        # More difficult! We only want to update the url when it's from the
        # correct tab
        browser.urlChanged.connect( lambda qurl, browser=browser:
            self.update_urlbar(qurl, browser) )

        browser.loadFinished.connect( lambda _, i=i, browser=browser:
            self.tabs.setTabText(i, browser.page().title()) )

As you can see, we set a lambda as the slot for the urlChanged signal, accepting the qurl parameter that is sent by this signal. We add the recently created browser object to pass into the update_urlbar function.

Now, whenever the urlChanged signal fires update_urlbar will receive both the new URL and the browser it came from. In the slot method we can then check to ensure that the source of the signal matches the currently visible browser — if not, we simply discard the signal.

    def update_urlbar(self, q, browser=None):

        if browser != self.tabs.currentWidget():
            # If this signal is not from the current tab, ignore

        if q.scheme() == 'https':
            # Secure padlock icon
            self.httpsicon.setPixmap( QPixmap( os.path.join('icons','lock-ssl.png') ) )

            # Insecure padlock icon
            self.httpsicon.setPixmap( QPixmap( os.path.join('icons','lock-nossl.png') ) )

        self.urlbar.setText( q.toString() )

This same technique is used to handle all other signals which we can receive from web views, and which need to be redirected. This is a good pattern to learn and practice as it gives you a huge amount of flexibility when building PyQt5 applications.

What's next?

Feel free to continue experimenting with the browser, adding features and tweaking things to your liking. Some ideas you might want to consider trying out --

  • Add support for Bookmarks/Favorites, either in the menus or as a "Bookmarks Bar"
  • Add a download manager using threads to download in the background and display progress
  • Customize how links are opened, see our quick tip on opening links in new windows
  • Implement real SSL verification (check the certificate)

Remember that the full source code for Mozzarella Ashbadger is available.

Continue reading

Animations and Transformations with QtQuick  PyQt

In the previous tutorial we implemented a basic QML clock application using Python code to get the current time, format it into a string and send that through to our QML layout for display using Qt signals. That gave us a good overview of the structure of Python/QML applications … More