WebEngine Widgets Simple Browser Example

Simple Browser demonstrates how to use the Qt WebEngine C++ classes to develop a small Web browser application that contains the following elements:

  • Menu bar for opening stored pages and managing windows and tabs.
  • Navigation bar for entering a URL and for moving backward and forward in the web page browsing history.
  • Multi-tab area for displaying web content within tabs.
  • Status bar for displaying hovered links.

The web content can be opened in new tabs or separate windows. HTTP and proxy authentication can be used for accessing web pages.

Running the Example

To run the example from Qt Creator, open the Welcome mode and select the example from Examples. For more information, visit Building and Running an Example.

Class Hierarchy

We start with sketching a diagram of the classes that we are going to implement:

  • Browser is a singleton class managing the application windows.
  • BrowserWindow is a QMainWindow showing the menu, a navigation bar, TabWidget, and a status bar.
  • TabWidget is a QTabWidget and contains one or multiple browser tabs.
  • WebView is a QWebEngineView, provides a view for WebPage, and is added as a tab in TabWidget.
  • WebPage is a QWebEnginePage that represents website content.

Creating the Browser Main Window

This example supports multiple main windows that are owned by a Browser singleton object. This class could also be used for further functionality, such as downloading files, bookmarks, and history managers.

In main.cpp, we create the first BrowserWindow instance and add it to the Browser object. If no arguments are passed on the command line, we open the Qt Homepage:

int main(int argc, char **argv)
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);

    QApplication app(argc, argv);
    app.setWindowIcon(QIcon(QLatin1String(":simplebrowser.svg")));

    QWebEngineSettings::defaultSettings()->setAttribute(QWebEngineSettings::PluginsEnabled, true);

    BrowserWindow *window = new BrowserWindow();
    Browser::instance().addWindow(window);

    const QString url = getCommandLineUrlArgument();
    if (!url.isEmpty())
        window->loadPage(url);
    else
        window->loadHomePage();

    return app.exec();
}

Creating Tabs

The BrowserWindow constructor initializes all the necessary user interface related objects. The centralWidget of BrowserWindow contains an instance of TabWidget. The TabWidget contains one or several WebView instances as tabs, and delegates it's signals and slots to the currently selected one:

class TabWidget : public QTabWidget
{
    ...
signals:
    // current tab/page signals
    void linkHovered(const QString &link);
    void loadProgress(int progress);
    void titleChanged(const QString &title);
    void urlChanged(const QUrl &url);
    void iconChanged(const QIcon &icon);
    void webActionEnabledChanged(QWebEnginePage::WebAction action, bool enabled);

public slots:
    // current tab/page slots
    void setUrl(const QUrl &url);
    void triggerWebPageAction(QWebEnginePage::WebAction action);

    ...
};

Each tab contains an instance of WebView:

WebView *TabWidget::createTab(bool makeCurrent)
{
    WebView *webView = new WebView;
    WebPage *webPage = new WebPage(QWebEngineProfile::defaultProfile(), webView);
    webView->setPage(webPage);
    setupView(webView);
    addTab(webView, tr("(Untitled)"));
    if (makeCurrent)
        setCurrentWidget(webView);
    return webView;
}

In TabWidget::setupView(), we make sure that the TabWidget always forwards the signals of the currently selected WebView:

void TabWidget::setupView(WebView *webView)
{
    QWebEnginePage *webPage = webView->page();

    connect(webView, &QWebEngineView::titleChanged, [this, webView](const QString &title) {
        int index = indexOf(webView);
        if (index != -1)
            setTabText(index, title);
        if (currentIndex() == index)
            emit titleChanged(title);
    });
    connect(webView, &QWebEngineView::urlChanged, [this, webView](const QUrl &url) {
        int index = indexOf(webView);
        if (index != -1)
            tabBar()->setTabData(index, url);
        if (currentIndex() == index)
            emit urlChanged(url);
    });
    connect(webView, &QWebEngineView::loadProgress, [this, webView](int progress) {
        if (currentIndex() == indexOf(webView))
            emit loadProgress(progress);
    });
    ...
}

Implementing WebView Functionality

The WebView is derived from QWebEngineView to support the following functionality:

  • Displaying error messages in case renderProcess dies
  • Handling createWindow requests
  • Adding custom menu items to context menus

First, we create the WebView with the necessary methods and signals:

