Introduction
In scientific computing and numerical modeling, there is often a situation where a model's behavior is determined by a set of parameters, such as gravitational acceleration, coefficient of friction, and timestep size. Most of these parameters remain constant throughout the course of the simulation, but it is desirable to adjust them quickly on the fly. Other numerical output often must be displayed, such as the total kinetic energy of the system, the current timestep number, etc.
High-level languages usually come with standard UI components that implement property browsing and editing. In C#, for example, one can employ a PropertyGrid
connected to an object. Since C# has an extensive metadata functionality, PropertyGrid
picks up the object's properties and presents them accordingly. Adding new numerical fields in this case is very straightforward.
Implementing the same functionality in C++ is not trivial. The often-used graphical API OpenGL does not directly implement text output or user interface components. There is a good selection of libraries built on top of OpenGL that implement property browsing. One example, SPlisHSPlasH, uses a custom-written library to store parameter values and another library, Anttweakbar, to implement the GUI. Nvidia's FleX demo uses a different implementation, Imgui, for user interface.
But what if the code is already based on Qt? Is there a way to present a list of properties? After all, Qt Designer has such component.
None of my Qt books say word about QPropertyBrowser
- an elusive widget used in the designer. To utilize its magic, one must first include qtpropertybrowser.pri
, typically situated in ~/Qt/5.12.6/Src/qttools/src/shared/qtpropertybrowser/.
If you do not have this folder, then install the Qt source via MaintenanceTool
. And if your project is using cmake
instead of qmake
, then you are out of luck. Refer to this solution for including the appropriate source files.
Instantiating the Widget
One option is to utilize QPropertyBrowser
directly, but it will not provide all the desired functionality. This widget does show and edit properties, but these won't be the properties of your object. The widget is created and populated as follows:
QtVariantPropertyManager *variantManager = new QtVariantPropertyManager;
QtTreePropertyBrowser browser;
browser.setFactoryForManager(variantManager, new QtVariantEditorFactory);
QtVariantProperty *p = variantManager->addProperty(QVariant::Int, "Property1");
browser.addProperty(p);
Include the following headers:
#include "qteditorfactory.h"
#include "qttreepropertybrowser.h"
#include "qtpropertymanager.h"
#include "qtvariantproperty.h"
An in-depth tutorial about instantiating this component is provided here. Note that this widget has no support in the designer and must be instantiated in code. At this point, we populated it manually with a single editable field.
Showing Properties of an Object
C++ language does not have object properties, neither is there any access to metadata. Qt extends C+ with its own property system, but how do we browse them via GUI? Let's create an ObjectPropertyBrowser
.
class ObjectPropertyBrowser : public QtTreePropertyBrowser
{
Q_OBJECT
public:
ObjectPropertyBrowser(QWidget* parent);
void setActiveObject(QObject *obj);
private:
QtVariantPropertyManager *variantManager;
QObject *currentlyConnectedObject = nullptr;
QMap<QtProperty *, const char*> propertyMap;
private slots:
void valueChanged(QtProperty *property, const QVariant &value);
public slots:
void objectUpdated(); };
We extend the original widget with setActiveObject(QObject*)
, which links the properties of any QObject
with our widget. This is the most difficult part, as we must loop through the object's properties and add them to the dictionary. The first item on this list is always the object's name
, which we don't care much about, so we skip it. Feel free to include it if you need it.
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();
}
The valueChanged()
slot is invoked whenever a user changes a value via UI. The change should be passed on to currentlyConnectedObject
, otherwise it will have no effect on the stored data. Thankfully, this is done in a single line of code, since our propertyMap
tells which QtVariantProperty
property corresponds to which object's property.
void ObjectPropertyBrowser::valueChanged(QtProperty *property, const QVariant &value)
{
currentlyConnectedObject->setProperty(propertyMap[property], value);
objectUpdated();
}
Note that a change in one property can cause changes in the others, so after the update, we refresh the whole UI by calling objectUpdated()
that enumerates through the propertyMap
and transfers the updated values to the browser. It can be called manually to update the UI after the object's data was changed externally, i.e., by a separate thread. We temporarily disconnect the signals to avoid circular notifications.
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*,>
Ideally, our QObject
will emit a propertyChanged
signal whenever a property is updated. But this requires a specifically designed QObject
:
class TestData : public QObject
{
Q_OBJECT
Q_PROPERTY(double Value1 MEMBER m_value1 NOTIFY propertyChanged)
signals:
void propertyChanged();
private:
double m_value1;
};
If QObject
does not emit this signal, no error will appear, but UI will not be automatically updated and the programmer will be responsible for calling the browser's objectUpdated()
whenever UI refresh is required. The screenshot and the included source code shows the final result, where the object's properties are fully editable, and any external changes are immediately reflected in the UI.
Conclusion
Editing an object's properties is a common task, and there is no super-easy way of achieving this in C++, as the language was conceived before the graphical user interface appeared. Various Qt-based solutions were posted as early as 2008 and have not been maintained, but still function quite well. Other solutions, like QtnProperty, are quite complex and have a steep learning curve. Since Qt Designer already includes a property browser, it seems like the most straightforward option within Qt framework and with a guarantee of long-term maintenance. If needed, a designer plugin can be created for the suggested ObjectPropertyBrowser
to visually handle this widget in Qt Designer. I hope that you find this information helpful. Thank you for any comments.
History
- 13th January, 2020: Initial version