实现 Web 浏览器使用 WebEngineView QML 类型。
Quick 纳米浏览器 演示如何使用 Qt WebEngine QML types to develop a small web browser application that consists of a browser window with a title bar, toolbar, tab view, and status bar. The web content is loaded in a web engine view within the tab view. If certificate errors occur, users are prompted for action in a message dialog. The status bar pops up to display the URL of a hovered link.
A web page can issue a request for being displayed in fullscreen mode. Users can allow full screen mode by using a toolbar button. They can leave fullscreen mode by using a keyboard shortcut. Additional toolbar buttons enable moving backwards and forwards in the browser history, reloading tab content, and opening a settings menu for enabling the following features: JavaScript, plugins, fullscreen mode, off the record, HTTP disk cache, autoloading images, and ignoring certificate errors.
要运行范例从 Qt Creator ,打开 欢迎 模式,然后选择范例从 范例 。更多信息,拜访 构建和运行范例 .
When the browser main window is loaded, it creates an empty tab using the default profile. Each tab is a web engine view that fills the main window.
We create the main window in the 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 control to create a tab bar anchored to the top of the window, and create a new, empty tab:
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);
}
The tab contains a web engine view that loads web content:
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()
}
}
}
使用 动作 type to create new tabs:
Action {
shortcut: StandardKey.AddTab
onTriggered: {
tabBar.createTab(tabBar.count != 0 ? currentWebView.profile : defaultProfile);
addressBar.forceActiveFocus();
addressBar.selectAll();
}
使用 TextField Qt Quick Control within a ToolBar to create an address bar that shows the current URL and where users can enter another 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
}
If the certificate of the site being loaded triggers a certificate error, we call the defer () QML method to pause the URL request and wait for user input:
onCertificateError: function(error) {
error.defer();
sslDialog.enqueue(error);
}
We use the Dialog type to prompt users to continue or cancel the loading of the web page. If users select Yes ,我们调用 acceptCertificate () method to continue loading content from the URL. If users select No ,我们调用 rejectCertificate () method to reject the request and stop loading content from the 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()
signal handler to handle requests for accessing a certain feature or device. The
securityOrigin
parameter identifies the requester web site, and the
feature
parameter is the requested feature. We use these to construct the message of the dialog:
onFeaturePermissionRequested: function(securityOrigin, feature) {
featurePermissionDialog.securityOrigin = securityOrigin;
featurePermissionDialog.feature = feature;
featurePermissionDialog.visible = true;
}
We show a dialog where the user is asked to grant or deny access. The custom
questionForFeature()
JavaScript function generates a human-readable question about the request. If users select
Yes
,我们调用
grantFeaturePermission
() method with a third
true
parameter to grant the
securityOrigin
web site the permission to access the
feature
. If users select
No
, we call the same method but with the
false
parameter to deny access:
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;
}
}
We create a menu item for allowing fullscreen mode in a settings menu that we place on the tool bar. Also, we create an action for leaving fullscreen mode by using a keyboard shortcut. We call the accept () method to accept the fullscreen request. The methdod sets the isFullScreen property to be equal to the 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();
}
When entering fullscreen mode, we display a notification using the FullScreenNotification custom type that we create in FullScreenNotification.qml .
使用 动作 type in the settings menu to create a shortcut for leaving fullscreen mode by pressing the escape key:
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;
}
}
使用
onWebAuthUxRequested()
signal handler to handle requests for WebAuth/FIDO UX. The
request
parameter is an instance of
WebEngineWebAuthUxRequest
which contains UX request details and APIs required to process the request. We use it to construct WebAuthUX dialog and initiates the UX request flow.
onWebAuthUxRequested: function(request) {
webAuthDialog.init(request);
}
The
WebEngineWebAuthUxRequest
object periodically emits the
stateChanged
signal to notify potential observers of the current WebAuth UX states. The observers update the WebAuth dialog accordingly. We use onStateChanged() signal handler to handle state change requests. See
WebAuthDialog.qml
for an example of how these signals can be handled.
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;
}
To allow web sites access to the location, camera, and microphone when running Quick 纳米浏览器 on macOS, the application needs to be signed. This is done automatically when building, but you need to set up a valid signing identity for the build environment.
范例使用的图标来自 Tango 图标库:
| Tango 图标库 | 公共领域 |