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.
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.
Create GUI Applications with Python & Qt5 by Martin Fitzpatrick — (PyQt5 Edition) The hands-on guide to making apps with Python — Over 10,000 copies sold!
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().__init__(*args, **kwargs)
self.tabs = QTabWidget()
self.tabs.setDocumentMode(True)
self.tabs.tabBarDoubleClicked.connect( self.tab_open_doubleclick )
self.tabs.currentChanged.connect( self.current_tab_changed )
self.tabs.setTabsClosable(True)
self.tabs.tabCloseRequested.connect( self.close_current_tab )
self.setCentralWidget(self.tabs)
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
self.add_new_tab()
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:
return
self.tabs.removeTab(i)
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)
self.tabs.setCurrentIndex(i)
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)
self.tabs.setCurrentIndex(i)
# 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
return
if q.scheme() == 'https':
# Secure padlock icon
self.httpsicon.setPixmap( QPixmap( os.path.join('icons','lock-ssl.png') ) )
else:
# Insecure padlock icon
self.httpsicon.setPixmap( QPixmap( os.path.join('icons','lock-nossl.png') ) )
self.urlbar.setText( q.toString() )
self.urlbar.setCursorPosition(0)
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.
Packaging Python Applications with PyInstaller by Martin Fitzpatrick — This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.