WebEngine小部件简易浏览器示例
这是一个基于Qt WebEngine小部件的简单浏览器。
简易浏览器展示了如何使用Qt WebEngine C++类来开发一个小型Web浏览器应用,它包含以下元素
- 菜单栏,用于打开存储的页面以及管理和窗口、标签。
- 导航栏,用于输入URL以及在网页浏览历史中前后移动。
- 多标签区域,用于在标签内显示网页内容。
- 状态栏,用于显示悬停的链接。
- 一个简单的下载管理器。
网页内容可以在新标签或单独的窗口中打开。可以使用HTTP和代理身份验证来访问网页。
运行示例
要从Qt Creator运行此示例,请打开欢迎模式并从示例中选择示例。更多信息,请访问构建和运行示例。
类层次结构
我们首先制作我们将要实现的 主要类的草稿图
Browser
是一个管理应用程序窗口的类。BrowserWindow
是一个 QMainWindow,显示菜单、导航栏、TabWidget
和状态栏。TabWidget
是一个 QTabWidget,包含一个或多个浏览器标签。WebView
是一个 QWebEngineView,提供对WebPage
的视图,并将其添加到TabWidget
中。WebPage
是一个 QWebEnginePage,代表网站内容。
此外,我们还将实现一些辅助类
创建浏览器主窗口
此示例支持多个主窗口,这些窗口由 Browser
对象拥有。此类还拥有 DownloadManagerWidget
,可以用于进一步的功能,例如书签和历史记录管理器。
在 main.cpp
中,我们创建第一个 BrowserWindow
实例并将其添加到 Browser
对象中。如果没有从命令行传递参数,我们将打开Qt 首页
int main(int argc, char **argv) { QCoreApplication::setOrganizationName("QtExamples"); QApplication app(argc, argv); app.setWindowIcon(QIcon(u":AppLogoColor.png"_s)); QLoggingCategory::setFilterRules(u"qt.webenginecontext.debug=true"_s); QWebEngineProfile::defaultProfile()->settings()->setAttribute(QWebEngineSettings::PluginsEnabled, true); QWebEngineProfile::defaultProfile()->settings()->setAttribute(QWebEngineSettings::DnsPrefetchEnabled, true); QWebEngineProfile::defaultProfile()->settings()->setAttribute( QWebEngineSettings::ScreenCaptureEnabled, true); QUrl url = commandLineUrlArgument(); Browser browser; BrowserWindow *window = browser.createHiddenWindow(); window->tabWidget()->setUrl(url); window->show(); return app.exec(); }
为了减少在切换窗口到OpenGL渲染时闪烁,我们在第一个浏览器标签添加后调用 show。
创建标签
《BrowserWindow》构造函数初始化所有必要的用户界面相关对象。《BrowserWindow》的《centralWidget》包含一个《TabWidget》实例。《TabWidget》包含一个或多个作为标签页的《WebView》实例,并将信号和槽委托给当前所选的实例。
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); void devToolsRequested(QWebEnginePage *source); void findTextFinished(const QWebEngineFindTextResult &result); public slots: // current tab/page slots void setUrl(const QUrl &url); void triggerWebPageAction(QWebEnginePage::WebAction action); ... };
每个标签页都包含一个《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()); // Workaround for QTBUG-61770 webView->resize(currentWidget()->size()); webView->show(); return webView; }
在《TabWidget::setupView()`中,我们确保《TabWidget》始终将当前所选《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); }); connect(webPage, &QWebEnginePage::linkHovered, [this, webView](const QString &url) { if (currentIndex() == indexOf(webView)) emit linkHovered(url); }); connect(webView, &WebView::favIconChanged, [this, webView](const QIcon &icon) { int index = indexOf(webView); if (index != -1) setTabIcon(index, icon); if (currentIndex() == index) emit favIconChanged(icon); }); connect(webView, &WebView::webActionEnabledChanged, [this, webView](QWebEnginePage::WebAction action, bool enabled) { if (currentIndex() == indexOf(webView)) emit webActionEnabledChanged(action,enabled); }); connect(webPage, &QWebEnginePage::windowCloseRequested, [this, webView]() { int index = indexOf(webView); if (webView->page()->inspectedPage()) window()->close(); else if (index >= 0) closeTab(index); }); connect(webView, &WebView::devToolsRequested, this, &TabWidget::devToolsRequested); connect(webPage, &QWebEnginePage::findTextFinished, [this, webView](const QWebEngineFindTextResult &result) { if (currentIndex() == indexOf(webView)) emit findTextFinished(result); }); }
实现 WebView 功能
《WebView》是从QWebEngineView派生出来的,以支持以下功能
- 在《renderProcess》死亡的情况下显示错误消息
- 处理《createWindow》请求
- 向上下文菜单添加自定义菜单项
首先,我们创建带有必要的方法和信号的WebView。
class WebView : public QWebEngineView { Q_OBJECT public: explicit WebView(QWidget *parent = nullptr); ... protected: void contextMenuEvent(QContextMenuEvent *event) override; QWebEngineView *createWindow(QWebEnginePage::WebWindowType type) override; signals: void webActionEnabledChanged(QWebEnginePage::WebAction webAction, bool enabled); ... };
显示错误消息
如果渲染进程终止,我们显示一个带有错误代码的QMessageBox,然后重新加载页面
WebView::WebView(QWidget *parent) : QWebEngineView(parent) { ... 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, &WebView::reload); }); }
管理 WebWindows
加载的页面可能希望创建QWebEnginePage::WebWindowType类型的窗口,例如,当JavaScript程序请求在新窗口或对话框中打开文档时。这通过覆盖《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(); }
对于《QWebEnginePage::WebDialog》,我们创建一个自定义的《WebPopupWindow》类实例。
class WebPopupWindow : public QWidget { Q_OBJECT public: explicit WebPopupWindow(QWebEngineProfile *profile); WebView *view() const; private slots: void handleGeometryChangeRequested(const QRect &newGeometry); private: QLineEdit *m_urlLineEdit; QAction *m_favAction; WebView *m_view; };
添加上下文菜单项
我们在上下文菜单中添加一个菜单项,以便用户可以右键点击在新窗口中打开检查器。我们重写了《QWebEngineView::contextMenuEvent》并使用QWebEnginePage::createStandardContextMenu创建一个带有默认QWebEnginePage::WebAction操作的默认QMenu。
默认名称为QWebEnginePage::InspectElement操作是检查。为了清楚,当我们还没有检查器时,我们将它重命名为在新窗口中打开检查器,当它已经创建时重命名为检查元素。
我们还检查QWebEnginePage::ViewSource操作是否在菜单中,因为如果不在,我们还需要添加一个分隔符。
void WebView::contextMenuEvent(QContextMenuEvent *event) { QMenu *menu = createStandardContextMenu(); const QList<QAction *> actions = menu->actions(); auto inspectElement = std::find(actions.cbegin(), actions.cend(), page()->action(QWebEnginePage::InspectElement)); if (inspectElement == actions.cend()) { auto viewSource = std::find(actions.cbegin(), actions.cend(), page()->action(QWebEnginePage::ViewSource)); if (viewSource == actions.cend()) menu->addSeparator(); QAction *action = menu->addAction("Open inspector in new window"); connect(action, &QAction::triggered, [this]() { emit devToolsRequested(page()); }); } else { (*inspectElement)->setText(tr("Inspect element")); } menu->popup(event->globalPos()); }
实现 WebPage 和 WebView 功能
我们将《WebPage》实现为一个继承自QWebEnginePage的子类,将《WebView》实现为一个继承自QWebEngineView的子类,以启用HTTP,代理身份验证以及在不访问网页时忽略SSL证书错误。
class WebPage : public QWebEnginePage { Q_OBJECT public: explicit WebPage(QWebEngineProfile *profile, QObject *parent = nullptr); signals: void createCertificateErrorDialog(QWebEngineCertificateError error); private slots: void handleCertificateError(QWebEngineCertificateError error); void handleSelectClientCertificate(QWebEngineClientCertificateSelection clientCertSelection); void handleDesktopMediaRequest(const QWebEngineDesktopMediaRequest &request); }; class WebView : public QWebEngineView { Q_OBJECT public: explicit WebView(QWidget *parent = nullptr); void setPage(WebPage *page); int loadProgress() const; bool isWebActionEnabled(QWebEnginePage::WebAction webAction) const; QIcon favIcon() const; protected: void contextMenuEvent(QContextMenuEvent *event) override; QWebEngineView *createWindow(QWebEnginePage::WebWindowType type) override; signals: void webActionEnabledChanged(QWebEnginePage::WebAction webAction, bool enabled); void favIconChanged(const QIcon &icon); void devToolsRequested(QWebEnginePage *source); private slots: void handleCertificateError(QWebEngineCertificateError error); void handleAuthenticationRequired(const QUrl &requestUrl, QAuthenticator *auth); void handleFeaturePermissionRequested(const QUrl &securityOrigin, QWebEnginePage::Feature feature); void handleProxyAuthenticationRequired(const QUrl &requestUrl, QAuthenticator *auth, const QString &proxyHost); void handleRegisterProtocolHandlerRequested(QWebEngineRegisterProtocolHandlerRequest request); #if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) void handleFileSystemAccessRequested(QWebEngineFileSystemAccessRequest request); void handleWebAuthUxRequested(QWebEngineWebAuthUxRequest *request); #endif private: void createWebActionTrigger(QWebEnginePage *page, QWebEnginePage::WebAction); void onStateChanged(QWebEngineWebAuthUxRequest::WebAuthUxState state); private: int m_loadProgress = 100; WebAuthDialog *m_authDialog = nullptr; };
在所有上述情况下,我们都会向用户显示适当的对话框。在身份验证的情况下,我们需要在QAuthenticator对象上设置正确的凭证值。
void WebView::handleAuthenticationRequired(const QUrl &requestUrl, QAuthenticator *auth) { QDialog dialog(window()); dialog.setModal(true); dialog.setWindowFlags(dialog.windowFlags() & ~Qt::WindowContextHelpButtonHint); Ui::PasswordDialog passwordDialog; passwordDialog.setupUi(&dialog); passwordDialog.m_iconLabel->setText(QString()); QIcon icon(window()->style()->standardIcon(QStyle::SP_MessageBoxQuestion, 0, window())); passwordDialog.m_iconLabel->setPixmap(icon.pixmap(32, 32)); QString introMessage(tr("Enter username and password for \"%1\" at %2") .arg(auth->realm(), 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(); } }
《handleProxyAuthenticationRequired》信号处理程序实现了HTTP代理身份验证的相同步骤。
在SSL错误的情况下,我们只需返回一个布尔值,表示是否应该忽略证书。
void WebPage::handleCertificateError(QWebEngineCertificateError error) { error.defer(); QTimer::singleShot(0, this, [this, error]() mutable { emit createCertificateErrorDialog(error); }); }
打开网页
BrowserWindow::BrowserWindow(Browser *browser, QWebEngineProfile *profile, bool forDevTools) { ... connect(m_urlLineEdit, &QLineEdit::returnPressed, [this]() { m_tabWidget->setUrl(QUrl::fromUserInput(m_urlLineEdit->text())); }); ... }
void TabWidget::setUrl(const QUrl &url) { if (WebView *view = currentWebView()) { view->setUrl(url); view->setFocus(); } }
class Browser { public: ... BrowserWindow *createHiddenWindow(bool offTheRecord = false); BrowserWindow *createWindow(bool offTheRecord = false); private: ... QScopedPointer<QWebEngineProfile> m_profile; };
BrowserWindow *Browser::createHiddenWindow(bool offTheRecord) { if (!offTheRecord && !m_profile) { const QString name = u"simplebrowser."_s + QLatin1StringView(qWebEngineChromiumVersion()); m_profile.reset(new QWebEngineProfile(name)); ...
... QObject::connect(m_profile.get(), &QWebEngineProfile::downloadRequested, &m_downloadManagerWidget, &DownloadManagerWidget::downloadRequested); } auto profile = !offTheRecord ? m_profile.get() : QWebEngineProfile::defaultProfile(); auto mainWindow = new BrowserWindow(this, profile, false); return mainWindow; }
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); }
void DownloadManagerWidget::downloadRequested(QWebEngineDownloadRequest *download) { Q_ASSERT(download && download->state() == QWebEngineDownloadRequest::DownloadRequested); QString path = QFileDialog::getSaveFileName(this, tr("Save as"), QDir(download->downloadDirectory()).filePath(download->downloadFileName())); if (path.isEmpty()) return; download->setDownloadDirectory(QFileInfo(path).path()); download->setDownloadFileName(QFileInfo(path).fileName()); download->accept(); add(new DownloadWidget(download)); show(); }
connect(page, &QWebEnginePage::webAuthUxRequested, this, &WebView::handleWebAuthUxRequested);
void WebView::handleWebAuthUxRequested(QWebEngineWebAuthUxRequest *request) { if (m_authDialog) delete m_authDialog; m_authDialog = new WebAuthDialog(request, window()); m_authDialog->setModal(false); m_authDialog->setWindowFlags(m_authDialog->windowFlags() & ~Qt::WindowContextHelpButtonHint); connect(request, &QWebEngineWebAuthUxRequest::stateChanged, this, &WebView::onStateChanged); m_authDialog->show(); }
QWebEngineWebAuthUxRequest 对象会定期发出 stateChanged 信号来通知潜在的观察者当前的 WebAuth UX 状态。观察者会相应地更新 WebAuth 对话框。有关如何处理这些信号的示例,请参阅 webview.cpp
和 webauthdialog.cpp
。
macOS 的签名要求
当在 macOS 上运行 Simple Browser 时,为了让网站能够访问位置、摄像头和麦克风,应用程序需要签名。这是在构建时自动完成的,但您需要为构建环境设置有效的签名身份。
文件和归属
本示例使用的是 Tango 图标库中的图标
Tango 图标库 | 版权免费 |
© 2024 Qt 公司有限公司。本文件中包含的文档贡献版权属于各自的拥有者。本文件中提供的文档是根据自由软件基金会发布的 GNU 自由文档许可证版本 1.3 的条款许可的。Qt 和相应的标志是芬兰及/或全球其他地区的 Qt 公司的商标。所有其他商标均为各自所有权者的财产。