WebEngine 小工具简易浏览器示例

基于Qt WebEngine Widgets 的简单浏览器。

简单浏览器演示了如何使用 Qt WebEngine C++ classes来开发一个包含以下元素的小型网络浏览器应用程序:

  • 菜单栏,用于打开存储的网页以及管理窗口和标签页。
  • 导航栏,用于输入 URL 和在网页浏览历史中前后移动。
  • 多标签区,用于在标签页中显示网页内容。
  • 状态栏,用于显示悬停链接。
  • 简单的下载管理器。

网页内容可在新标签页或单独窗口中打开。访问网页时可使用 HTTP 和代理验证。

运行示例

运行示例 Qt Creator,打开Welcome 模式,并从Examples 中选择示例。更多信息,请参阅Qt Creator: 教程:构建并运行

类层次结构

我们先绘制一个将要实现的主要类的草图:

  • Browser 是管理应用程序窗口的类。
  • BrowserWindow 是一个 ,显示菜单、导航栏、 和状态栏。QMainWindow TabWidget
  • TabWidget 是 ,包含一个或多个浏览器标签。QTabWidget
  • WebView 是 ,为 提供视图,并在 中添加为标签页。QWebEngineView WebPage TabWidget
  • WebPage 是 ,表示网站内容。QWebEnginePage

此外,我们还将实现一些辅助类:

  • WebPopupWindow 是一个 ,用于显示弹出窗口。QWidget
  • DownloadManagerWidget 是一个实现下载列表的 。QWidget

创建浏览器主窗口

本示例支持由Browser 对象拥有的多个主窗口。该类还拥有DownloadManagerWidget ,可用于书签和历史记录管理器等其他功能。

main.cpp 中,我们创建了第一个BrowserWindow 实例,并将其添加到Browser 对象中。如果命令行中没有传递参数,我们将打开Qt 主页

int main(int argc, char **argv)
{
    QCoreApplication::setOrganizationName("QtExamples");

    QApplication app(argc, argv);
    app.setWindowIcon(QIcon(u":AppLogoColor.png"_s));
    QLoggingCategory::setFilterRules(u"qt.webenginecontext.debug=true"_s);

    QWebEngineProfile::defaultProfile()->settings()->setAttribute(QWebEngineSettings::PluginsEnabled, true);
    QWebEngineProfile::defaultProfile()->settings()->setAttribute(QWebEngineSettings::DnsPrefetchEnabled, true);
    QWebEngineProfile::defaultProfile()->settings()->setAttribute(
            QWebEngineSettings::ScreenCaptureEnabled, true);

    QUrl url = commandLineUrlArgument();

    Browser browser;
    BrowserWindow *window = browser.createHiddenWindow();
    window->tabWidget()->setUrl(url);
    window->show();
    return app.exec();
}

为了在将窗口切换为 OpenGL 渲染时抑制闪烁,我们在添加第一个浏览器标签后调用 show。

创建标签页

BrowserWindow 构造函数初始化了所有必要的用户界面相关对象。BrowserWindowcentralWidget 包含TabWidget 的一个实例。TabWidget 包含一个或多个WebView 实例作为标签页,并将其信号和插槽委托给当前选定的标签页:

class TabWidget : public QTabWidget
{
    ...
signals:
    // current tab/page signals
    void linkHovered(const QString &link);
    void loadProgress(int progress);
    void titleChanged(const QString &title);
    void urlChanged(const QUrl &url);
    void favIconChanged(const QIcon &icon);
    void webActionEnabledChanged(QWebEnginePage::WebAction action, bool enabled);
    void devToolsRequested(QWebEnginePage *source);
    void findTextFinished(const QWebEngineFindTextResult &result);

public slots:
    // current tab/page slots
    void setUrl(const QUrl &url);
    void triggerWebPageAction(QWebEnginePage::WebAction action);
    ...
};

每个标签页都包含一个WebView 实例:

WebView *TabWidget::createTab()
{
    WebView *webView = createBackgroundTab();
    setCurrentWidget(webView);
    return webView;
}

