WebEngine Quick Nano Browser

Ein Webbrowser, der unter Verwendung des WebEngineView QML-Typs implementiert wurde.

Quick Nano Browser demonstriert, wie man mit dem Qt WebEngine QML types um eine kleine Webbrowser-Anwendung zu entwickeln, die aus einem Browserfenster mit Titelleiste, Symbolleiste, Registerkartenansicht und Statusleiste besteht. Der Webinhalt wird in einer Web-Engine-Ansicht innerhalb der Registerkartenansicht geladen. Treten Zertifikatsfehler auf, wird der Benutzer in einem Meldungsdialog zum Handeln aufgefordert. Die Statusleiste wird eingeblendet, um die URL eines mit dem Mauszeiger gehaltenen Links anzuzeigen.

Eine Webseite kann eine Anfrage zur Anzeige im Vollbildmodus stellen. Der Benutzer kann den Vollbildmodus über eine Schaltfläche in der Symbolleiste zulassen. Sie können den Vollbildmodus mit einem Tastaturkürzel verlassen. Weitere Schaltflächen in der Symbolleiste ermöglichen es, im Browserverlauf vor- und zurückzuspringen, den Inhalt von Registerkarten neu zu laden und ein Einstellungsmenü zum Aktivieren der folgenden Funktionen zu öffnen: JavaScript, Plugins, Vollbildmodus, Off the Record, HTTP Disk Cache, Autoloading von Bildern und Ignorieren von Zertifikatsfehlern.

Ausführen des Beispiels

Zum Ausführen des Beispiels von Qt Creatorzu starten, öffnen Sie den Modus Welcome und wählen Sie das Beispiel unter Examples aus. Weitere Informationen finden Sie unter Erstellen und Ausführen eines Beispiels.

Erstellen des Browser-Hauptfensters

Wenn das Hauptfenster des Browsers geladen wird, wird eine leere Registerkarte unter Verwendung des Standardprofils erstellt. Jede Registerkarte ist eine Web-Engine-Ansicht, die das Hauptfenster füllt.

Wir erstellen das Hauptfenster in der Datei BrowserWindow.qml unter Verwendung des Typs 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

Wir verwenden das Steuerelement TabBar Qt Quick , um eine Registerkartenleiste zu erstellen, die am oberen Rand des Fensters verankert ist, und erstellen eine neue, leere Registerkarte:

    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);
            }

Die Registerkarte enthält eine Web-Engine-Ansicht, die Webinhalte lädt:

        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
                settings.imageAnimationPolicy: appSettings.imageAnimationPolicy
                settings.screenCaptureEnabled: true

                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(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();
                }

                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: currentWebView.reload()
                }
            }
        }

Wir verwenden den Typ Action, um neue Registerkarten zu erstellen:

    Action {
        shortcut: StandardKey.AddTab
        onTriggered: {
            tabBar.createTab(tabBar.count != 0 ? currentWebView.profile : defaultProfile);
            addressBar.forceActiveFocus();
            addressBar.selectAll();
        }

Wir verwenden das TextField Qt Quick Control innerhalb eines ToolBar, um eine Adressleiste zu erstellen, die die aktuelle URL anzeigt und in die der Benutzer eine andere URL eingeben kann:

    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
            }

Behandlung von Zertifikatsfehlern

Im Falle eines Zertifikatsfehlers prüfen wir, ob er vom Hauptrahmen oder von einer Ressource innerhalb der Seite stammt. Fehler in der Ressource führen automatisch zu einer Ablehnung des Zertifikats, da der Benutzer nicht genug Kontext hat, um eine Entscheidung zu treffen. In allen anderen Fällen rufen wir die QML-Methode defer() auf, um die URL-Anfrage zu unterbrechen und auf Benutzereingaben zu warten:

                onCertificateError: function(error) {
                    if (!error.isMainFrame) {
                        error.rejectCertificate();
                        return;
                    }

                    error.defer();
                    sslDialog.enqueue(error);
                }

Wir verwenden den Typ Dialog, um den Benutzer aufzufordern, das Laden der Webseite fortzusetzen oder abzubrechen. Wenn der Benutzer Yes auswählt, rufen wir die Methode acceptCertificate() auf, um das Laden von Inhalten aus der URL fortzusetzen. Wenn der Benutzer No auswählt, rufen wir die Methode rejectCertificate() auf, um die Anfrage abzulehnen und das Laden von Inhalten aus der URL zu beenden:

    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
        }
    }

