WebEngine Quick Nano 浏览器
使用 WebEngineView QML 类型实现的网页浏览器。
《Quick Nano 浏览器》演示了如何使用 Qt WebEngine QML 类型 来开发一个小型网页浏览器应用程序,该应用程序包含一个具有标题栏、工具栏、标签视图和状态栏的浏览器窗口。网页内容在标签视图内的网页引擎视图中加载。如果发生证书错误,将提示用户在消息对话框中进行操作。状态栏弹出以显示悬停链接的 URL。
网页可以下发一个请求以在全屏模式下显示。用户可以通过使用工具栏按钮允许全屏模式。他们可以通过使用键盘快捷键离开全屏模式。额外的工具栏按钮可启用在浏览器历史记录中前后移动、重新加载标签内容以及打开设置菜单以启用以下功能:JavaScript、插件、全屏模式、无记录、HTTP 磁盘缓存、自动加载图像和忽略证书错误。
运行示例
要从 Qt Creator 运行示例,请打开 欢迎 模式并从 示例 中选择示例。有关更多信息,请访问 构建和运行示例。
创建主浏览器窗口
当浏览器主窗口加载时,它使用默认配置创建一个空标签。每个标签都是一个填充主窗口的网页引擎视图。
我们在 BrowserWindow.qml 文件中使用 ApplicationWindow 类型创建主窗口
ApplicationWindow { id: browserWindow property QtObject applicationRoot property Item currentWebView: tabBar.currentIndex < tabBar.count ? tabLayout.children[tabBar.currentIndex] : null ... width: 1300 height: 900 visible: true title: currentWebView && currentWebView.title
我们使用 TabBar Qt Quick 控件在窗口顶部创建标签栏,并创建一个新的空标签
TabBar { id: tabBar anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right Component.onCompleted: createTab(defaultProfile) function createTab(profile, focusOnNewTab = true, url = undefined) { var webview = tabComponent.createObject(tabLayout, {profile: profile}); var newTabButton = tabButtonComponent.createObject(tabBar, {tabTitle: Qt.binding(function () { return webview.title; })}); tabBar.addItem(newTabButton); if (focusOnNewTab) { tabBar.setCurrentIndex(tabBar.count - 1); }
标签包含一个网页引擎视图,该视图加载网页内容
Component { id: tabComponent WebEngineView { id: webEngineView focus: true onLinkHovered: function(hoveredUrl) { if (hoveredUrl == "") hideStatusText.start(); else { statusText.text = hoveredUrl; statusBubble.visible = true; hideStatusText.stop(); } } states: [ State { name: "FullScreen" PropertyChanges { target: tabBar visible: false height: 0 } PropertyChanges { target: navigationBar visible: false } } ] settings.localContentCanAccessRemoteUrls: true settings.localContentCanAccessFileUrls: false settings.autoLoadImages: appSettings.autoLoadImages settings.javascriptEnabled: appSettings.javaScriptEnabled settings.errorPageEnabled: appSettings.errorPageEnabled settings.pluginsEnabled: appSettings.pluginsEnabled settings.fullScreenSupportEnabled: appSettings.fullScreenSupportEnabled settings.autoLoadIconsForPage: appSettings.autoLoadIconsForPage settings.touchIconsEnabled: appSettings.touchIconsEnabled settings.webRTCPublicInterfacesOnly: appSettings.webRTCPublicInterfacesOnly settings.pdfViewerEnabled: appSettings.pdfViewerEnabled onCertificateError: function(error) { error.defer(); sslDialog.enqueue(error); } onNewWindowRequested: function(request) { if (!request.userInitiated) console.warn("Blocked a popup window."); else if (request.destination === WebEngineNewWindowRequest.InNewTab) { var tab = tabBar.createTab(currentWebView.profile, true, request.requestedUrl); tab.acceptAsNewWindow(request); } else if (request.destination === WebEngineNewWindowRequest.InNewBackgroundTab) { var backgroundTab = tabBar.createTab(currentWebView.profile, false); backgroundTab.acceptAsNewWindow(request); } else if (request.destination === WebEngineNewWindowRequest.InNewDialog) { var dialog = applicationRoot.createDialog(currentWebView.profile); dialog.currentWebView.acceptAsNewWindow(request); } else { var window = applicationRoot.createWindow(currentWebView.profile); window.currentWebView.acceptAsNewWindow(request); } } onFullScreenRequested: function(request) { if (request.toggleOn) { webEngineView.state = "FullScreen"; browserWindow.previousVisibility = browserWindow.visibility; browserWindow.showFullScreen(); fullScreenNotification.show(); } else { webEngineView.state = ""; browserWindow.visibility = browserWindow.previousVisibility; fullScreenNotification.hide(); } request.accept(); } onRegisterProtocolHandlerRequested: function(request) { console.log("accepting registerProtocolHandler request for " + request.scheme + " from " + request.origin); request.accept(); } onRenderProcessTerminated: function(terminationStatus, exitCode) { var status = ""; switch (terminationStatus) { case WebEngineView.NormalTerminationStatus: status = "(normal exit)"; break; case WebEngineView.AbnormalTerminationStatus: status = "(abnormal exit)"; break; case WebEngineView.CrashedTerminationStatus: status = "(crashed)"; break; case WebEngineView.KilledTerminationStatus: status = "(killed)"; break; } print("Render process exited with code " + exitCode + " " + status); reloadTimer.running = true; } onSelectClientCertificate: function(selection) { selection.certificates[0].select(); } onFindTextFinished: function(result) { if (!findBar.visible) findBar.visible = true; findBar.numberOfMatches = result.numberOfMatches; findBar.activeMatch = result.activeMatch; } onLoadingChanged: function(loadRequest) { if (loadRequest.status == WebEngineView.LoadStartedStatus) findBar.reset(); } onFeaturePermissionRequested: function(securityOrigin, feature) { featurePermissionDialog.securityOrigin = securityOrigin; featurePermissionDialog.feature = feature; featurePermissionDialog.visible = true; } onWebAuthUxRequested: function(request) { webAuthDialog.init(request); } Timer { id: reloadTimer interval: 0 running: false repeat: false onTriggered: currentWebView.reload() } } }
我们使用 Action 类型创建新的标签
Action { shortcut: StandardKey.AddTab onTriggered: { tabBar.createTab(tabBar.count != 0 ? currentWebView.profile : defaultProfile); addressBar.forceActiveFocus(); addressBar.selectAll(); }
我们使用位于 ToolBar 中的 TextField Qt Quick 控件创建地址栏,该地址栏显示当前 URL 并允许用户输入另一个 URL
menuBar: ToolBar { id: navigationBar RowLayout { anchors.fill: parent ... TextField { id: addressBar ... focus: true Layout.fillWidth: true Binding on text { when: currentWebView value: currentWebView.url } onAccepted: currentWebView.url = Utils.fromUserInput(text) selectByMouse: true }
处理证书错误
如果正在加载的网站的证书触发证书错误,我们将调用 QML 方法 defer() 来暂停 URL 请求并等待用户输入
onCertificateError: function(error) { error.defer(); sslDialog.enqueue(error); }
我们使用对话框类型来提示用户是否继续或取消加载网页。如果用户选择 是,我们将调用 acceptCertificate() 方法来继续从URL加载内容。如果用户选择 否,我们将调用 rejectCertificate() 方法拒绝请求并停止从URL加载内容
Dialog { id: sslDialog anchors.centerIn: parent contentWidth: Math.max(mainTextForSSLDialog.width, detailedTextForSSLDialog.width) contentHeight: mainTextForSSLDialog.height + detailedTextForSSLDialog.height property var certErrors: [] // fixme: icon! // icon: StandardIcon.Warning standardButtons: Dialog.No | Dialog.Yes title: "Server's certificate not trusted" contentItem: Item { Label { id: mainTextForSSLDialog text: "Do you wish to continue?" } Text { id: detailedTextForSSLDialog anchors.top: mainTextForSSLDialog.bottom text: "If you wish so, you may continue with an unverified certificate.\n" + "Accepting an unverified certificate means\n" + "you may not be connected with the host you tried to connect to.\n" + "Do you wish to override the security check and continue?" } } onAccepted: { certErrors.shift().acceptCertificate(); presentError(); } onRejected: reject() function reject(){ certErrors.shift().rejectCertificate(); presentError(); } function enqueue(error){ certErrors.push(error); presentError(); } function presentError(){ visible = certErrors.length > 0 } }
处理功能权限请求
我们使用 onFeaturePermissionRequested()
信号处理器来处理访问特定功能或设备的请求。参数 securityOrigin
识别请求的网站,参数 feature
是请求的功能。我们使用这些信息来构造对话框消息
onFeaturePermissionRequested: function(securityOrigin, feature) { featurePermissionDialog.securityOrigin = securityOrigin; featurePermissionDialog.feature = feature; featurePermissionDialog.visible = true; }
我们显示一个对话框,询问用户是允许还是拒绝访问。自定义 questionForFeature()
JavaScript 函数生成关于请求的人可读问题。如果用户选择 是,我们使用带有第三个参数 true
的 grantFeaturePermission() 方法,授予 securityOrigin
网站访问 feature
的权限。如果用户选择 否,我们使用带有参数 false
的同一方法拒绝访问
Dialog { id: featurePermissionDialog anchors.centerIn: parent width: Math.min(browserWindow.width, browserWindow.height) / 3 * 2 contentWidth: mainTextForPermissionDialog.width contentHeight: mainTextForPermissionDialog.height standardButtons: Dialog.No | Dialog.Yes title: "Permission Request" property var feature; property url securityOrigin; contentItem: Item { Label { id: mainTextForPermissionDialog text: featurePermissionDialog.questionForFeature() } } onAccepted: currentWebView && currentWebView.grantFeaturePermission(securityOrigin, feature, true) onRejected: currentWebView && currentWebView.grantFeaturePermission(securityOrigin, feature, false) onVisibleChanged: { if (visible) width = contentWidth + 20; } function questionForFeature() { var question = "Allow " + securityOrigin + " to " switch (feature) { case WebEngineView.Geolocation: question += "access your location information?"; break; case WebEngineView.MediaAudioCapture: question += "access your microphone?"; break; case WebEngineView.MediaVideoCapture: question += "access your webcam?"; break; case WebEngineView.MediaVideoCapture: question += "access your microphone and webcam?"; break; case WebEngineView.MouseLock: question += "lock your mouse cursor?"; break; case WebEngineView.DesktopVideoCapture: question += "capture video of your desktop?"; break; case WebEngineView.DesktopAudioVideoCapture: question += "capture audio and video of your desktop?"; break; case WebEngineView.Notifications: question += "show notification on your desktop?"; break; default: question += "access unknown or unsupported feature [" + feature + "] ?"; break; } return question; } }
进入和退出全屏模式
我们在工具栏上的设置菜单中创建一个允许全屏模式的菜单项。此外,我们通过使用键盘快捷键创建一个退出全屏模式的行为。我们调用 accept() 方法来接受全屏请求。此方法将 isFullScreen 属性设置与 toggleOn 属性相等。
onFullScreenRequested: function(request) { if (request.toggleOn) { webEngineView.state = "FullScreen"; browserWindow.previousVisibility = browserWindow.visibility; browserWindow.showFullScreen(); fullScreenNotification.show(); } else { webEngineView.state = ""; browserWindow.visibility = browserWindow.previousVisibility; fullScreenNotification.hide(); } request.accept(); }
进入全屏模式时,我们使用在 FullScreenNotification.qml 中创建的 FullScreenNotification 自定义类型来显示通知。
我们在设置菜单中使用了 Action 类型,通过按下Esc键来创建退出全屏模式的快捷键。
Settings { id : appSettings property alias fullScreenSupportEnabled: fullScreenSupportEnabled.checked property alias autoLoadIconsForPage: autoLoadIconsForPage.checked property alias touchIconsEnabled: touchIconsEnabled.checked property alias webRTCPublicInterfacesOnly : webRTCPublicInterfacesOnly.checked property alias devToolsEnabled: devToolsEnabled.checked property alias pdfViewerEnabled: pdfViewerEnabled.checked } Action { shortcut: "Escape" onTriggered: { if (currentWebView.state == "FullScreen") { browserWindow.visibility = browserWindow.previousVisibility; fullScreenNotification.hide(); currentWebView.triggerWebAction(WebEngineView.ExitFullScreen); } if (findBar.visible) findBar.visible = false; } }
处理 WebAuth/FIDO UX 请求
我们使用 onWebAuthUxRequested()
信号处理器来处理 WebAuth/FIDO UX 的请求。参数 request
是一个包含 UX 请求详情和应用程序处理此请求所需的 API 的 WebEngineWebAuthUxRequest 实例。我们使用它来构建 WebAuthUX 对话框并启动 UX 请求流程。
onWebAuthUxRequested: function(request) { webAuthDialog.init(request); }
WebEngineWebAuthUxRequest 对象定期发出 stateChanged 信号,以通知潜在的观察者当前的 WebAuth UX 状态。观察者相应地更新 WebAuth 对话框。我们使用 onStateChanged() 信号处理器来处理状态变化请求。请参阅 WebAuthDialog.qml
中的示例,了解如何处理这些信号。
Connections { id: webauthConnection ignoreUnknownSignals: true function onStateChanged(state) { webAuthDialog.setupUI(state); } function init(request) { pinLabel.visible = false; pinEdit.visible = false; confirmPinLabel.visible = false; confirmPinEdit.visible = false; selectAccountModel.clear(); webAuthDialog.authrequest = request; webauthConnection.target = request; setupUI(webAuthDialog.authrequest.state) webAuthDialog.visible = true; pinEntryError.visible = false; }
macOS 的签名要求
为了在 macOS 上运行 Quick Nano Browser 时允许网站访问位置、摄像头和麦克风,应用程序需要经过签名。在构建过程中自动完成,但您需要为构建环境设置有效的签名身份。
文件和归功于
本例使用了来自Tango图标库的图标
Tango图标库 | 公有领域 |
© 2024 Qt公司有限公司。本文件中包含的文档贡献均为各自所有者的版权。本文件中提供的文档根据由自由软件基金会发布的GNU自由文档许可证1.3版条款授权。Qt及其相关商标为Qt公司有限公司在芬兰及全球其他国家的商标。所有其他商标均为其各自所有者的财产。