Navegador WebEngine Quick Nano
Un navegador web implementado utilizando el tipo WebEngineView QML.

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 Tango | Dominio Público |
© 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.