Behandlung von Erlaubnisanfragen

Wir verwenden den Signalhandler onPermissionRequested(), um Anfragen für den Zugriff auf eine bestimmte Funktion oder ein Gerät zu behandeln. Der Parameter permission ist ein Objekt vom Typ WebEnginePermission, das zur Bearbeitung der eingehenden Anfrage verwendet werden kann. Wir speichern dieses Objekt vorübergehend, da wir es zum Aufbau der Nachricht des Dialogs verwenden müssen:

                onPermissionRequested: function(permission) {
                    permissionDialog.permission = permission;
                    permissionDialog.visible = true;
                }

Wir zeigen einen Dialog an, in dem der Benutzer gefragt wird, ob er den Zugriff gewähren oder verweigern möchte. Die benutzerdefinierte JavaScript-Funktion questionForFeature() generiert eine für den Menschen lesbare Frage über die Anfrage. Wenn der Benutzer Yes wählt, rufen wir die Methode grant() auf, und wenn er No wählt, rufen wir deny() auf.

    Dialog {
        id: permissionDialog
        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 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;
        }
    }

Aufrufen und Verlassen des Vollbildmodus

Wir erstellen einen Menüpunkt für den Vollbildmodus in einem Einstellungsmenü, das wir in der Symbolleiste platzieren. Außerdem erstellen wir eine Aktion zum Verlassen des Vollbildmodus mit Hilfe eines Tastaturkürzels. Wir rufen die Methode accept() auf, um die Anforderung des Vollbildmodus zu akzeptieren. Die Methode setzt die Eigenschaft isFullScreen gleich der Eigenschaft 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();
                }

Wenn der Vollbildmodus aktiviert wird, wird eine Benachrichtigung mit dem benutzerdefinierten Typ FullScreenNotification angezeigt, den wir in FullScreenNotification.qml erstellen.

Wir verwenden den Typ Action im Einstellungsmenü, um eine Abkürzung für das Verlassen des Vollbildmodus durch Drücken der Escape-Taste zu erstellen:

    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
    }

    Action {
        shortcut: "Escape"
        onTriggered: {
            if (currentWebView.state == "FullScreen") {
                browserWindow.visibility = browserWindow.previousVisibility;
                fullScreenNotification.hide();
                currentWebView.triggerWebAction(WebEngineView.ExitFullScreen);
            }

            if (findBar.visible)
                findBar.visible = false;
        }
    }

Behandlung von WebAuth/FIDO UX-Anfragen

Wir verwenden den onWebAuthUxRequested() Signalhandler, um Anfragen für WebAuth/FIDO UX zu behandeln. Der Parameter request ist eine Instanz von WebEngineWebAuthUxRequest, die UX-Anfragedetails und APIs enthält, die zur Verarbeitung der Anfrage erforderlich sind. Wir verwenden es, um den WebAuthUX-Dialog zu erstellen und den UX-Anfragefluss zu initiieren.

                onWebAuthUxRequested: function(request) {
                    webAuthDialog.init(request);
                }

Das Objekt WebEngineWebAuthUxRequest sendet in regelmäßigen Abständen das Signal stateChanged aus, um potenzielle Beobachter über den aktuellen WebAuth-UX-Status zu informieren. Die Beobachter aktualisieren den WebAuth-Dialog entsprechend. Wir verwenden den onStateChanged()-Signalhandler, um Zustandsänderungsanforderungen zu behandeln. Siehe WebAuthDialog.qml für ein Beispiel, wie diese Signale behandelt werden können.

    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;
    }

Signieranforderung für macOS

Um Websites den Zugriff auf den Standort, die Kamera und das Mikrofon zu ermöglichen, wenn Quick Nano Browser auf macOS läuft, muss die Anwendung signiert werden. Dies geschieht automatisch beim Erstellen, aber Sie müssen eine gültige Signieridentität für die Build-Umgebung einrichten.

Dateien und Attribute

Das Beispiel verwendet Icons aus der Tango Icon Library:

Tango-SymbolbibliothekÖffentlicher Bereich

Beispielprojekt @ code.qt.io

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