WebView *TabWidget::createBackgroundTab()
{
    WebView *webView = new WebView;
    WebPage *webPage = new WebPage(m_profile, webView);
    webView->setPage(webPage);
    setupView(webView);
    int index = addTab(webView, tr("(Untitled)"));
    setTabIcon(index, webView->favIcon());
    // Workaround for QTBUG-61770
    webView->resize(currentWidget()->size());
    webView->show();
    return webView;
}

TabWidget::setupView() 中,我们确保TabWidget 始终转发当前选定的WebView 的信号:

void TabWidget::setupView(WebView *webView)
{
    QWebEnginePage *webPage = webView->page();

    connect(webView, &QWebEngineView::titleChanged, [this, webView](const QString &title) {
        int index = indexOf(webView);
        if (index != -1) {
            setTabText(index, title);
            setTabToolTip(index, title);
        }
        if (currentIndex() == index)
            emit titleChanged(title);
    });
    connect(webView, &QWebEngineView::urlChanged, [this, webView](const QUrl &url) {
        int index = indexOf(webView);
        if (index != -1)
            tabBar()->setTabData(index, url);
        if (currentIndex() == index)
            emit urlChanged(url);
    });
    connect(webView, &QWebEngineView::loadProgress, [this, webView](int progress) {
        if (currentIndex() == indexOf(webView))
            emit loadProgress(progress);
    });
    connect(webPage, &QWebEnginePage::linkHovered, [this, webView](const QString &url) {
        if (currentIndex() == indexOf(webView))
            emit linkHovered(url);
    });
    connect(webView, &WebView::favIconChanged, [this, webView](const QIcon &icon) {
        int index = indexOf(webView);
        if (index != -1)
            setTabIcon(index, icon);
        if (currentIndex() == index)
            emit favIconChanged(icon);
    });
    connect(webView, &WebView::webActionEnabledChanged, [this, webView](QWebEnginePage::WebAction action, bool enabled) {
        if (currentIndex() ==  indexOf(webView))
            emit webActionEnabledChanged(action,enabled);
    });
    connect(webPage, &QWebEnginePage::windowCloseRequested, [this, webView]() {
        int index = indexOf(webView);
        if (webView->page()->inspectedPage())
            window()->close();
        else if (index >= 0)
            closeTab(index);
    });
    connect(webView, &WebView::devToolsRequested, this, &TabWidget::devToolsRequested);
    connect(webPage, &QWebEnginePage::findTextFinished, [this, webView](const QWebEngineFindTextResult &result) {
        if (currentIndex() == indexOf(webView))
            emit findTextFinished(result);
    });
}

关闭标签页

当用户关闭一个标签页时,我们首先会在相应的WebView 上触发RequestClose 网页操作:

    connect(tabBar, &QTabBar::tabCloseRequested, [this](int index) {
        if (WebView *view = webView(index))
            view->page()->triggerAction(QWebEnginePage::WebAction::RequestClose);
    });

这就允许任何 JavaScriptbeforeunload 事件监听器触发,可能会通过对话框提示用户确认是否要关闭页面。在这种情况下,用户可以拒绝关闭请求并继续打开标签页,否则就会发出windowCloseRequested 信号并关闭标签页:

    connect(webPage, &QWebEnginePage::windowCloseRequested, [this, webView]() {
        int index = indexOf(webView);
        if (webView->page()->inspectedPage())
            window()->close();
        else if (index >= 0)
            closeTab(index);
    });

实现 WebView 功能

WebView 源自QWebEngineView ,支持以下功能:

  • renderProcess 死机时显示错误信息
  • 处理createWindow 请求
  • 在上下文菜单中添加自定义菜单项

首先,我们使用必要的方法和信号创建WebView

class WebView : public QWebEngineView
{
    Q_OBJECT

public:
    explicit WebView(QWidget *parent = nullptr);
    ...
protected:
    void contextMenuEvent(QContextMenuEvent *event) override;
    QWebEngineView *createWindow(QWebEnginePage::WebWindowType type) override;

signals:
    void webActionEnabledChanged(QWebEnginePage::WebAction webAction, bool enabled);
    ...
};
显示错误信息

如果呈现过程被终止,我们将显示带有错误代码的QMessageBox ,然后重新加载页面:

WebView::WebView(QWidget *parent)
    : QWebEngineView(parent)
{
    ...
    connect(this, &QWebEngineView::renderProcessTerminated,
            [this](QWebEnginePage::RenderProcessTerminationStatus termStatus, int statusCode) {
        QString status;
        switch (termStatus) {
        case QWebEnginePage::NormalTerminationStatus:
            status = tr("Render process normal exit");
            break;
        case QWebEnginePage::AbnormalTerminationStatus:
            status = tr("Render process abnormal exit");
            break;
        case QWebEnginePage::CrashedTerminationStatus:
            status = tr("Render process crashed");
            break;
        case QWebEnginePage::KilledTerminationStatus:
            status = tr("Render process killed");
            break;
        }
        QMessageBox::StandardButton btn = QMessageBox::question(window(), status,
                                                   tr("Render process exited with code: %1\n"
                                                      "Do you want to reload the page ?").arg(statusCode));
        if (btn == QMessageBox::Yes)
            QTimer::singleShot(0, this, &WebView::reload);
    });
}
管理网络窗口

加载的页面可能希望创建QWebEnginePage::WebWindowType 类型的窗口,例如,当 JavaScript 程序请求在新窗口或对话框中打开文档时。这可以通过覆盖QWebView::createWindow() 来处理:

QWebEngineView *WebView::createWindow(QWebEnginePage::WebWindowType type)
{
    BrowserWindow *mainWindow = qobject_cast<BrowserWindow*>(window());
    if (!mainWindow)
        return nullptr;

    switch (type) {
    case QWebEnginePage::WebBrowserTab: {
        return mainWindow->tabWidget()->createTab();
    }

QWebEnginePage::WebDialog 的情况下,我们会创建一个自定义WebPopupWindow 类的实例:

class WebPopupWindow : public QWidget
{
    Q_OBJECT

public:
    explicit WebPopupWindow(QWebEngineProfile *profile);
    WebView *view() const;

private slots:
    void handleGeometryChangeRequested(const QRect &newGeometry);

private:
    QLineEdit *m_urlLineEdit;
    QAction *m_favAction;
    WebView *m_view;
};
添加上下文菜单项

我们在上下文菜单中添加了一个菜单项,这样用户就可以通过右键单击在新窗口中打开检查器。我们覆盖QWebEngineView::contextMenuEvent ,并使用QWebEngineView::createStandardContextMenu 创建默认的QMenu ,其中包含QWebEnginePage::WebAction 操作的默认列表。

QWebEnginePage::InspectElement 操作的默认名称是Inspect 。为清晰起见,当还没有检查器时,我们将其重命名为Open Inspector In New Window ;当已经创建检查器时,我们将其重命名为Inspect Element

我们还要检查QWebEnginePage::ViewSource 操作是否在菜单中,因为如果不在菜单中,我们还必须添加一个分隔符。

void WebView::contextMenuEvent(QContextMenuEvent *event)
{
    QMenu *menu = createStandardContextMenu();
    const QList<QAction *> actions = menu->actions();
    auto inspectElement = std::find(actions.cbegin(), actions.cend(), page()->action(QWebEnginePage::InspectElement));
    if (inspectElement == actions.cend()) {
        auto viewSource = std::find(actions.cbegin(), actions.cend(), page()->action(QWebEnginePage::ViewSource));
        if (viewSource == actions.cend())
            menu->addSeparator();

        QAction *action = menu->addAction("Open inspector in new window");
        connect(action, &QAction::triggered, [this]() { emit devToolsRequested(page()); });
    } else {
        (*inspectElement)->setText(tr("Inspect element"));
    }

    // add conext menu for image policy
    QMenu *editImageAnimation = new QMenu(tr("Image animation policy"));

    m_imageAnimationGroup = new QActionGroup(editImageAnimation);
    m_imageAnimationGroup->setExclusive(true);

    QAction *disableImageAnimation =
            editImageAnimation->addAction(tr("Disable all image animation"));
    disableImageAnimation->setCheckable(true);
    m_imageAnimationGroup->addAction(disableImageAnimation);
    connect(disableImageAnimation, &QAction::triggered, [this]() {
        handleImageAnimationPolicyChange(QWebEngineSettings::ImageAnimationPolicy::Disallow);
    });
    QAction *allowImageAnimationOnce =
            editImageAnimation->addAction(tr("Allow animated images, but only once"));
    allowImageAnimationOnce->setCheckable(true);
    m_imageAnimationGroup->addAction(allowImageAnimationOnce);
    connect(allowImageAnimationOnce, &QAction::triggered,
            [this]() { handleImageAnimationPolicyChange(QWebEngineSettings::ImageAnimationPolicy::AnimateOnce); });
    QAction *allowImageAnimation = editImageAnimation->addAction(tr("Allow all animated images"));
    allowImageAnimation->setCheckable(true);
    m_imageAnimationGroup->addAction(allowImageAnimation);
    connect(allowImageAnimation, &QAction::triggered, [this]() {
        handleImageAnimationPolicyChange(QWebEngineSettings::ImageAnimationPolicy::Allow);
    });

    switch (page()->settings()->imageAnimationPolicy()) {
    case QWebEngineSettings::ImageAnimationPolicy::Allow:
        allowImageAnimation->setChecked(true);
        break;
    case QWebEngineSettings::ImageAnimationPolicy::AnimateOnce:
        allowImageAnimationOnce->setChecked(true);
        break;
    case QWebEngineSettings::ImageAnimationPolicy::Disallow:
        disableImageAnimation->setChecked(true);
        break;
    default:
        allowImageAnimation->setChecked(true);
        break;
    }

    menu->addMenu(editImageAnimation);
    menu->popup(event->globalPos());
}

实现 WebPage 和 WebView 功能

我们将WebPage 作为QWebEnginePage 的子类来实现,将WebView 作为QWebEngineView 的子类来实现,以启用 HTTP、代理验证以及在访问网页时忽略 SSL 证书错误:

class WebPage : public QWebEnginePage
{
    Q_OBJECT

public:
    explicit WebPage(QWebEngineProfile *profile, QObject *parent = nullptr);

signals:
    void createCertificateErrorDialog(QWebEngineCertificateError error);

private slots:
    void handleCertificateError(QWebEngineCertificateError error);
    void handleSelectClientCertificate(QWebEngineClientCertificateSelection clientCertSelection);
    void handleDesktopMediaRequest(const QWebEngineDesktopMediaRequest &request);
};

class WebView : public QWebEngineView
{
    Q_OBJECT

public:
    explicit WebView(QWidget *parent = nullptr);
    ~WebView();
    void setPage(WebPage *page);

    int loadProgress() const;
    bool isWebActionEnabled(QWebEnginePage::WebAction webAction) const;
    QIcon favIcon() const;

protected:
    void contextMenuEvent(QContextMenuEvent *event) override;
    QWebEngineView *createWindow(QWebEnginePage::WebWindowType type) override;

signals:
    void webActionEnabledChanged(QWebEnginePage::WebAction webAction, bool enabled);
    void favIconChanged(const QIcon &icon);
    void devToolsRequested(QWebEnginePage *source);
private slots:
    void handleCertificateError(QWebEngineCertificateError error);
    void handleAuthenticationRequired(const QUrl &requestUrl, QAuthenticator *auth);
    void handlePermissionRequested(QWebEnginePermission permission);
    void handleProxyAuthenticationRequired(const QUrl &requestUrl, QAuthenticator *auth,
                                           const QString &proxyHost);
    void handleRegisterProtocolHandlerRequested(QWebEngineRegisterProtocolHandlerRequest request);
#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
    void handleFileSystemAccessRequested(QWebEngineFileSystemAccessRequest request);
    void handleWebAuthUxRequested(QWebEngineWebAuthUxRequest *request);
#endif
    void handleImageAnimationPolicyChange(QWebEngineSettings::ImageAnimationPolicy policy);

private:
    void createWebActionTrigger(QWebEnginePage *page, QWebEnginePage::WebAction);
    void onStateChanged(QWebEngineWebAuthUxRequest::WebAuthUxState state);

private:
    int m_loadProgress = 100;
    WebAuthDialog *m_authDialog = nullptr;
    QActionGroup *m_imageAnimationGroup = nullptr;
};

在上述所有情况下,我们都会向用户显示相应的对话框。在进行身份验证时,我们需要在QAuthenticator 对象上设置正确的凭证值:

void WebView::handleAuthenticationRequired(const QUrl &requestUrl, QAuthenticator *auth)
{
    QDialog dialog(window());
    dialog.setModal(true);
    dialog.setWindowFlags(dialog.windowFlags() & ~Qt::WindowContextHelpButtonHint);

    Ui::PasswordDialog passwordDialog;
    passwordDialog.setupUi(&dialog);

    passwordDialog.m_iconLabel->setText(QString());
    QIcon icon(window()->style()->standardIcon(QStyle::SP_MessageBoxQuestion, 0, window()));
    passwordDialog.m_iconLabel->setPixmap(icon.pixmap(32, 32));

    QString introMessage(tr("Enter username and password for \"%1\" at %2")
                                 .arg(auth->realm(),
                                      requestUrl.toString().toHtmlEscaped()));
    passwordDialog.m_infoLabel->setText(introMessage);
    passwordDialog.m_infoLabel->setWordWrap(true);

    if (dialog.exec() == QDialog::Accepted) {
        auth->setUser(passwordDialog.m_userNameLineEdit->text());
        auth->setPassword(passwordDialog.m_passwordLineEdit->text());
    } else {
        // Set authenticator null if dialog is cancelled
        *auth = QAuthenticator();
    }
}

handleProxyAuthenticationRequired 信号处理器会执行与 HTTP 代理验证相同的步骤。

如果出现 SSL 错误,我们要检查这些错误是来自主框架,还是来自页面内的资源。资源错误会自动触发证书拒绝,因为用户没有足够的上下文来做出决定。对于所有其他情况,我们会触发一个对话框,让用户允许或拒绝证书。

void WebPage::handleCertificateError(QWebEngineCertificateError error)
{
    // Automatically block certificate errors from page resources without prompting the user.
    // This mirrors the behavior found in other major browsers.
    if (!error.isMainFrame()) {
        error.rejectCertificate();
        return;
    }

    error.defer();
    QTimer::singleShot(0, this,
                       [this, error]() mutable { emit createCertificateErrorDialog(error); });
}

打开网页

本节介绍打开新网页的工作流程。当用户在导航栏中输入 URL 并按下Enter 时,就会发出QLineEdit::returnPressed 信号,然后新 URL 就会被转到TabWidget::setUrl

BrowserWindow::BrowserWindow(Browser *browser, QWebEngineProfile *profile, bool forDevTools)
{
    ...
        connect(m_urlLineEdit, &QLineEdit::returnPressed, [this]() {
            m_tabWidget->setUrl(QUrl::fromUserInput(m_urlLineEdit->text()));
        });
    ...
}

调用被转发到当前选定的标签页:

void TabWidget::setUrl(const QUrl &url)
{
    if (WebView *view = currentWebView()) {
        view->setUrl(url);
        view->setFocus();
    }
}

WebViewsetUrl() 方法只是将url 转发给相关的WebPage ,后者则在后台开始下载页面内容。

实现私密浏览

私密浏览隐身模式"无记录 "模式是许多浏览器的一项功能,在这种模式下,cookie、HTTP 缓存或浏览历史记录等通常持续存在的数据只保留在内存中,而不会在磁盘上留下任何痕迹。在本示例中,我们将在窗口级别实现隐私浏览,在一个窗口中的所有标签页都处于正常或隐私模式。或者,我们也可以在标签页一级实现隐私浏览,窗口中的一些标签页处于正常模式,其他标签页处于隐私模式。

使用Qt WebEngine 实现私人浏览非常简单。只需创建一个新的QWebEngineProfile ,然后在QWebEnginePage 中使用它来代替默认配置文件。在示例中,这个新配置文件为Browser 对象所有:

class Browser
{
public:
    ...
    BrowserWindow *createHiddenWindow(bool offTheRecord = false);
    BrowserWindow *createWindow(bool offTheRecord = false);
private:
    ...
    QScopedPointer<QWebEngineProfile> m_profile;
};

私人浏览所需的配置文件与其第一个窗口一起创建。QWebEngineProfile 的默认构造函数已将其置于非记录模式。

BrowserWindow *Browser::createHiddenWindow(bool offTheRecord)
{
    if (!offTheRecord && !m_profile) {
        const QString name = u"simplebrowser."_s + QLatin1StringView(qWebEngineChromiumVersion());
        QWebEngineProfileBuilder profileBuilder;
        m_profile.reset(profileBuilder.createProfile(name));
    ...

剩下要做的就是将相应的配置文件传递给相应的QWebEnginePage 对象。Browser 对象将向每个新的BrowserWindow 传递全局默认配置文件(请参阅QWebEngineProfile::defaultProfile )或一个共享的非记录配置文件实例:

    ...
        QObject::connect(m_profile.get(), &QWebEngineProfile::downloadRequested,
                         &m_downloadManagerWidget, &DownloadManagerWidget::downloadRequested);
    }
    auto profile = !offTheRecord ? m_profile.get() : QWebEngineProfile::defaultProfile();
    auto mainWindow = new BrowserWindow(this, profile, false);
    return mainWindow;
}

然后,BrowserWindowTabWidget 对象将确保窗口中包含的所有QWebEnginePage 对象都使用该配置文件。

管理下载

下载与QWebEngineProfile 相关联。每当在网页上触发下载时,QWebEngineProfile::downloadRequested 信号就会与QWebEngineDownloadRequest 一起发出,在本例中, 信号被转发到DownloadManagerWidget::downloadRequested

Browser::Browser()
{
    // Quit application if the download manager window is the only remaining window
    m_downloadManagerWidget.setAttribute(Qt::WA_QuitOnClose, false);

    QObject::connect(
        QWebEngineProfile::defaultProfile(), &QWebEngineProfile::downloadRequested,
        &m_downloadManagerWidget, &DownloadManagerWidget::downloadRequested);
}

该方法会提示用户输入文件名(预填建议)并开始下载(除非用户取消Save As 对话框):

void DownloadManagerWidget::downloadRequested(QWebEngineDownloadRequest *download)
{
    Q_ASSERT(download && download->state() == QWebEngineDownloadRequest::DownloadRequested);

    QString path = QFileDialog::getSaveFileName(this, tr("Save as"), QDir(download->downloadDirectory()).filePath(download->downloadFileName()));
    if (path.isEmpty())
        return;

    download->setDownloadDirectory(QFileInfo(path).path());
    download->setDownloadFileName(QFileInfo(path).fileName());
    download->accept();
    add(new DownloadWidget(download));

    show();
}

QWebEngineDownloadRequest 对象将定期发出receivedBytesChanged 信号,通知潜在观察者下载进度,并在下载完成或出现错误时发出stateChanged 信号。有关如何处理这些信号的示例,请参阅downloadmanagerwidget.cpp

管理 WebAuth/FIDO 用户体验请求

WebAuth UX 请求与QWebEnginePage 相关联。每当验证器需要用户交互时,就会在QWebEnginePage 上触发 UX 请求,并通过QWebEngineWebAuthUxRequest 发出QWebEnginePage::webAuthUxRequested 信号,在本示例中,该信号被转发到WebView::handleAuthenticatorRequired

    connect(page, &QWebEnginePage::webAuthUxRequested, this, &WebView::handleWebAuthUxRequested);

该方法创建一个 WebAuth 用户体验对话框,并启动用户体验请求流。

void WebView::handleWebAuthUxRequested(QWebEngineWebAuthUxRequest *request)
{
    if (m_authDialog)
        delete m_authDialog;

    m_authDialog = new WebAuthDialog(request, window());
    m_authDialog->setModal(false);
    m_authDialog->setWindowFlags(m_authDialog->windowFlags() & ~Qt::WindowContextHelpButtonHint);

    connect(request, &QWebEngineWebAuthUxRequest::stateChanged, this, &WebView::onStateChanged);
    m_authDialog->show();
}

QWebEngineWebAuthUxRequest 对象会定期发出stateChanged 信号,通知潜在的观察者当前的 WebAuth 用户体验状态。观察者会相应地更新 WebAuth 对话框。有关如何处理这些信号的示例,请参见webview.cppwebauthdialog.cpp

macOS 的签名要求

在 macOS 上运行简单浏览器时,要允许网站访问位置、摄像头和麦克风,应用程序需要签名。这在构建时会自动完成,但你需要为构建环境设置一个有效的签名身份。

文件和属性

本示例使用 Tango 图标库中的图标:

Tango 图标库公共领域

示例项目 @ 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.