Sur cette page

Navigateur WebEngine Quick Nano

Un navigateur web implémenté en utilisant le type QML WebEngineView.

Démonstration du navigateur

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 :

Exemple de projet @ code.qt.io

© 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.