WebEngineクイックナノブラウザ

WebEngineView QML タイプを使用して実装されたウェブブラウザです。

クイックナノブラウザはQt WebEngine QML types を使用して、タイトルバー、ツールバー、タブビュー、ステータスバーを備えたブラウザウィンドウで構成される小さなウェブブラウザアプリケーションを開発する方法を示します。ウェブ・コンテンツは、タブ・ビュー内のウェブ・エンジン・ビューにロードされます。証明書エラーが発生した場合、ユーザはメッセージダイアログで対応を求められます。ステータスバーがポップアップして、カーソルを合わせたリンクの URL が表示されます。

ウェブページは、フルスクリーンモードでの表示要求を発行できます。ユーザーはツールバーボタンを使ってフルスクリーンモードを許可できます。キーボードショートカットを使えば、フルスクリーンモードから抜けることができます。追加のツールバーボタンを使用すると、ブラウザの履歴を前後に移動したり、タブの内容を再読み込みしたり、次の機能を有効にするための設定メニューを開いたりできます:JavaScript、プラグイン、フルスクリーンモード、オフザレコード、HTTPディスクキャッシュ、画像のオートロード、証明書エラーの無視。

サンプルを実行する

Qt Creator からサンプルを実行するには、Welcome モードを開き、Examples からサンプルを選択します。詳細については、Building and Running an Example を参照してください。

メインブラウザウィンドウの作成

ブラウザのメインウィンドウがロードされると、デフォルトのプロファイルを使用して空のタブが作成されます。各タブはメインウィンドウを埋めるウェブエンジン・ビューです。

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 Control を使用して、現在の 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 パラメータはWebEngineWebAuthUxRequest のインスタンスで、UX リクエストの詳細とリクエストの処理に必要な API が含まれています。これを使用して 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上でQuick Nano Browserを実行する際、Webサイトが位置情報、カメラ、マイクにアクセスできるようにするには、アプリケーションに署名する必要があります。これはビルド時に自動的に行われますが、ビルド環境に有効な署名IDを設定する必要があります。

ファイルと帰属

このサンプルではTango Icon Libraryのアイコンを使用しています:

Tangoアイコンライブラリパブリックドメイン

サンプルプロジェクト @ code.qt.io

©2024 The Qt Company Ltd. 本書に含まれる文書の著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。