Read the free tutorial below or

Get the Video

Adding navigational controls to a PyQt5 Web Browser
Hook up QAction signals to web browser slots

Mozzarella Ashbadger course

In the first part of this tutorial we put together a simple skeleton of a browser using Qt's built-in browser widget. This allows you to open a webpage, view it and click around -- all that is handled automatically for you. But the interface is entirely up to you.

To convert this bare-bones application into something more resembling an actual browser, in this part we'll set about adding UI controls. These are added as a series of QActions on a QToolbar. We add these definitions to the __init__ block of the QMainWindow.

python
navtb = QToolBar("Navigation")
navtb.setIconSize( QSize(16,16) )
self.addToolBar(navtb)

back_btn = QAction( QIcon(os.path.join('icons','arrow-180.png')), "Back", self)
back_btn.setStatusTip("Back to previous page")
back_btn.triggered.connect( self.browser.back )
navtb.addAction(back_btn)

The QWebEngineView includes slots for forward, back and reload navigation, which we can connect to directly to our action's .triggered signals.

python
next_btn = QAction( QIcon(os.path.join('icons','arrow-000.png')), "Forward", self)
next_btn.setStatusTip("Forward to next page")
next_btn.triggered.connect( self.browser.forward )
navtb.addAction(next_btn)

reload_btn = QAction( QIcon(os.path.join('icons','arrow-circle-315.png')), "Reload", self)
reload_btn.setStatusTip("Reload page")
reload_btn.triggered.connect( self.browser.reload )
navtb.addAction(reload_btn)

home_btn = QAction( QIcon(os.path.join('icons','home.png')), "Home", self)
home_btn.setStatusTip("Go home")
home_btn.triggered.connect( self.navigate_home )
navtb.addAction(home_btn)

While forward, back and reload can use built-in slots to perform their actions, the navigate home button requires a custom slot function. The slot function is defined on our QMainWindow class, and simply sets the URL of the browser to the Google homepage. Note that the URL must be passed as a QUrl object.

python
def navigate_home(self):
    self.browser.setUrl( QUrl("http://www.google.com") )

Any decent web browser also needs an URL bar, and some way to stop the navigation.

python
self.httpsicon = QLabel() # Yes, really!
self.httpsicon.setPixmap( QPixmap( os.path.join('icons','lock-nossl.png') ) )
navtb.addWidget(self.httpsicon)

self.urlbar = QLineEdit()
self.urlbar.returnPressed.connect( self.navigate_to_url )
navtb.addWidget(self.urlbar)

stop_btn = QAction( QIcon(os.path.join('icons','cross-circle.png')), "Stop", self)
stop_btn.setStatusTip("Stop loading current page")
stop_btn.triggered.connect( self.browser.stop )
navtb.addAction(stop_btn)

As before the 'stop' functionality is available as a slot on the QWebEngineView itself, and we can simply connect the .triggered signal from the stop button to the existing slot. However, other features of the URL bar we must handle independently.

First we add a QLabel to hold our SSL or non-SSL icon to indicate whether the page is secure. Next, we add the URL bar which is simply a QLineEdit. To trigger the loading of the URL in the bar when entered (return key pressed) we connect to the .returnPressed signal on the widget to drive a custom slot function to trigger navigation to the specified URL.

python
def navigate_to_url(self): # Does not receive the Url
    q = QUrl( self.urlbar.text() )
    if q.scheme() == "":
        q.setScheme("http")

    self.browser.setUrl(q)

We also want the URL bar to update in response to page changes. To do this we can use the .urlChanged and .loadFinished signals from the QWebEngineView. We set up the connections from the signals in the __init__ block as follows:

python
self.browser.urlChanged.connect(self.update_urlbar)
self.browser.loadFinished.connect(self.update_title)

Then we define the target slot functions which for these signals. The first, to update the URL bar accepts a QUrl object and determines whether this is a http or https URL, using this to set the SSL icon.

This is a terrible way to test if a connection is 'secure'. To be correct we should perform a certificate validation.

The QUrl is converted to a string and the URL bar is updated with the value. Note that we also set the cursor position back to the beginning of the line to prevent the QLineEdit widget scrolling to the end.

python
def update_urlbar(self, q):

    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)

It's also a nice touch to update the title of the application window with the title of the current page. We can get this via browser.page().title() which returns the contents of the <title></title> tag in the currently loaded web page.

python
def update_title(self):
    title = self.browser.page().title()
    self.setWindowTitle("%s - Mozarella Ashbadger" % title)

That's the basic interface implemented. In the next part we'll look at adding the standard Load & Save operations, to allow us to open local HTML files and save web pages to disk.

Create GUI Applications with Python & Qt6
The easy way to create desktop applications

My complete guide, updated for 2021 & PyQt6. Everything you need build real apps.

Downloadable ebook (PDF, ePub) & Complete Source code

To support developers in [[ countryRegion ]] I give a [[ localizedDiscount[couponCode] ]]% discount with the code [[ couponCode ]] — Enjoy!

