Navigateur WebEngine Quick Nano
Un navigateur web implémenté en utilisant le type QML WebEngineView.

Quick Nano Browser montre comment utiliser le type QML pour développer une petite application de navigateur web. Qt WebEngine QML types pour développer une petite application de navigateur web qui consiste en une fenêtre de navigateur avec une barre de titre, une barre d'outils, une vue d'onglet et une barre d'état. Le contenu web est chargé dans une vue du moteur web à l'intérieur de la vue de l'onglet. En cas d'erreur de certificat, les utilisateurs sont invités à agir dans une boîte de dialogue. La barre d'état s'ouvre pour afficher l'URL d'un lien survolé.
Une page web peut demander à être affichée en mode plein écran. Les utilisateurs peuvent autoriser le mode plein écran en utilisant un bouton de la barre d'outils. Ils peuvent quitter le mode plein écran en utilisant un raccourci clavier. D'autres boutons de la barre d'outils permettent d'avancer ou de reculer dans l'historique du navigateur, de recharger le contenu des onglets et d'ouvrir un menu de paramètres permettant d'activer les fonctions suivantes : JavaScript, plugins, mode plein écran, désactivation de l'enregistrement, cache disque HTTP, chargement automatique des images et ignorance des erreurs de certificat.
Exécution de l'exemple
Pour exécuter l'exemple à partir de Qt CreatorOuvrez le mode Welcome et sélectionnez l'exemple à partir de Examples. Pour plus d'informations, voir Qt Creator: Tutoriel : Construire et exécuter.
Création de la fenêtre principale du navigateur
Lorsque la fenêtre principale du navigateur est chargée, elle crée un onglet vide en utilisant le profil par défaut. Chaque onglet est une vue du moteur web qui remplit la fenêtre principale.
Nous créons la fenêtre principale dans le fichier BrowserWindow.qml en utilisant le type ApplicationWindow:
ApplicationWindow { id: win required property ApplicationRoot applicationRoot property WebEngineView currentWebView: tabBar.currentIndex < tabBar.count ? tabLayout.children[tabBar.currentIndex] : null ... width: 1300 height: 900 visible: true title: win.currentWebView?.title ?? ""
Nous utilisons le contrôle TabBar Qt Quick pour créer une barre d'onglets ancrée en haut de la fenêtre et créer un nouvel onglet vide :
TabBar { id: tabBar anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right Component.onCompleted: createTab(win.applicationRoot.defaultProfilePrototype.instance()) 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; })}); webview.index = Qt.binding(function () { return newTabButton.TabBar.index; })
L'onglet contient une vue du moteur web qui charge le contenu web :
Component { id: tabComponent WebEngineView { id: webEngineView property int index: 0 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 settings.imageAnimationPolicy: appSettings.imageAnimationPolicy settings.screenCaptureEnabled: true settings.javascriptCanAccessClipboard: appSettings.javascriptCanAccessClipboard settings.javascriptCanPaste: appSettings.javascriptCanPaste onWindowCloseRequested: function() { tabBar.removeView(webEngineView.index); } onCertificateError: function(error) { if (!error.isMainFrame) { error.rejectCertificate(); return; } 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(win.currentWebView.profile, true, request.requestedUrl); tab.acceptAsNewWindow(request); } else if (request.destination === WebEngineNewWindowRequest.InNewBackgroundTab) { var backgroundTab = tabBar.createTab(win.currentWebView.profile, false); backgroundTab.acceptAsNewWindow(request); } else if (request.destination === WebEngineNewWindowRequest.InNewDialog) { var dialog = win.applicationRoot.createDialog(win.currentWebView.profile); dialog.win.currentWebView.acceptAsNewWindow(request); } else { var window = win.applicationRoot.createWindow(win.currentWebView.profile); window.win.currentWebView.acceptAsNewWindow(request); } } onFullScreenRequested: function(request) { if (request.toggleOn) { webEngineView.state = "FullScreen"; win.previousVisibility = win.visibility; win.showFullScreen(); fullScreenNotification.show(); } else { webEngineView.state = ""; win.visibility = win.previousVisibility; fullScreenNotification.hide(); } request.accept(); } onRegisterProtocolHandlerRequested: function(request) { console.log("accepting registerProtocolHandler request for " + request.scheme + " from " + request.origin); request.accept(); } onDesktopMediaRequested: function(request) { // select the primary screen request.selectScreen(request.screensModel.index(0, 0)); } 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(); } onPermissionRequested: function(permission) { permissionDialog.permission = permission; permissionDialog.visible = true; } onWebAuthUxRequested: function(request) { webAuthDialog.init(request); } Timer { id: reloadTimer interval: 0 running: false repeat: false onTriggered: win.currentWebView.reload() } } }
Nous utilisons le type Action pour créer de nouveaux onglets :
Action { shortcut: StandardKey.AddTab onTriggered: { tabBar.createTab(tabBar.count !== 0 ? win.currentWebView.profile : win.applicationRoot.defaultProfilePrototype.instance()); addressBar.forceActiveFocus(); addressBar.selectAll(); }
Nous utilisons le contrôle TextField Qt Quick à l'intérieur d'un contrôle ToolBar pour créer une barre d'adresse qui affiche l'URL actuel et où les utilisateurs peuvent saisir un autre URL :
menuBar: ToolBar { id: navigationBar RowLayout { anchors.fill: parent ... TextField { id: addressBar ... focus: true Layout.fillWidth: true Binding on text { when: win.currentWebView value: win.currentWebView.url } onAccepted: win.currentWebView.url = Utils.fromUserInput(text) selectByMouse: true }
Gestion des erreurs de certificat
En cas d'erreur de certificat, nous vérifions si elle provient du cadre principal ou d'une ressource à l'intérieur de la page. Les erreurs provenant d'une ressource déclenchent automatiquement le rejet du certificat, car l'utilisateur ne dispose pas d'un contexte suffisant pour prendre une décision. Dans tous les autres cas, nous appelons la méthode QML defer() pour mettre en pause la demande d'URL et attendre la contribution de l'utilisateur :
onCertificateError: function(error) { if (!error.isMainFrame) { error.rejectCertificate(); return; } error.defer(); sslDialog.enqueue(error); }
Nous utilisons le type Dialog pour inviter les utilisateurs à poursuivre ou à annuler le chargement de la page web. Si l'utilisateur choisit Yes, nous appelons la méthode acceptCertificate() pour continuer à charger le contenu de l'URL. Si l'utilisateur choisit No, nous appelons la méthode rejectCertificate() pour rejeter la demande et arrêter le chargement du contenu de l'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 } }
Traitement des demandes de permission
Nous utilisons le gestionnaire de signal onPermissionRequested() pour gérer les demandes d'accès à une certaine fonctionnalité ou à un certain appareil. Le paramètre permission est un objet de type WebEnginePermission, qui peut être utilisé pour traiter la demande entrante. Nous stockons temporairement cet objet, car nous devons l'utiliser pour construire le message de la boîte de dialogue :
onPermissionRequested: function(permission) { permissionDialog.permission = permission; permissionDialog.visible = true; }
Nous affichons une boîte de dialogue dans laquelle l'utilisateur est invité à accorder ou à refuser l'accès. La fonction JavaScript personnalisée questionForFeature() génère une question lisible par l'homme à propos de la demande. Si l'utilisateur choisit Yes, nous appelons la méthode grant(), et s'il choisit No, nous appelons deny().
Dialog { id: permissionDialog anchors.centerIn: parent width: Math.min(win.width, win.height) / 3 * 2 contentWidth: mainTextForPermissionDialog.width contentHeight: mainTextForPermissionDialog.height standardButtons: Dialog.No | Dialog.Yes title: "Permission Request" property var permission; contentItem: Item { Label { id: mainTextForPermissionDialog } } onAccepted: permission.grant() onRejected: permission.deny() onVisibleChanged: { if (visible) { mainTextForPermissionDialog.text = questionForPermissionType(); width = contentWidth + 20; } } function questionForPermissionType() { var question = "Allow " + permission.origin + " to " switch (permission.permissionType) { case WebEnginePermission.PermissionType.Geolocation: question += "access your location information?"; break; case WebEnginePermission.PermissionType.MediaAudioCapture: question += "access your microphone?"; break; case WebEnginePermission.PermissionType.MediaVideoCapture: question += "access your webcam?"; break; case WebEnginePermission.PermissionType.MediaAudioVideoCapture: question += "access your microphone and webcam?"; break; case WebEnginePermission.PermissionType.MouseLock: question += "lock your mouse cursor?"; break; case WebEnginePermission.PermissionType.DesktopVideoCapture: question += "capture video of your desktop?"; break; case WebEnginePermission.PermissionType.DesktopAudioVideoCapture: question += "capture audio and video of your desktop?"; break; case WebEnginePermission.PermissionType.Notifications: question += "show notification on your desktop?"; break; case WebEnginePermission.PermissionType.ClipboardReadWrite: question += "read from and write to your clipboard?"; break; case WebEnginePermission.PermissionType.LocalFontsAccess: question += "access the fonts stored on your machine?"; break; default: question += "access unknown or unsupported permission type [" + permission.permissionType + "] ?"; break; } return question; } }
Entrer et sortir du mode plein écran
Nous créons un élément de menu pour autoriser le mode plein écran dans un menu de paramètres que nous plaçons dans la barre d'outils. Nous créons également une action permettant de quitter le mode plein écran à l'aide d'un raccourci clavier. Nous appelons la méthode accept() pour accepter la demande de mode plein écran. La méthode définit la propriété isFullScreen comme étant égale à la propriété toggleOn.
onFullScreenRequested: function(request) { if (request.toggleOn) { webEngineView.state = "FullScreen"; win.previousVisibility = win.visibility; win.showFullScreen(); fullScreenNotification.show(); } else { webEngineView.state = ""; win.visibility = win.previousVisibility; fullScreenNotification.hide(); } request.accept(); }
Lors de l'entrée en mode plein écran, nous affichons une notification à l'aide du type personnalisé FullScreenNotification que nous créons dans FullScreenNotification.qml.
Nous utilisons le type Action dans le menu des paramètres pour créer un raccourci permettant de quitter le mode plein écran en appuyant sur la touche Echap :
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 property int imageAnimationPolicy: WebEngineSettings.ImageAnimationPolicy.Allow property alias javascriptCanAccessClipboard: javascriptCanAccessClipboard.checked property alias javascriptCanPaste: javascriptCanPaste.checked } Action { shortcut: "Escape" onTriggered: { if (win.currentWebView.state === "FullScreen") { win.visibility = win.previousVisibility; fullScreenNotification.hide(); win.currentWebView.triggerWebAction(WebEngineView.ExitFullScreen); } if (findBar.visible) findBar.visible = false; } }
Traitement des demandes WebAuth/FIDO UX
Nous utilisons le gestionnaire de signaux onWebAuthUxRequested() pour traiter les demandes de WebAuth/FIDO UX. Le paramètre request est une instance de WebEngineWebAuthUxRequest qui contient les détails de la demande UX et les API nécessaires pour traiter la demande. Nous l'utilisons pour construire le dialogue WebAuthUX et lancer le flux de demandes d'interface utilisateur.
onWebAuthUxRequested: function(request) { webAuthDialog.init(request); }
L'objet WebEngineWebAuthUxRequest émet périodiquement le signal stateChanged pour informer les observateurs potentiels de l'état actuel de l'interface utilisateur WebAuth. Les observateurs mettent à jour le dialogue WebAuth en conséquence. Nous utilisons le gestionnaire de signal onStateChanged() pour gérer les demandes de changement d'état. Voir WebAuthDialog.qml pour un exemple de la façon dont ces signaux peuvent être gérés.
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; }
Exigences de signature pour macOS
Pour permettre aux sites web d'accéder à la localisation, à l'appareil photo et au microphone lors de l'exécution de Quick Nano Browser sur macOS, l'application doit être signée. Cela se fait automatiquement lors de la construction, mais vous devez configurer une identité de signature valide pour l'environnement de construction.
Fichiers et attributions
L'exemple utilise des icônes de la bibliothèque d'icônes de Tango :
| Bibliothèque d'icônes Tango | Domaine public |
© 2026 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.