En esta página

Navegador WebEngine Quick Nano

Un navegador web implementado utilizando el tipo WebEngineView QML.

Demostración del navegador

Quick Nano Browser demuestra cómo utilizar el lenguaje de programación Qt WebEngine QML types para desarrollar una pequeña aplicación de navegador web que consiste en una ventana de navegador con una barra de título, una barra de herramientas, una vista de pestañas y una barra de estado. El contenido web se carga en una vista de motor web dentro de la vista de pestañas. Si se producen errores de certificado, se solicita la intervención del usuario en un cuadro de diálogo de mensaje. La barra de estado se despliega para mostrar la URL de un enlace sobre el que se pase el ratón.

Una página web puede emitir una solicitud para mostrarse en modo de pantalla completa. Los usuarios pueden permitir el modo de pantalla completa utilizando un botón de la barra de herramientas. Pueden salir del modo de pantalla completa mediante un atajo de teclado. Otros botones de la barra de herramientas permiten retroceder y avanzar en el historial del navegador, recargar el contenido de las pestañas y abrir un menú de configuración para activar las siguientes funciones: JavaScript, plugins, modo de pantalla completa, off the record, caché de disco HTTP, autocarga de imágenes e ignorar errores de certificado.

Ejecución del ejemplo

Para ejecutar el ejemplo desde Qt Creatorabra el modo Welcome y seleccione el ejemplo de Examples. Para más información, consulte Qt Creator: Tutorial: Construir y ejecutar.

Creación de la ventana principal del navegador

Cuando se carga la ventana principal del navegador, se crea una pestaña vacía utilizando el perfil por defecto. Cada pestaña es una vista del motor web que llena la ventana principal.

Creamos la ventana principal en el archivo BrowserWindow.qml usando el tipo 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 ?? ""

Usamos el control TabBar Qt Quick para crear una barra de pestañas anclada a la parte superior de la ventana, y creamos una nueva pestaña vacía:

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

La pestaña contiene una vista del motor web que carga contenido 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()
                }
            }
        }

Utilizamos el tipo Action para crear nuevas pestañas:

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

Utilizamos el Control TextField Qt Quick dentro de un ToolBar para crear una barra de direcciones que muestra la URL actual y donde los usuarios pueden introducir otra 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
            }

Gestión de errores de certificado

En caso de error de certificado, comprobamos si procede del marco principal o de un recurso dentro de la página. Los errores de recursos provocan automáticamente un rechazo del certificado, ya que el usuario no tendrá contexto suficiente para tomar una decisión. Para todos los demás casos, llamamos al método QML defer() para pausar la solicitud de URL y esperar la entrada del usuario:

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

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

Utilizamos el tipo de diálogo para pedir a los usuarios que continúen o cancelen la carga de la página web. Si los usuarios seleccionan Yes, llamamos al método acceptCertificate() para continuar cargando el contenido de la URL. Si los usuarios seleccionan No, llamamos al método rejectCertificate() para rechazar la solicitud y detener la carga del contenido de la 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
        }
    }

Gestión de solicitudes de permiso

Utilizamos el gestor de señales onPermissionRequested() para gestionar las solicitudes de acceso a una determinada función o dispositivo. El parámetro permission es un objeto del tipo WebEnginePermission, que puede utilizarse para gestionar la solicitud entrante. Almacenamos temporalmente este objeto, ya que necesitamos utilizarlo para construir el mensaje del diálogo:

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

Mostramos un diálogo en el que se pide al usuario que conceda o deniegue el acceso. La función JavaScript personalizada questionForFeature() genera una pregunta legible por humanos sobre la solicitud. Si el usuario selecciona Yes, llamamos al método grant(), y si selecciona No llamamos a 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;
        }
    }

Entrar y salir del modo de pantalla completa

Creamos un elemento de menú para permitir el modo de pantalla completa en un menú de configuración que colocamos en la barra de herramientas. Además, creamos una acción para salir del modo de pantalla completa utilizando un atajo de teclado. Llamamos al método accept() para aceptar la petición de pantalla completa. El método establece la propiedad isFullScreen igual a la propiedad 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();
                }

Al entrar en el modo de pantalla completa, mostramos una notificación utilizando el tipo personalizado FullScreenNotification que creamos en FullScreenNotification.qml.

Utilizamos el tipo Action en el menú de configuración para crear un acceso directo para salir del modo de pantalla completa pulsando la tecla escape:

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

Gestión de solicitudes WebAuth/FIDO UX

Utilizamos el gestor de señales onWebAuthUxRequested() para gestionar las solicitudes de WebAuth/FIDO UX. El parámetro request es una instancia de WebEngineWebAuthUxRequest que contiene los detalles de la solicitud de UX y las API necesarias para procesar la solicitud. Lo utilizamos para construir el diálogo WebAuthUX e inicia el flujo de peticiones UX.

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

El objeto WebEngineWebAuthUxRequest emite periódicamente la señal stateChanged para notificar a los observadores potenciales los estados actuales de WebAuth UX. Los observadores actualizan el diálogo WebAuth en consecuencia. Utilizamos el manejador de señal onStateChanged() para manejar las solicitudes de cambio de estado. Consulte WebAuthDialog.qml para ver un ejemplo de cómo gestionar estas señales.

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

Requisito de firma para macOS

Para permitir que los sitios web accedan a la ubicación, la cámara y el micrófono cuando se ejecuta Quick Nano Browser en macOS, es necesario firmar la aplicación. Esto se hace automáticamente durante la compilación, pero es necesario configurar una identidad de firma válida para el entorno de compilación.

Archivos y atribuciones

El ejemplo utiliza iconos de la Tango Icon Library:

Biblioteca de iconos TangoDominio Público

Proyecto de ejemplo @ 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.