For [[ activeDiscount.description ]] I'm giving a [[ activeDiscount.discount ]]% discount with the code [[ couponCode ]] — Enjoy!

Complete Mark As Complete Next

Continue reading

QtWebEngineWidgets, the new browser API in PyQt 5.6  pyqt

With the release of Qt 5.5 the Qt WebKit API was deprecated and replaced with the new QtWebEngine API, based on Chromium. The WebKit API was subsequently removed from Qt entirely with the release of Qt 5.6 in mid-2016. The change to use Chromium for web widgets within Qt was motivated by improved cross-platform support, multimedia and HTML5 features, together with a rapid development pace offering better future proofing. The benefits were considered enough to warrant breaking API changes in a non-major release, rather than waiting for Qt 6. The API for `QtWebEngineWidgets` was designed to make it — more or less — a drop-in replacement for it's predecessor, and this is also the case when using the API from PyQt5. However, the opportunity was taken to make a number of improvements to the API, which require changes to existing code. These changes include simplification of the page model and the conversion of HTML export and printing methods to be asynchronous. This is good news since it prevents either of these operations from blocking your application execution. This course gives a quick walkthrough of the changes, including working examples with the new API. Goodbye .mainFrame() In the WebKit based API, each page was represented by a QWebPage object, which in turn contained frames of content. This reflects the origin of this API during a time when HTML frames were thing, used to break a document down into grid of constituent parts, each loaded as a separate source. The .mainFrame() of the page refers to the browser window, as a QWebFrame object which can itself contain multiple child frames, or HTML content. To get the HTML content of the page we would previously have to access it via — python browser.page().mainFrame().toHtml() With the use of frames in web pages falling out favour, and being deprecated in HTML5, this API becomes an unnecessary complication. Now the call to .page() returns a QWebEnginePage object, which directly contains the HTML itself. python browser.page().toHtml() If you try to use the above to actually get the HTML content of a page, it won't work. That's because this method has been changed to use an asynchronous callback. See the next section. Asynchronous toHtml Generating the HTML for a page can take some time, particularly on large pages. Not a really long time, but enough to make your application stutter. To avoid this, the .toHtml() method on QWebEnginePage has been converted to be asynchronous. To receive a copy of the HTML content you call .toHtml() as before, but now pass in a callback function to receive the HTML once the generation has completed. The initial completes immediately and so does not block your application execution. The following example shows a save method using a small callback function write_html_to_file to complete the process of writing the page HTML to the selected file location. python def save_file(self): filename, _ = QFileDialog.getSaveFileName(self, "Save Page As", "", "Hypertext Markup Language (*.htm *html);;" "All files (*.*)") if filename: def write_html_to_file(html): with open(filename, 'w') as f: f.write(html) self.browser.page().toHtml(write_html_to_file) The `write_html_to_file` method will be called whenever the generation of the HTML has completed, assuming there are no errors in doing so. You can pass any Python function/method in as a callback. Asynchronous Printing & Print to PDF In the previous API printing was also handled from the QWebFrame object, via the print slot (print_ in PyQt due to print being a keyword in Python 2.7). This too has been moved to the QWebEnginePage object, now as a normal method rather than a slot. Because PyQt5 is Python 3 only, the method is now named `print()`, without a trailing underscore. To print a page we can call .print() on any QWebEnginePage object, passing in an instance of QPrinter to print to, and a callback function to call once complete. The QPrinter instance can be configured using a QPrintDialog as normal, however you must ensure the printer object is not cleaned up/removed before the end of the printing process. While you can generate a new `QPrinter` object from a call to `QPrintDialog`, this will be cleared up by Qt even if you hold onto a Python reference, leading to a segmentation fault. The simplest solution is just to create a `QPrinter` object at the start of your application and re-use it. First create an instance of QPrinter on your main window during initialization, e.g. python self.printer = QPrinter() Then you can use the following to show a print dialog and (optionally) print a page: python def print_page(self): dlg = QPrintDialog(self.printer) if dlg.exec_(): self.browser.page().print(self.printer, self.print_completed) def print_completed(self, success): pass # Do something in here, maybe update the status bar? The callback is required, but your not required to do anything in it. It receives a bool value indicating the success/failure of the printing process. Print to PDF The new QWebEnginePage API also provides a little bonus in the form of PDF printing from HTML pages via printToPdf. This method accepts either a str filename to write the resulting PDF to, or a callback function to receive the rendered PDF as a QByteArray. python browser.page().printToPdf(path) # render to PDF and save to path Existing files will be overwritten when writing to file. While this operation is is asynchronous there is no callback. Instead the notification of success/failure comes via a signal on the page object pdfPrintingFinished, which receives the file path string and a success bool. This signal is not triggered when using a callback: python def rendered_pdf_callback(b): print(b) browser.page().printToPdf(rendered_pdf_callback) # render to PDF and send as `QByteArray` to `rendered_pdf_callback` Paper formats and margins can all be configured by passing additional parameters. More