65.9K
CodeProject 正在变化。 阅读更多。
Home

QtTreePropertyBrowser 与 PropertyGrid 对比:在科学计算中的应用

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2020 年 1 月 13 日

CPOL

4分钟阅读

viewsIcon

19702

downloadIcon

679

属性浏览是一项常见的任务,已有许多独立的库可用,但让我们探索一下 Qt 库内置的功能来处理这项任务。

引言

在科学计算和数值建模中,经常会出现这种情况:模型的行为由一组参数决定,例如重力加速度、摩擦系数和时间步长。其中大多数参数在模拟过程中保持不变,但最好能够快速地动态调整它们。此外,通常还需要显示其他数值输出,例如系统的总动能、当前时间步数等。

高级语言通常带有实现属性浏览和编辑的标准 UI 组件。例如,在 C# 中,可以使用一个连接到对象的 PropertyGrid。由于 C# 具有广泛的元数据功能,PropertyGrid 可以拾取对象的属性并相应地呈现它们。在这种情况下,添加新的数值字段非常直接。

在 C++ 中实现相同的功能并非易事。常用的图形 API OpenGL 不直接实现文本输出或用户界面组件。有许多基于 OpenGL 构建的库实现了属性浏览。一个例子是 SPlisHSPlasH,它使用一个 自定义编写的库 来存储参数值,并使用另一个库 Anttweakbar 来实现 GUI。Nvidia 的 FleX 演示使用了一个不同的实现 Imgui 来实现用户界面。

但是,如果代码已经基于 Qt 呢?是否有办法显示属性列表?毕竟,Qt Designer 中有这样的组件。

我的任何 Qt 书籍都没有提到 QPropertyBrowser——一个在 Designer 中使用的神秘小部件。要利用它的魔力,首先必须包含 qtpropertybrowser.pri,它通常位于 ~/Qt/5.12.6/Src/qttools/src/shared/qtpropertybrowser/ 目录中。

如果您没有此文件夹,则需要通过 MaintenanceTool 安装 Qt 源代码。如果您的项目使用 cmake 而不是 qmake,那么您就无能为力了。有关包含适当源文件的信息,请参阅 此解决方案

实例化小部件

一种选择是直接使用 QPropertyBrowser,但它不会提供所有期望的功能。这个小部件确实可以显示和编辑属性,但这些属性不是您对象的属性。小部件的创建和填充如下:

QtVariantPropertyManager *variantManager = new QtVariantPropertyManager;
QtTreePropertyBrowser browser;
browser.setFactoryForManager(variantManager, new QtVariantEditorFactory);
QtVariantProperty *p = variantManager->addProperty(QVariant::Int, "Property1");
browser.addProperty(p);

包含以下头文件

#include "qteditorfactory.h"
#include "qttreepropertybrowser.h"
#include "qtpropertymanager.h"
#include "qtvariantproperty.h"

这里提供了一个关于实例化此组件的深入教程 在此。请注意,此小部件在 Designer 中不受支持,必须在代码中实例化。此时,我们只手动填充了一个可编辑字段。

显示对象的属性

C++ 语言没有对象属性,也没有对元数据的访问。Qt 通过其自有的 属性系统 扩展了 C++,但我们如何通过 GUI 浏览它们呢?让我们创建一个 ObjectPropertyBrowser

class ObjectPropertyBrowser : public QtTreePropertyBrowser
{
    Q_OBJECT

public:
    ObjectPropertyBrowser(QWidget* parent);
    void setActiveObject(QObject *obj); // connect to QObject using this

private:
    QtVariantPropertyManager *variantManager;
    QObject *currentlyConnectedObject = nullptr;
    QMap<QtProperty *, const char*> propertyMap;

private slots:
    void valueChanged(QtProperty *property, const QVariant &value);

public slots:
    void objectUpdated(); // call this whenever currentlyConnectedObject is updated
};