class WebView : public QWebEngineView
{
    Q_OBJECT

public:
    WebView(QWidget *parent = nullptr);
    ...
protected:
    void contextMenuEvent(QContextMenuEvent *event) override;
    QWebEngineView *createWindow(QWebEnginePage::WebWindowType type) override;

signals:
    void webActionEnabledChanged(QWebEnginePage::WebAction webAction, bool enabled);

    ...
};

Displaying Error Messages

If the render process is terminated, we display a QMessageBox with an error code, and then we reload the page:

WebView::WebView(QWidget *parent)
    : QWebEngineView(parent)
    , m_loadProgress(0)
{
    ...
    connect(this, &QWebEngineView::renderProcessTerminated,
            [this](QWebEnginePage::RenderProcessTerminationStatus termStatus, int statusCode) {
        QString status;
        switch (termStatus) {
        case QWebEnginePage::NormalTerminationStatus:
            status = tr("Render process normal exit");
            break;
        case QWebEnginePage::AbnormalTerminationStatus:
            status = tr("Render process abnormal exit");
            break;
        case QWebEnginePage::CrashedTerminationStatus:
            status = tr("Render process crashed");
            break;
        case QWebEnginePage::KilledTerminationStatus:
            status = tr("Render process killed");
            break;
        }
        QMessageBox::StandardButton btn = QMessageBox::question(window(), status,
                                                   tr("Render process exited with code: %1\n"
                                                      "Do you want to reload the page ?").arg(statusCode));
        if (btn == QMessageBox::Yes)
            QTimer::singleShot(0, [this] { reload(); });
    });
}

Managing WebWindows

The loaded page might want to create windows of the type QWebEnginePage::WebWindowType, for example, when a JavaScript program requests to open a document in a new window or dialog. This is handled by overriding QWebView::createWindow():

QWebEngineView *WebView::createWindow(QWebEnginePage::WebWindowType type)
{
    switch (type) {
    case QWebEnginePage::WebBrowserTab: {
        BrowserWindow *mainWindow = qobject_cast<BrowserWindow*>(window());
        return mainWindow->tabWidget()->createTab();
    }
    case QWebEnginePage::WebBrowserBackgroundTab: {
        BrowserWindow *mainWindow = qobject_cast<BrowserWindow*>(window());
        return mainWindow->tabWidget()->createTab(false);
    }
    case QWebEnginePage::WebBrowserWindow: {
        BrowserWindow *mainWindow = new BrowserWindow();
        Browser::instance().addWindow(mainWindow);
        return mainWindow->currentTab();
    }
    case QWebEnginePage::WebDialog: {
        WebPopupWindow *popup = new WebPopupWindow(page()->profile());
        return popup->view();
    }
    }
    return nullptr;
}

In case of QWebEnginePage::WebDialog, we create an instance of a custom WebPopupWindow class:

class WebPopupWindow : public QWidget
{
    Q_OBJECT

public:
    WebPopupWindow(QWebEngineProfile *profile);
    QWebEngineView *view() const;
    void setUrl(const QUrl &url);

private slots:
    void handleGeometryChangeRequested(const QRect &newGeometry);
    void handleIconChanged(const QIcon &icon);

private:
    UrlLineEdit *m_addressBar;
    WebView *m_view;
};

Adding Context Menu Items

We add menu items to the context menu, so that users can right-click a link to have it opened in the same tab, a new window, or a new tab. We override QWebEngineView::contextMenuEvent and use QWebEnginePage::createStandardContextMenu to create a default QMenu with a default list of QWebEnginePage::WebAction actions.

The default name for QWebEnginePage::OpenLinkInThisWindow action is Follow. For clarity, we rename it Open Link in This Tab. Also, we add the actions for opening links in a separate window or in a new tab:

void WebView::contextMenuEvent(QContextMenuEvent *event)
{
    QMenu *menu = page()->createStandardContextMenu();
    const QList<QAction*> actions = menu->actions();
    auto it = std::find(actions.cbegin(), actions.cend(), page()->action(QWebEnginePage::OpenLinkInThisWindow));
    if (it != actions.cend()) {
        (*it)->setText(tr("Open Link in This Tab"));
        ++it;
        QAction *before(it == actions.cend() ? nullptr : *it);
        menu->insertAction(before, page()->action(QWebEnginePage::OpenLinkInNewWindow));
        menu->insertAction(before, page()->action(QWebEnginePage::OpenLinkInNewTab));
    }
    connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater);
    menu->popup(event->globalPos());
}

Implementing WebPage Functionality

