웹엔진 퀵 나노 브라우저

WebEngineView QML 유형을 사용하여 구현된 웹 브라우저입니다.

퀵 나노 브라우저는 제목 표시줄과 도구 모음, 상태 표시줄로 구성된 작은 웹 브라우저 애플리케이션을 개발하기 위해 Qt WebEngine QML types 를 사용하여 제목 표시줄, 도구 모음, 탭 보기 및 상태 표시줄이 있는 브라우저 창으로 구성된 작은 웹 브라우저 애플리케이션을 개발하는 방법을 보여줍니다. 웹 콘텐츠는 탭 보기 내의 웹 엔진 보기에 로드됩니다. 인증서 오류가 발생하면 메시지 대화 상자에서 사용자에게 조치를 취하라는 메시지가 표시됩니다. 상태 표시줄이 팝업되어 마우스를 가져간 링크의 URL을 표시합니다.

웹 페이지가 전체 화면 모드로 표시되도록 요청할 수 있습니다. 사용자는 툴바 버튼을 사용하여 전체 화면 모드를 허용할 수 있습니다. 키보드 단축키를 사용하여 전체 화면 모드를 종료할 수 있습니다. 추가 툴바 버튼을 사용하면 브라우저 기록에서 앞뒤로 이동하고, 탭 콘텐츠를 다시 로드하고, 다음 기능을 활성화하기 위한 설정 메뉴를 열 수 있습니다: 자바스크립트, 플러그인, 전체 화면 모드, 오프 더 레코드, HTTP 디스크 캐시, 이미지 자동 로드, 인증서 오류 무시.

예제 실행하기

에서 예제를 실행하려면 Qt Creator에서 Welcome 모드를 열고 Examples 에서 예제를 선택합니다. 자세한 내용은 예제 빌드 및 실행하기를 참조하세요.

기본 브라우저 창 만들기

브라우저 기본 창이 로드되면 기본 프로필을 사용하여 빈 탭이 생성됩니다. 각 탭은 기본 창을 채우는 웹 엔진 보기입니다.

ApplicationWindow 유형을 사용하여 BrowserWindow.qml 파일에 기본 창을 만듭니다:

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

TabBar Qt Quick 컨트롤을 사용하여 창 상단에 고정된 탭 표시줄을 만들고 새 빈 탭을 만듭니다:

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

이 탭에는 웹 콘텐츠를 로드하는 웹 엔진 보기가 포함됩니다:

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

Action 유형을 사용하여 새 탭을 만듭니다:

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

ToolBar 내에 TextField Qt Quick 컨트롤을 사용하여 현재 URL을 표시하고 사용자가 다른 URL을 입력할 수 있는 주소 표시줄을 만듭니다:

    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
            }

인증서 오류 처리

인증서 오류가 발생하면 기본 프레임에서 발생했는지 또는 페이지 내부의 리소스에서 발생했는지 확인합니다. 리소스 오류는 사용자가 결정을 내릴 수 있는 충분한 컨텍스트가 없기 때문에 자동으로 인증서 거부를 트리거합니다. 다른 모든 경우에는 defer() QML 메서드를 호출하여 URL 요청을 일시 중지하고 사용자 입력을 기다립니다:

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

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

대화 상자 유형을 사용하여 사용자에게 웹 페이지 로딩을 계속하거나 취소할지 묻는 메시지를 표시합니다. 사용자가 Yes 를 선택하면 acceptCertificate() 메서드를 호출하여 URL에서 콘텐츠를 계속 로드합니다. 사용자가 No 을 선택하면 rejectCertificate() 메서드를 호출하여 요청을 거부하고 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
        }
    }

권한 요청 처리

onPermissionRequested() 시그널 핸들러를 사용하여 특정 기능이나 디바이스에 대한 액세스 요청을 처리합니다. permission 매개변수는 들어오는 요청을 처리하는 데 사용할 수 있는 WebEnginePermission 유형의 객체입니다. 대화 상자의 메시지를 구성하는 데 이 객체를 사용해야 하므로 이 객체를 임시로 저장합니다:

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

사용자에게 액세스 허용 또는 거부를 요청하는 대화 상자를 표시합니다. 사용자 정의 questionForFeature() JavaScript 함수는 요청에 대해 사람이 읽을 수 있는 질문을 생성합니다. 사용자가 Yes 를 선택하면 grant() 메서드를 호출하고 No 를 선택하면 deny() 메서드를 호출합니다.

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

전체화면 모드 시작 및 종료

설정 메뉴에 전체 화면 모드를 허용하는 메뉴 항목을 만들어 툴바에 배치합니다. 또한 키보드 단축키를 사용하여 전체 화면 모드를 종료하는 동작을 만듭니다. accept () 메서드를 호출하여 전체 화면 요청을 수락합니다. 이 메서드는 isFullScreen 속성을 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();
                }

전체 화면 모드로 들어가면 FullScreenNotification.qml에서 만든 FullScreenNotification 사용자 정의 유형을 사용하여 알림을 표시합니다.

설정 메뉴에서 Action 유형을 사용하여 이스케이프 키를 눌러 전체 화면 모드를 종료할 수 있는 바로 가기를 만듭니다:

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

WebAuth/FIDO UX 요청 처리하기

onWebAuthUxRequested() 시그널 핸들러를 사용하여 WebAuth/FIDO UX 요청을 처리합니다. request 매개변수는 요청을 처리하는 데 필요한 UX 요청 세부 정보 및 API를 포함하는 WebEngineWebAuthUxRequest 의 인스턴스입니다. 이를 사용하여 WebAuthUX 대화 상자를 구성하고 UX 요청 흐름을 시작합니다.

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

WebEngineWebAuthUxRequest 객체는 주기적으로 stateChanged 신호를 전송하여 잠재적 관찰자에게 현재 WebAuth UX 상태를 알립니다. 관찰자는 그에 따라 WebAuth 대화 상자를 업데이트합니다. 상태 변경 요청을 처리하기 위해 onStateChanged() 신호 처리기를 사용합니다. 이러한 신호를 처리하는 방법에 대한 예는 WebAuthDialog.qml 을 참조하세요.

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

macOS용 서명 요구 사항

macOS에서 퀵 나노 브라우저를 실행할 때 웹 사이트에서 위치, 카메라 및 마이크에 액세스할 수 있도록 허용하려면 애플리케이션에 서명해야 합니다. 이 작업은 빌드 시 자동으로 수행되지만 빌드 환경에 대해 유효한 서명 ID를 설정해야 합니다.

파일 및 어트리뷰션

이 예제에서는 탱고 아이콘 라이브러리의 아이콘을 사용합니다:

예제 프로젝트 @ 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.