I'm using QWebEngineView to display HTML that's been produced from Markdown. Clicking a link causes the loaded web page to replace the entire contents of the QWebEngineView widget. How can I launch the page in a separate browser window instead? Is there something I can override to intercept the event and handle it myself?
When you load HTML content into a QWebEngineView, any links that the user clicks will navigate inside that same widget by default. The HTML you were displaying gets replaced with whatever the link points to, which usually isn't what you want — especially if your QWebEngineView is being used to show rendered Markdown or documentation.
The good news is that Qt gives you a clean way to intercept navigation requests and decide what to do with them. You can open links in a brand new window, reuse a single external window, or even hand links off to the user's default system browser.
Let's walk through each approach.
How navigation works in QWebEngineView
Every QWebEngineView has an associated QWebEnginePage that handles the actual page logic — loading content, running JavaScript, and processing navigation requests. When a user clicks a link, the page calls a method named acceptNavigationRequest(). This method receives the URL, the type of navigation, and whether it's happening in the main frame. By returning True, navigation proceeds as normal. By returning False, you block it.
By subclassing QWebEnginePage and overriding acceptNavigationRequest(), you gain full control over what happens when a link is clicked. This follows the same pattern used throughout Qt, where you subclass existing widgets to customize behavior.
Setting up the project
Before we start, make sure you have PyQt6 and PyQt6-WebEngine installed:
pip install PyQt6 PyQt6-WebEngine
The web engine components are shipped as a separate package, so you need both.
Opening each link in a new window
The most straightforward approach is to open every clicked link in its own QWebEngineView window. Here's a custom page class that does exactly that:
from PyQt6.QtWebEngineCore import QWebEnginePage
from PyQt6.QtWebEngineWidgets import QWebEngineView
class CustomWebEnginePage(QWebEnginePage):
"""Custom WebEnginePage to customize how we handle link navigation."""
# Store external windows so they aren't garbage collected.
external_windows = []
def acceptNavigationRequest(self, url, _type, isMainFrame):
if _type == QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
w = QWebEngineView()
w.setUrl(url)
w.show()
# Keep a reference so the window stays alive.
self.external_windows.append(w)
return False
return super().acceptNavigationRequest(url, _type, isMainFrame)
When acceptNavigationRequest is called, we check whether the navigation type is NavigationTypeLinkClicked. If it is, we create a new QWebEngineView, set its URL, and show it. We return False to prevent the original view from navigating away.
One thing to note: we store a reference to each new window in the external_windows list. Without this, Python's garbage collector would destroy the window object as soon as the method returns, and you'd never see it appear. This is the same challenge you encounter when creating multiple windows in PyQt6 — you always need to keep a reference.
For any other type of navigation — like the initial page load — we pass the request through to the default handler via super().
To use this custom page, you set it on your QWebEngineView with .setPage():
from PyQt6.QtCore import QUrl
self.browser = QWebEngineView()
self.browser.setPage(CustomWebEnginePage(self))
self.browser.setUrl(QUrl("https://www.example.com"))
Reusing a single external window
If you'd rather send all clicked links to the same external window instead of opening a new one every time, you can keep a single reference and reuse it:
from PyQt6.QtWebEngineCore import QWebEnginePage
from PyQt6.QtWebEngineWidgets import QWebEngineView
class CustomWebEnginePage(QWebEnginePage):
"""Custom WebEnginePage that reuses a single external window."""
external_window = None
def acceptNavigationRequest(self, url, _type, isMainFrame):
if _type == QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
if self.external_window is None:
self.external_window = QWebEngineView()
self.external_window.setUrl(url)
self.external_window.show()
return False
return super().acceptNavigationRequest(url, _type, isMainFrame)
The first time a link is clicked, a new window is created. After that, every subsequent click updates the same window's URL. This gives a cleaner experience if you don't want multiple windows piling up.
Opening links in the system default browser
Sometimes you don't want to manage extra windows at all — you just want links to open in whatever browser the user normally uses (Chrome, Firefox, etc.). Python's QDesktopServices makes this easy:
from PyQt6.QtGui import QDesktopServices
from PyQt6.QtWebEngineCore import QWebEnginePage
class CustomWebEnginePage(QWebEnginePage):
"""Custom WebEnginePage that opens links in the system browser."""
def acceptNavigationRequest(self, url, _type, isMainFrame):
if _type == QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
QDesktopServices.openUrl(url)
return False
return super().acceptNavigationRequest(url, _type, isMainFrame)
QDesktopServices.openUrl() hands the URL off to the operating system, which opens it in the default browser. This is often the best choice when your QWebEngineView is displaying local or generated content and external links should leave your application entirely.
Handling different link types differently
You can also combine these approaches. For example, you might want internal links (same domain) to navigate normally inside the view, while external links open in the system browser:
from PyQt6.QtGui import QDesktopServices
from PyQt6.QtWebEngineCore import QWebEnginePage
class CustomWebEnginePage(QWebEnginePage):
"""Open external links in system browser, allow internal navigation."""
def acceptNavigationRequest(self, url, _type, isMainFrame):
if _type == QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
if url.host() != "myapp.local":
QDesktopServices.openUrl(url)
return False
return super().acceptNavigationRequest(url, _type, isMainFrame)
Here, any link that doesn't point to myapp.local gets sent to the system browser. Links within myapp.local are allowed to navigate normally. You can adjust the condition to match whatever domain or URL pattern makes sense for your application.
Complete working example
Here's a full application that loads some HTML containing links, and opens any clicked link in the system's default browser. If you're new to building PyQt6 applications, you might want to start with the first window tutorial before diving into this example.
import sys
from PyQt6.QtCore import QUrl
from PyQt6.QtGui import QDesktopServices
from PyQt6.QtWebEngineCore import QWebEnginePage
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
class CustomWebEnginePage(QWebEnginePage):
"""Custom WebEnginePage that opens links in the system browser."""
def acceptNavigationRequest(self, url, _type, isMainFrame):
if _type == QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
QDesktopServices.openUrl(url)
return False
return super().acceptNavigationRequest(url, _type, isMainFrame)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("QWebEngineView Link Handling")
self.setMinimumSize(800, 600)
# Create the web view with our custom page.
self.browser = QWebEngineView()
self.browser.setPage(CustomWebEnginePage(self.browser))
# Load some HTML with links to demonstrate the behavior.
html = """
<html>
<head>
<style>
body {
font-family: sans-serif;
padding: 40px;
line-height: 1.6;
}
a {
color: #1a73e8;
}
</style>
</head>
<body>
<h1>Link Handling Demo</h1>
<p>
This content is displayed inside a QWebEngineView.
Clicking any of the links below will open them in your
system's default browser, rather than navigating away
from this page.
</p>
<ul>
<li><a href="https://www.python.org">Python.org</a></li>
<li><a href="https://www.qt.io">Qt.io</a></li>
<li><a href="https://www.pythonguis.com">PythonGUIs.com</a></li>
</ul>
</body>
</html>
"""
self.browser.setHtml(html, QUrl("https://myapp.local/"))
# Set up the layout.
container = QWidget()
layout = QVBoxLayout()
layout.addWidget(self.browser)
container.setLayout(layout)
self.setCentralWidget(container)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Run this and you'll see a simple page with three links. Click any of them and they'll open in your default web browser, while the original content stays exactly as it was in the QWebEngineView.
You can swap CustomWebEnginePage for either of the other versions shown earlier (new window per link, or single reused window) depending on the behavior you need. The pattern is always the same: subclass QWebEnginePage, override acceptNavigationRequest, and set your custom page on the view with setPage(). Once your application is ready for distribution, you can package it with PyInstaller for easy deployment.
Create GUI Applications with Python & Qt6 by Martin Fitzpatrick
(PyQt6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!