我们扩展了原始小部件,增加了 setActiveObject(QObject*) 方法,该方法将任何 QObject 的属性与我们的窗口部件关联起来。这是最困难的部分,因为我们必须遍历对象的属性并将它们添加到字典中。列表中的第一项始终是对象的 name,我们不太关心它,所以跳过它。如果您需要,可以随意包含它。

void ObjectPropertyBrowser::setActiveObject(QObject *obj)
{
    clear();
    variantManager->clear();
    propertyMap.clear();
    if(currentlyConnectedObject) currentlyConnectedObject->disconnect(this);
    currentlyConnectedObject = obj;
    if(!obj) return;

    for(int i=1; i< obj->metaObject()->propertyCount();i++) {
        QMetaProperty mp = obj->metaObject()->property(i);
        QtVariantProperty *property = variantManager->addProperty(mp.type(),mp.name());
        property->setEnabled(mp.isWritable());
        propertyMap[property] = mp.name();
        addProperty(property);
    }
    connect(obj, SIGNAL(propertyChanged()), this, SLOT(objectUpdated()));
    objectUpdated();
}

每当用户通过 UI 更改值时,就会调用 valueChanged() 插槽。更改应传递给 currentlyConnectedObject,否则将对存储的数据没有影响。幸运的是,这只需一行代码即可完成,因为我们的 propertyMap 告诉我们哪个 QtVariantProperty 属性对应于哪个对象的属性。

void ObjectPropertyBrowser::valueChanged(QtProperty *property, const QVariant &value)
{
    currentlyConnectedObject->setProperty(propertyMap[property], value);
    objectUpdated();
}

请注意,一个属性的更改可能会导致其他属性的更改,因此在更新后,我们通过调用 objectUpdated() 来刷新整个 UI。该方法会遍历 propertyMap 并将更新后的值传输到浏览器。当对象的數據(例如,由单独的线程)外部更改时,可以手动调用它来更新 UI。我们暂时断开了信号连接,以避免循环通知。

void ObjectPropertyBrowser::objectUpdated()
{
    disconnect(variantManager, SIGNAL(valueChanged(QtProperty*, QVariant)), 
               this, SLOT(valueChanged(QtProperty*, QVariant)));
    QMapIterator<qtproperty*, char="" const=""> i(propertyMap);
    while(i.hasNext()) {
        i.next();
        variantManager->setValue(i.key(), currentlyConnectedObject->property(i.value()));
    }
    connect(variantManager, SIGNAL(valueChanged(QtProperty*, QVariant)), 
            this, SLOT(valueChanged(QtProperty*, QVariant)));
}
</qtproperty*,>

理想情况下,我们的 QObject 在属性更新时会发出 propertyChanged 信号。但这需要一个专门设计的 QObject

class TestData : public QObject
{
    Q_OBJECT
    Q_PROPERTY(double Value1 MEMBER m_value1 NOTIFY propertyChanged)

signals:
    void propertyChanged();
private:
    double m_value1;
};

如果 QObject 没有发出此信号,则不会出现错误,但 UI 不会自动更新,并且程序员将负责在需要刷新 UI 时调用浏览器的 objectUpdated()。屏幕截图和包含的源代码显示了最终结果,其中对象的属性是完全可编辑的,并且任何外部更改都会立即反映在 UI 中。

结论

编辑对象的属性是一项常见任务,在 C++ 中实现这一点并非易事,因为该语言是在图形用户界面出现之前设计的。各种基于 Qt 的解决方案最早可以追溯到 2008 年,但并未得到维护,尽管仍然运行良好。其他解决方案,例如 QtnProperty,非常复杂且学习曲线陡峭。由于 Qt Designer 已包含属性浏览器,因此在 Qt 框架内,它似乎是最直接的选择,并保证了长期的维护。如果需要,可以为建议的 ObjectPropertyBrowser 创建一个 Designer 插件,以便在 Qt Designer 中直观地处理此小部件。希望您发现这些信息很有帮助。感谢您的任何评论。

历史

  • 2020 年 1 月 13 日:初始版本
© . All rights reserved.