WebEngine Widgets 简单浏览器范例

简单浏览器 演示如何使用 Qt WebEngine C++ 类 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.
  • A simple download manager.

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

运行范例

要运行范例从 Qt Creator ,打开 欢迎 模式,然后选择范例从 范例 。更多信息,拜访 构建和运行范例 .

类层次结构

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

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

Additionally, we will implement some auxiliary classes:

  • WebPopupWindow QWidget for showing popup windows.
  • DownloadManagerWidget QWidget implementing the downloads list.

创建浏览器主窗口

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

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);
    QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
    QApplication app(argc, argv);
    app.setWindowIcon(QIcon(QStringLiteral(":AppLogoColor.png")));
    QWebEngineSettings::defaultSettings()->setAttribute(QWebEngineSettings::PluginsEnabled, true);
    QUrl url = getCommandLineUrlArgument();
    if (!url.isValid())
        url = QStringLiteral("https://www.qt.io");
    Browser browser;
    BrowserWindow *window = browser.createWindow();
    window->currentTab()->setUrl(url);
    return app.exec();
}
					
					

创建选项卡

The BrowserWindow constructor initializes all the necessary user interface related objects. The centralWidget of BrowserWindow contains an instance of TabWidget 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 favIconChanged(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()
{
    WebView *webView = createBackgroundTab();
    setCurrentWidget(webView);
    return webView;
}
WebView *TabWidget::createBackgroundTab()
{
    WebView *webView = new WebView;
    WebPage *webPage = new WebPage(m_profile, webView);
    webView->setPage(webPage);
    setupView(webView);
    int index = addTab(webView, tr("(Untitled)"));
    setTabIcon(index, webView->favIcon());
    return webView;
}
					

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);
            setTabToolTip(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);
    });
    ...
}
					
					

实现 WebView 功能

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);
    ...
};
					

显示错误消息

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(100)
{
    ...
    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(); });
    });
}
					

管理 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)
{
    BrowserWindow *mainWindow = qobject_cast<BrowserWindow*>(window());
    if (!mainWindow)
        return nullptr;
    switch (type) {
    case QWebEnginePage::WebBrowserTab: {
        return mainWindow->tabWidget()->createTab();
    }
					

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

class WebPopupWindow : public QWidget
{
    Q_OBJECT
public:
    WebPopupWindow(QWebEngineProfile *profile);
    WebView *view() const;
private slots:
    void handleGeometryChangeRequested(const QRect &newGeometry);
private:
    QLineEdit *m_urlLineEdit;
    QAction *m_favAction;
    WebView *m_view;
};
					

添加上下文菜单项

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));
    }
    menu->popup(event->globalPos());
}
					
					

实现 WebPage 功能

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 对象:

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;
}
					
					

打开网页

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 signal is emitted and the new URL is then handed over to TabWidget::setUrl :

BrowserWindow::BrowserWindow(Browser *browser, QWebEngineProfile *profile)
{
    ...
    connect(m_urlLineEdit, &QLineEdit::returnPressed, [this]() {
        m_tabWidget->setUrl(QUrl::fromUserInput(m_urlLineEdit->text()));
    });
    ...
}
					

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() 方法为 WebView just forwards the url to the associated WebPage , which in turn starts the downloading of the page's content in the background.

实现隐私浏览

Private browsing , incognito mode ,或 off-the-record mode is a feature of many browsers where normally persistent data, such as cookies, the HTTP cache, or browsing history, is kept only in memory, leaving no trace on disk. In this example we will implement private browsing on the window level with tabs in one window all in either normal or private mode. Alternatively we could implement private browsing on the tab-level, with some tabs in a window in normal mode, others in private mode.

Implementing private browsing is quite easy using Qt WebEngine. All one has to do is to create a new QWebEngineProfile and use it in the QWebEnginePage instead of the default profile. In the example this new profile is created and owned by the Browser 对象:

class Browser
{
public:
    ...
    BrowserWindow *createWindow(bool offTheRecord = false);
private:
    ...
    QWebEngineProfile m_otrProfile;
};
					

The default constructor for QWebEngineProfile already puts it in off-the-record mode. All that is left to do is to pass the appropriate profile down to the appropriate QWebEnginePage objects. The Browser object will hand to each new BrowserWindow either the global default profile (see QWebEngineProfile::defaultProfile ) or its own off-the-record profile:

BrowserWindow *Browser::createWindow(bool offTheRecord)
{
    auto profile = offTheRecord ? &m_otrProfile : QWebEngineProfile::defaultProfile();
    auto mainWindow = new BrowserWindow(this, profile);
    return mainWindow;
}
					

The BrowserWindow and TabWidget objects will then ensure that all QWebEnginePage objects contained in a window will use this profile.

管理下载

Downloads are associated with a QWebEngineProfile . Whenever a download is triggered on a web page the QWebEngineProfile::downloadRequested signal is emitted with a QWebEngineDownloadItem , which in this example is forwarded to DownloadManagerWidget::downloadRequested :

Browser::Browser()
{
    // Quit application if the download manager window is the only remaining window
    m_downloadManagerWidget.setAttribute(Qt::WA_QuitOnClose, false);
    QObject::connect(
        QWebEngineProfile::defaultProfile(), &QWebEngineProfile::downloadRequested,
        &m_downloadManagerWidget, &DownloadManagerWidget::downloadRequested);
    QObject::connect(
        &m_otrProfile, &QWebEngineProfile::downloadRequested,
        &m_downloadManagerWidget, &DownloadManagerWidget::downloadRequested);
}
					

This method prompts the user for a file name (with a pre-filled suggestion) and starts the download (unless the user cancels the 另存为 dialog):

void DownloadManagerWidget::downloadRequested(QWebEngineDownloadItem *download)
{
    Q_ASSERT(download && download->state() == QWebEngineDownloadItem::DownloadRequested);
    QString path = QFileDialog::getSaveFileName(this, tr("Save as"), download->path());
    if (path.isEmpty())
        return;
    download->setPath(path);
    download->accept();
    add(new DownloadWidget(download));
    show();
}
					

The QWebEngineDownloadItem object will periodically emit the downloadProgress signal to notify potential observers of the download progress and the stateChanged signal when the download is finished or when an error occurs. See downloadmanagerwidget.cpp for an example of how these signals can be handled.

Licensing

All icons used in the example, with the exception of AppLogoColor.png , originate from the public domain Tango 图标库 .

文件: