容器扩展示例

Qt 设计器 创建自定义多页插件。

容器扩展示例演示了如何使用 QDesignerContainerExtension 类创建 Qt 设计器 的自定义多页插件。

要提供一个可与 Qt 设计器 一起使用的自定义小部件,我们需要提供一个自包含的实现。在本示例中,我们使用了一个自定义的多页小部件,该小部件旨在显示容器扩展功能。

扩展是一个对象,它修改 Qt 设计器 的行为。QDesignerContainerExtension 允许 Qt 设计器 管理和操作自定义多页小部件,即向小部件添加和删除页面。

Qt 设计器 中有四种可用的扩展类型

您可以使用与本示例中相同的模式使用所有扩展,只需替换相应的扩展基类。有关更多信息,请参阅 Qt Designer C++ 类

容器扩展示例由四个类组成

  • MultiPageWidget 是一个自定义容器小部件,允许用户操作和填充其页面,并使用组合框在这些页面之间导航。
  • MultiPageWidgetPluginMultiPageWidget 类暴露给 Qt 设计器
  • MultiPageWidgetExtensionFactory 创建一个 MultiPageWidgetContainerExtension 对象。
  • MultiPageWidgetContainerExtension 提供容器扩展。

自定义小部件插件的工程文件需要一些附加信息,以确保它们能在 Qt 设计器 中正常工作。例如,自定义小部件插件依赖于 Qt 设计器 伴随提供的一些组件,这些必须在我们的工程文件中指定。我们首先看一下插件的项目文件。

接下来,我们将审查 MultiPageWidgetPlugin 类,然后看一下 MultiPageWidgetExtensionFactoryMultiPageWidgetContainerExtension 类。最后,我们将简要查看 MultiPageWidget 类定义。

项目文件

CMake

工程文件需要声明构建一个链接到 Qt 设计器 库的插件。

find_package(Qt6 REQUIRED COMPONENTS Core Designer Gui Widgets)

qt_add_plugin(containerextension)

target_link_libraries(containerextension PUBLIC
    Qt::Core
    Qt::Designer
    Qt::Gui
    Qt::Widgets
)

以下示例展示了如何添加小部件的头文件和源文件

target_sources(containerextension PRIVATE
    multipagewidget.cpp multipagewidget.h
    multipagewidgetcontainerextension.cpp multipagewidgetcontainerextension.h
    multipagewidgetextensionfactory.cpp multipagewidgetextensionfactory.h
    multipagewidgetplugin.cpp multipagewidgetplugin.h
)

我们提供了一个插件接口的实现,以便 Qt Designer 可以使用自定义小部件。在这个特定的例子中,我们还提供了容器扩展接口和扩展工厂的实现。

确保插件安装在一个 `Qt Designer` 会搜索的位置是很重要的。我们通过指定项目的目标路径并将其添加到要安装的项目列表中来实现这一点。

   set(INSTALL_EXAMPLEDIR "${QT6_INSTALL_PREFIX}/${QT6_INSTALL_PLUGINS}/designer")
install(TARGETS containerextension
    RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
    BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
    LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
)

容器扩展被创建为一个库。当项目安装时,它将与其他 `Qt Designer` 插件一起安装(使用 `ninja install` 或等效的安装过程)。

有关插件的信息,请参阅 如何创建 Qt 插件 文档。

qmake

以下示例展示了如何将插件链接到 Qt Designer

TEMPLATE = lib
CONFIG  += plugin

QT      += widgets designer

以下示例展示了如何添加小部件的头文件和源文件

HEADERS += multipagewidget.h \
           multipagewidgetplugin.h \
           multipagewidgetcontainerextension.h \
           multipagewidgetextensionfactory.h

SOURCES += multipagewidget.cpp \
           multipagewidgetplugin.cpp \
           multipagewidgetcontainerextension.cpp \
           multipagewidgetextensionfactory.cpp

OTHER_FILES += multipagewidget.json

以下示例展示了如何将插件安装到 Qt Designer 的插件路径

target.path = $$[QT_INSTALL_PLUGINS]/designer
INSTALLS += target

MultiPageWidgetPlugin 类定义

MultiPageWidgetPlugin 类将 `MultiPageWidget 类暴露给 `Qt Designer`。其定义类似于 自定义小部件插件 示例中的插件类,该类在详细说明中进行了解释。特定于该自定义小部件的类定义部分包括类名和几个私有槽

#ifndef MULTIPAGEWIDGETPLUGIN_H
#define MULTIPAGEWIDGETPLUGIN_H

#include <QtUiPlugin/QDesignerCustomWidgetInterface>

class QIcon;
class QWidget;

class MultiPageWidgetPlugin: public QObject, public QDesignerCustomWidgetInterface
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QDesignerCustomWidget")
    Q_INTERFACES(QDesignerCustomWidgetInterface)
public:
    explicit MultiPageWidgetPlugin(QObject *parent = nullptr);

    QString name() const override;
    QString group() const override;
    QString toolTip() const override;
    QString whatsThis() const override;
    QString includeFile() const override;
    QIcon icon() const override;
    bool isContainer() const override;
    QWidget *createWidget(QWidget *parent) override;
    bool isInitialized() const override;
    void initialize(QDesignerFormEditorInterface *formEditor) override;
    QString domXml() const override;

private slots:
    void currentIndexChanged(int index);
    void pageTitleChanged(const QString &title);

private:
    bool initialized = false;
};

#endif

插件类为 Qt Designer 提供有关我们的插件的基本信息,例如其类名及其包含的文件。此外,它知道如何创建 `MultiPageWidget` 小部件的实例。《code translate="no">MultiPageWidgetPlugin 还定义了一个 initialize() 函数,该函数在插件被加载到 `Qt Designer` 中后调用。函数的 QDesignerFormEditorInterface 参数为插件提供了一个通往 `Qt Designer` 所有 API 的门户。

对于像我们这样的多页小部件,我们必须实现两个私有槽,即 currentIndexChanged() 和 pageTitleChanged(),以便在用户查看另一页或更改其中一个页标题时,能够更新 `Qt Designer` 的属性编辑器。为了给每个页面设置自己的标题,我们选择使用 QWidget::windowTitle 属性来存储页面标题(有关更多信息,请参阅 containerextension/multipagewidget.cpp 中的 MultiPageWidget 类实现。请注意,目前没有通过使用预定义属性作为占位符的方式来添加自定义属性(例如,页面标题)到页面的方法。

由于 `MultiPageWidgetPlugin 类继承自 `QObject` 和 `QDesignerCustomWidgetInterface`,因此在使用多继承时,要记住确保所有接口(即不继承 Q_OBJECT 的类)都通过使用 Q_INTERFACES() 宏通知元对象系统。这使得 `Qt Designer` 能够使用 qobject_cast() 仅通过 QObject 指针来查询支持的接口。

为了确保 Qt 能够识别小部件为插件,通过添加 Q_PLUGIN_METADATA() 宏以导出有关小部件的相关信息

    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QDesignerCustomWidget")

使用此宏,Qt Designer 可以访问和构造自定义小部件。没有这个宏,Qt Designer 无法使用这个小部件。

MultiPageWidgetPlugin 类实现

MultiPageWidgetPlugin类的实现大部分与自定义小部件插件示例的插件类相当。

MultiPageWidgetPlugin::MultiPageWidgetPlugin(QObject *parent)
    : QObject(parent)
{
}

QString MultiPageWidgetPlugin::name() const
{
    return u"MultiPageWidget"_s;
}

QString MultiPageWidgetPlugin::group() const
{
    return u"Display Widgets [Examples]"_s;
}

QString MultiPageWidgetPlugin::toolTip() const
{
    return {};
}

QString MultiPageWidgetPlugin::whatsThis() const
{
    return {};
}

QString MultiPageWidgetPlugin::includeFile() const
{
    return u"multipagewidget.h"_s;
}

QIcon MultiPageWidgetPlugin::icon() const
{
    return {};
}

bool MultiPageWidgetPlugin::isInitialized() const
{
    return initialized;
}

不同的一个函数是isContainer()函数,在这个示例中返回true,因为我们的自定义小部件旨在用作容器。

bool MultiPageWidgetPlugin::isContainer() const
{
    return true;
}

另一个不同的函数是创建我们的自定义小部件的函数

QWidget *MultiPageWidgetPlugin::createWidget(QWidget *parent)
{
    auto *widget = new MultiPageWidget(parent);
    connect(widget, &MultiPageWidget::currentIndexChanged,
            this, &MultiPageWidgetPlugin::currentIndexChanged);
    connect(widget, &MultiPageWidget::pageTitleChanged,
            this, &MultiPageWidgetPlugin::pageTitleChanged);
    return widget;
}

除了创建和返回小部件,我们还连接了自定义容器小部件的currentIndexChanged()信号到插件中的currentIndexChanged()槽,以确保当用户查看另一个页面时,Qt Designer的属性编辑器会更新。我们还连接了小部件的pageTitleChanged()信号到插件的pageTitleChanged()槽。

每当我们的自定义小部件的currentIndexChanged()信号发出时,即用户查看另一个页面时,就会调用currentIndexChanged()槽。

void MultiPageWidgetPlugin::currentIndexChanged(int index)
{
    Q_UNUSED(index);
    auto *widget = qobject_cast<MultiPageWidget*>(sender());

首先,我们使用QObject::sender()和qobject_cast()函数检索发出信号的槽对象。如果在由信号激活的槽中被调用,QObject::sender()返回发送信号的对象的指针;否则返回0。

    if (widget) {
        auto *form = QDesignerFormWindowInterface::findFormWindow(widget);
        if (form)
            form->emitSelectionChanged();
    }
}

一旦我们获得了小部件,就可以更新属性编辑器。Qt Designer使用QDesignerPropertySheetExtension类为其属性编辑器提供数据,并且每当在它的工作空间中选择小部件时,Qt Designer都会查询小部件的属性表扩展,并更新属性编辑器。

所以我们要实现的目标是通知Qt Designer我们的小部件的内部选择已更改:首先,我们使用静态函数QDesignerFormWindowInterface::findFormWindow()检索包含小部件的QDesignerFormWindowInterface对象。QDesignerFormWindowInterface类允许你在Qt Designer的工作空间中查询和操作窗口表单。然后,我们只需要发出它的emitSelectionChanged()信号,强制属性编辑器更新。

在更改页面标题时,仅刷新属性编辑器是不够的,因为实际上需要更新页面的属性扩展。因此,我们需要访问我们想要更改标题的页面的QDesignerPropertySheetExtension对象。QDesignerPropertySheetExtension类还允许你操作小部件的属性,但为了获取扩展,我们首先需要检索到Qt Designer的扩展管理器。

void MultiPageWidgetPlugin::pageTitleChanged(const QString &title)
{
    Q_UNUSED(title);
    auto *widget = qobject_cast<MultiPageWidget*>(sender());
    if (widget) {
        auto *page = widget->widget(widget->currentIndex());
        auto *form = QDesignerFormWindowInterface::findFormWindow(widget);

同样,我们首先使用QObject::sender()和qobject_cast()函数检索发出信号的槽对象。然后我们检索发出信号的当前页面,并使用静态函数QDesignerFormWindowInterface::findFormWindow()检索含有小部件的表单。

            auto *editor = form->core();
            auto *manager = editor->extensionManager();

现在我们有了表单窗口,QDesignerFormWindowInterface类提供了core()函数,该函数返回当前的QDesignerFormEditorInterface对象。QDesignerFormEditorInterface类允许你访问Qt Designer的各种组件。特别是,QDesignerFormEditorInterface::extensionManager()函数返回对当前扩展管理器的引用。

            auto *sheet = qt_extension<QDesignerPropertySheetExtension*>(manager, page);
            const int propertyIndex = sheet->indexOf(QLatin1String("windowTitle"));
            sheet->setChanged(propertyIndex, true);
        }
    }
}

一旦我们拥有扩展管理器,我们就可以更新扩展工作表:首先,我们使用 qt_extension() 函数检索我们想要更改标题的页面的属性扩展。然后,我们使用 QDesignerPropertySheetExtension::indexOf() 函数检索页面标题的索引。如前所述,我们选择使用 QWidget::windowTitle 属性来存储页面标题(更多信息请参见 containerextension/multipagewidget.cpp 中的 MultiPageWidget 类实现)。最后,我们通过调用 QDesignerPropertySheetExtension::setChanged() 函数来隐式地强制更新页面的属性工作表。

void MultiPageWidgetPlugin::initialize(QDesignerFormEditorInterface *formEditor)
{
    if (initialized)
        return;

请注意 initialize() 函数:initialize() 函数接受一个 QDesignerFormEditorInterface 对象作为参数。

    auto *manager = formEditor->extensionManager();

当我们创建与自定义小部件插件相关联的扩展时,我们需要访问 Qt Designer 的当前扩展管理器,我们可以从 QDesignerFormEditorInterface 参数中检索它。

除了允许你操作小部件的属性外,QExtensionManager 类还为 Qt Designer 提供了扩展管理功能。使用 Qt Designer 的当前扩展管理器,你可以检索给定对象的扩展。你还可以为给定对象注册和注销扩展。请记住,扩展是一个对象,它修改了 Qt Designer 的行为。

在注册扩展时,实际上是注册了相关的扩展工厂。在 Qt Designer 中,扩展工厂用于查找和创建名称扩展,当需要时创建它们。因此,在本例中,容器扩展本身是在 Qt Designer 必须知道关联的小部件是否是容器之前不会创建的。

    auto *factory = new MultiPageWidgetExtensionFactory(manager);

    Q_ASSERT(manager != nullptr);
    manager->registerExtensions(factory, Q_TYPEID(QDesignerContainerExtension));

    initialized = true;
}

我们创建一个 MultiPageWidgetExtensionFactory 对象,并使用从 QDesignerFormEditorInterface 参数检索的 Qt Designer 的当前 extension manager 进行注册。第一个参数是新建的工厂,第二个参数是一个字符串形式的扩展标识符。简单的说,Q_TYPEID() 宏将字符串转换为 QLatin1String

MultiPageWidgetExtensionFactory 类是 QExtensionFactory 的子类。当 Qt Designer 必须知道小部件是否是容器时,Qt Designer 的扩展管理器将遍历所有已注册的工厂,调用能够为该小部件创建容器扩展的第一个工厂。这个工厂将创建一个 MultiPageWidgetExtension 对象。

QString MultiPageWidgetPlugin::domXml() const
{
    return uR"(
<ui language="c++">
    <widget class="MultiPageWidget" name="multipagewidget">
        <widget class="QWidget" name="page"/>
    </widget>
    <customwidgets>
        <customwidget>
            <class>MultiPageWidget</class>
            <extends>QWidget</extends>
            <addpagemethod>addPage</addpagemethod>
        </customwidget>
    </customwidgets>
</ui>)"_s;
}

最后,看看 domXml() 函数。这个函数包含了小部件在 Qt Designer 中使用的标准 XML 格式的默认设置。在本例中,我们指定了容器的第一页;多页小部件的任何初始页必须在函数内部指定。

MultiPageWidgetExtensionFactory 类定义

MultiPageWidgetExtensionFactory 类继承自 QExtensionFactory,为 Qt Designer 提供了一个标准扩展工厂。

class MultiPageWidgetExtensionFactory: public QExtensionFactory
{
    Q_OBJECT

public:
    explicit MultiPageWidgetExtensionFactory(QExtensionManager *parent = nullptr);

protected:
    QObject *createExtension(QObject *object, const QString &iid, QObject *parent) const override;
};

子类的目的是重新实现 QExtensionFactory::createExtension() 函数,使其能够创建一个 MultiPageWidget 容器扩展。

MultiPageWidgetExtensionFactory 类实现

类构造函数仅仅是调用 QExtensionFactory 基类构造函数

MultiPageWidgetExtensionFactory::MultiPageWidgetExtensionFactory(QExtensionManager *parent)
    : QExtensionFactory(parent)
{}

如上所述,当 Qt Designer 必须知道关联的小部件是否是容器时,将调用这个工厂。

QObject *MultiPageWidgetExtensionFactory::createExtension(QObject *object,
                                                          const QString &iid,
                                                          QObject *parent) const
{
    auto *widget = qobject_cast<MultiPageWidget*>(object);

    if (widget && (iid == Q_TYPEID(QDesignerContainerExtension)))
        return new MultiPageWidgetContainerExtension(widget, parent);
    return nullptr;
}

无论是请求的扩展与容器、成员表单、属性表单还是任务菜单相关联,Qt Designer 的行为都是相同的:它的扩展管理器会遍历所有注册的扩展工厂,并为每个调用 createExtension(),直到有响应创建所需的扩展。

因此,我们在 MultiPageWidgetExtensionFactory::createExtension() 中的第一件事就是检查请求扩展的 QObject 是否实际上是 MultiPageWidget 对象。然后我们检查请求的扩展是否是容器扩展。

如果对象是请求容器扩展的 MultiPageWidget,我们创建并返回一个 MultiPageWidgetExtension 对象。否则,我们只返回一个空指针,允许 iT Designer 的扩展管理器继续在注册的工厂中搜索。

MultiPageWidgetContainerExtension 类定义

MultiPageWidgetContainerExtension 类继承自 QDesignerContainerExtension,这使得您可以在 iT Designer 中向多页面容器插件添加(和删除)页面。

class MultiPageWidgetContainerExtension: public QObject,
                                         public QDesignerContainerExtension
{
    Q_OBJECT
    Q_INTERFACES(QDesignerContainerExtension)

public:
    explicit MultiPageWidgetContainerExtension(MultiPageWidget *widget, QObject *parent);

    bool canAddWidget() const override;
    void addWidget(QWidget *widget) override;
    int count() const override;
    int currentIndex() const override;
    void insertWidget(int index, QWidget *widget) override;
    bool canRemove(int index) const override;
    void remove(int index) override;
    void setCurrentIndex(int index) override;
    QWidget *widget(int index) const override;

private:
    MultiPageWidget *myWidget;
};

重要的是要认识到,QDesignerContainerExtension 类仅用于为 iT Designer 提供对自定义多页面小部件功能的访问;您的自定义多页面小部件必须实现与扩展函数相对应的功能。

请注意,我们实现了一个接受 两个 参数的构造函数:父窗口小部件和请求数据菜单的 MultiPageWidget 对象。

QDesignerContainerExtension 默认在 iT Designer 的任务菜单中提供一些菜单项,使用户能够向关联的自定义多页面小部件添加或删除页面。

MultiPageWidgetContainerExtension 类实现

在构造函数中,我们保存了发送为参数的 MultiPageWidget 对象的引用,即与扩展关联的窗口小部件。我们稍后将需要此对象来访问执行请求动作的自定义多页面小部件。

MultiPageWidgetContainerExtension::MultiPageWidgetContainerExtension(MultiPageWidget *widget,
                                                                     QObject *parent)
    : QObject(parent)
    , myWidget(widget)
{
}

要使 iT Designer 能够完全管理并操作您的自定义多页面小部件,您必须重新实现 QDesignerContainerExtension 的所有函数。

bool MultiPageWidgetContainerExtension::canAddWidget() const
{
    return true;
}

void MultiPageWidgetContainerExtension::addWidget(QWidget *widget)
{
    myWidget->addPage(widget);
}

int MultiPageWidgetContainerExtension::count() const
{
    return myWidget->count();
}

int MultiPageWidgetContainerExtension::currentIndex() const
{
    return myWidget->currentIndex();
}

您必须重新实现 canAddWidget() 和 addWidget() 添加给定页面到容器,count() 返回容器中页面的数量,以及 currentIndex() 返回当前选定的页面索引。

void MultiPageWidgetContainerExtension::insertWidget(int index, QWidget *widget)
{
    myWidget->insertPage(index, widget);
}

bool MultiPageWidgetContainerExtension::canRemove(int index) const
{
    Q_UNUSED(index);
    return true;
}

void MultiPageWidgetContainerExtension::remove(int index)
{
    myWidget->removePage(index);
}

void MultiPageWidgetContainerExtension::setCurrentIndex(int index)
{
    myWidget->setCurrentIndex(index);
}

QWidget* MultiPageWidgetContainerExtension::widget(int index) const
{
    return myWidget->widget(index);
}

必须重新实现 insertWidget() 在给定索引处添加给定页面到容器,canRemove() 和 remove() 在给定索引处删除页面,setCurrentIndex() 设置当前选中页面的索引,最后通过 widget() 返回给定索引的页面。

MultiPageWidget 类定义

MultiPageWidget 类是一个自定义容器小部件,允许用户操作和填充其页面,并使用组合框在这些页面之间导航。

class MultiPageWidget : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex)
    Q_PROPERTY(QString pageTitle READ pageTitle WRITE setPageTitle STORED false)

public:
    explicit MultiPageWidget(QWidget *parent = nullptr);

    QSize sizeHint() const override;

    int count() const;
    int currentIndex() const;
    QWidget *widget(int index);
    QString pageTitle() const;

public slots:
    void addPage(QWidget *page);
    void insertPage(int index, QWidget *page);
    void removePage(int index);
    void setPageTitle(const QString &newTitle);
    void setCurrentIndex(int index);

private slots:
    void pageWindowTitleChanged();

signals:
    void currentIndexChanged(int index);
    void pageTitleChanged(const QString &title);

private:
    QStackedWidget *stackWidget;
    QComboBox *comboBox;
};

要观察的主要细节是,您的自定义多页面小部件必须实现与 QDesignerContainerExtension 的成员函数相对应的功能,因为 QDesignerContainerExtension 类仅旨在为 iT Designer 提供对自定义多页面小部件功能的访问。

此外,我们声明了 currentIndexpageTitle 属性及其相关的设置和获取函数。通过将这些属性声明为属性,我们允许 Qt Designer 以相同的方式管理它们,例如特性编辑器,就像它管理从 QWidgetQObject 继承过来的属性一样。

注意 pageTitle 属性声明中的 STORED 属性:STORED 属性表示持久性,即它声明属性值是否需要在保存对象状态时被记住。如上所述,我们选择使用 QWidget::windowTitle 属性来保存页面标题,以便每个页面都可以有自己的标题。因此,pageTitle 属性是一个“假的”属性,它是为编辑目的提供的,并且不需要被存储。

我们还需要实现和发出 currentIndexChanged() 和 pageTitleChanged() 信号,以确保当用户查看另一个页面或更改页面标题时,Qt Designer 的属性编辑器能够更新。

更多详细信息请参阅 containerextension/multipagewidget.cpp 中的 MultiPageWidget 类实现。

示例项目见 @ code.qt.io

© 2024 Qt 公司有限。此处包含的文档贡献的版权属于各自的拥有者。此处提供的文档根据 Free Software Foundation 发布的 GNU 自由文档许可证版本 1.3 的条款提供许可。Qt 及其相关标志是芬兰以及世界其他地区 Qt 公司的商标。所有其他商标是各自所有者的财产。