As mentioned earlier, each WebView contains a WebPage instance that was created by using QWebEngineProfile::defaultProfile().

We implement WebPage as a subclass of QWebEnginePage to enable HTTP, proxy authentication, and ignoring SSL certificate errors when accessing web pages:

class WebPage : public QWebEnginePage
{
    Q_OBJECT

public:
    WebPage(QWebEngineProfile *profile, QObject *parent = nullptr);

protected:
    bool certificateError(const QWebEngineCertificateError &error) override;

private slots:
    void handleAuthenticationRequired(const QUrl &requestUrl, QAuthenticator *auth);
    void handleProxyAuthenticationRequired(const QUrl &requestUrl, QAuthenticator *auth, const QString &proxyHost);
};

In all the cases above, we display the appropriate dialog to the user. In case of authentication, we need to set the correct credential values on the QAuthenticator object:

void WebPage::handleAuthenticationRequired(const QUrl &requestUrl, QAuthenticator *auth)
{
    QWidget *mainWindow = view()->window();
    QDialog dialog(mainWindow);
    dialog.setModal(true);
    dialog.setWindowFlags(dialog.windowFlags() & ~Qt::WindowContextHelpButtonHint);

    Ui::PasswordDialog passwordDialog;
    passwordDialog.setupUi(&dialog);

    passwordDialog.m_iconLabel->setText(QString());
    QIcon icon(mainWindow->style()->standardIcon(QStyle::SP_MessageBoxQuestion, 0, mainWindow));
    passwordDialog.m_iconLabel->setPixmap(icon.pixmap(32, 32));

    QString introMessage(tr("Enter username and password for \"%1\" at %2")
                         .arg(auth->realm()).arg(requestUrl.toString().toHtmlEscaped()));
    passwordDialog.m_infoLabel->setText(introMessage);
    passwordDialog.m_infoLabel->setWordWrap(true);

    if (dialog.exec() == QDialog::Accepted) {
        auth->setUser(passwordDialog.m_userNameLineEdit->text());
        auth->setPassword(passwordDialog.m_passwordLineEdit->text());
    } else {
        // Set authenticator null if dialog is cancelled
        *auth = QAuthenticator();
    }
}

The handleProxyAuthenticationRequired signal handler implements the very same steps for the authentication of HTTP proxies.

In case of SSL errors, we just need to return a boolean value indicating whether the certificate should be ignored.

bool WebPage::certificateError(const QWebEngineCertificateError &error)
{
    QWidget *mainWindow = view()->window();
    if (error.isOverridable()) {
        QDialog dialog(mainWindow);
        dialog.setModal(true);
        dialog.setWindowFlags(dialog.windowFlags() & ~Qt::WindowContextHelpButtonHint);
        Ui::CertificateErrorDialog certificateDialog;
        certificateDialog.setupUi(&dialog);
        certificateDialog.m_iconLabel->setText(QString());
        QIcon icon(mainWindow->style()->standardIcon(QStyle::SP_MessageBoxWarning, 0, mainWindow));
        certificateDialog.m_iconLabel->setPixmap(icon.pixmap(32, 32));
        certificateDialog.m_errorLabel->setText(error.errorDescription());
        dialog.setWindowTitle(tr("Certificate Error"));
        return dialog.exec() == QDialog::Accepted;
    }

    QMessageBox::critical(mainWindow, tr("Certificate Error"), error.errorDescription());
    return false;
}

Opening a Web Page

This section describes the workflow for opening a new page. When the user enters a URL in the navigation bar and presses Enter, QLineEdit::returnPressed is emitted, which lets BrowserWindow load the requested page:

    connect(m_urlLineEdit, &QLineEdit::returnPressed, this, [this]() {
        m_urlLineEdit->setFavIcon(QIcon(QStringLiteral(":defaulticon.png")));
        loadPage(m_urlLineEdit->url());
    });

The loadPage() method calls the setUrl() method of TabWidget:

void BrowserWindow::loadPage(const QUrl &url)
{
    if (url.isValid()) {
        m_urlLineEdit->setUrl(url);
        m_tabWidget->setUrl(url);
    }
}

The call is forwarded to the currently selected tab:

void TabWidget::setUrl(const QUrl &url)
{
    if (WebView *view = currentWebView()) {
        view->setUrl(url);
        view->setFocus();
    }
}

The setUrl() method of WebView just forwards the url to the associated WebPage, which in turn starts the downloading of the page's content in the background.

Files:

© 2016 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.