Workspace 6.21.5
Writing a custom widget - looking at the code

In this section, we're going to understand the code that was automatically generated by Workspace so that we know how to customise our widget, and implement its custom visualisation capability.

Rebuilding (continue on with tutorial Writing a Custom Widget )

The first thing we need to understand is that the wizard has generated three classes:

  1. RectangleWidget: The widget itself. It knows how to display the data in a Rectangle object.
  2. RectangleWidgetConnector: Connects an input / output to our RectangleWidget. It knows how to take the data from the input / output, and give it to the widget. It also knows how to take the data displayed in the widget and use it to update the input / output's data.
  3. RectangleWidgetFactory: Knows how to create RectangleWidgets and RectangleWidgetConnectors. An instance of this class will be registered with our SimplePlugin so that Workspace can automatically create RectangleWidgets when necessary.

Now that we understand that, we can have a look at the code that comprises each of our classes. Let's start with the RectangleWidget.

rectanglewidget.h

Here we can see that we extend QWidget. QWidget is the base class for all widget types in the Qt toolkit. Anything you see in a user interface is some type of QWidget. By default, a QWidget is essentially an empty container into which we can place other widgets.

We can also see that we add the Q_OBJECT macro at the top of the class. This makes sure that Qt adds the appropriate meta-object functionality when we compile the class. In other words, it allows us to use fancy things called Signals and Slots which are not available in standard C++.

As in the previous tutorials, we can also see our implementation class, RectangleWidgetImpl being forward-declared and a pointer to an instance of it being declared as a private member of the class.

#ifndef CSIRO_RECTANGLEWIDGET_H
#define CSIRO_RECTANGLEWIDGET_H
#include <memory>
#include <QWidget>
#include "simpleplugin.h"
namespace CSIRO
{
class Rectangle;
}
namespace CSIRO
{
class RectangleWidgetImpl;
class SIMPLEPLUGIN_API RectangleWidget : public QWidget
{
Q_OBJECT
// Uncomment the line below for large widgets that should be collapsed by default
// in Workspace's Operation Editor tree view. The READ value of this property
// isn't used (but is required by Qt), we simply check if the property exists.
//Q_PROPERTY(bool collapseInIOTree READ isVisible())
std::unique_ptr<RectangleWidgetImpl> pImpl_;
Top level namespace for all Workspace code.
Definition: applicationsupportplugin.cpp:32

The next thing to do is to define our constructor, destructor and our two key public member functions updateWidget and updateData. As one would expect, updateWidget is used to update the widget by taking a reference to a Rectangle, and updateData is used to update a reference to a data object with information gathered from the widget's UI.

public:
RectangleWidget(QWidget* parent = 0);
~RectangleWidget() override;
bool updateWidget(const CSIRO::Rectangle& data);
bool updateData(CSIRO::Rectangle& data);

Lastly, we can see a fancy section called signals, and within it the declaration of a signal called widgetUpdated. As one might expect, this signal is emitted whenever the widget has been updated by the user, indicating that it is ready to reflect these updates in the data, if any is available.

signals:
void widgetUpdated();
};
} // namespace CSIRO
#endif

We'll see how our widgetUpdated signal is used when we look at our widget connector later. For now, if you'd like any more information about how Signals and Slots work in Qt, you can take a look at this Qt documention page.


Rebuilding (continue on with tutorial Writing a Custom Widget )


rectanglewidget.cpp

We will show this implementation file in stages and talk about each part as we go. As usual, we start by including the required headers, the most notable of which is the ui_rectanglewidget.h file. This file contains the Qt generated UI class, which we are essentially wrapping with our class. We also include the usual namespace scopes:

#include <memory>
#include "ui_rectanglewidget.h"
#include "rectangle.h"
#include "rectanglewidget.h"
#include "simpleplugin.h"
namespace CSIRO
{
using namespace CSIRO;

The next thing we need to do is define our private implementation class:

class RectangleWidgetImpl : public CSIRO::Application::TextLogger
{
public:
RectangleWidget& owner_;
std::unique_ptr<Ui::RectangleWidget> ui_;
RectangleWidgetImpl(RectangleWidget& owner) :
owner_(owner),
ui_(std::make_unique<Ui::RectangleWidget>())
{
ui_->setupUi(&owner);
}
bool updateWidget(const CSIRO::Rectangle& data);
bool updateData(CSIRO::Rectangle& data);
};
Convenience base class for anything associated with an operation that needs to write text to the log ...
Definition: textlogger.h:66
Definition: remoteschedulerwidget.h:44
WSGLModelUpdateInfo::Impl & owner
Definition: wsglmodelupdateinfo.cpp:56

This class is very straightforward. It contains the implementations of our updateWidget and updateData methods, as well as a pointer to the UI implementation class generated by Qt's UI file generator.

Next up, we find the definitions of our updateWidget and updateData methods:

bool RectangleWidgetImpl::updateWidget(const CSIRO::Rectangle& data)
{
// ================================== //
// Update your widget's controls here //
// ================================== //
return true;
}
bool RectangleWidgetImpl::updateData(CSIRO::Rectangle& data)
{
// =================================== //
// Update the data argument with data //
// from your widgets controls here //
// =================================== //
return true;
}
//====================================//

This is where we update the controls in the UI using our data, and update a reference to some data with the information presented in the widget.

Note
The updateData method is the only place that we are permitted to modify the data that our widget is attached to. If we were to store a reference to the data and try to update it during some other event, we cannot guarantee that the underlying workflow will not be modifying - or have deleted - the data. Similarly, it is only safe to update the widget using the data in the updateWidget method.

The next important piece of implementation is the Widget's constructor:

RectangleWidget::RectangleWidget(QWidget* parent) :
QWidget(parent),
pImpl_(std::make_unique<RectangleWidgetImpl>(*this))
{
bool connected = true;
// ====================================== //
// Connect up your signals and slots here //
// (see examples below) //
// ====================================== //
//connected = connect(pImpl_->ui_->addButton, &QPushButton::clicked, this, &RectangleWidget::widgetUpdated);
WS_VERIFY_RUNTIME(connected);
}
#define WS_VERIFY_RUNTIME(cond)
Definition: errorchecks.h:227

Note that it contains a comment indicating that this is the location to connect up any signals from our UI controls to our widgetUpdated signal. Triggering the widgetUpdated signal will cause the updateData function to be called when Workspace deems it safe to do so.

The remainder of the file is simple forwarding functions:

RectangleWidget::~RectangleWidget() = default;
bool RectangleWidget::updateWidget(const CSIRO::Rectangle& data)
{
return pImpl_->updateWidget(data);
}

Rebuilding (continue on with tutorial Writing a Custom Widget )


rectanglewidgetconnector.h

Note
Despite its name, QWidgetConnector is a Workspace class (and not a standard Qt class) - sorry for any confusion this causes.

As discussed above, our RectangleWidgetConnector class extends QWidgetConnector and is used by Workspace to control the safe updating of the widget, as well as the safe updating of the connected data in the underlying Workflow. The header file for the connector is not overly complex. First up, we can see the inclusion of the relevant headers:

We can then see the class declaration with features such as the public inheritance from CSIRO::Widgets::QWidgetConnector, the inclusion of the Q_OBJECT macro for Qt meta-object compilation, and two key virtual methods, updateWidget and updateData. These methods override pure-virtual methods in the base class QWidgetConnector, and accept as parameters DataObjects. When we look at the implementation, we will see that it is the responsibliity of these functions to extract the raw data from these data objects, and either supply it to the widget for display, or to supply it to the widget for updating.

class SIMPLEPLUGIN_API RectangleWidgetConnector : public CSIRO::Widgets::QWidgetConnector
{
Q_OBJECT
void updateWidget(CSIRO::DataExecution::DataObject& dataObject) override;
void updateData(CSIRO::DataExecution::DataObject& dataObject) override;
public:
RectangleWidgetConnector(QWidget& widget, const CSIRO::Widgets::NamePath& namePath);
void setWidgetReadOnly(bool b) override;
};
} // namespace CSIRO
#endif
Base class for all data objects.
Definition: dataobject.h:56
Provides a way to identify a particular input or output of an operation in a (possibly nested) namesp...
Definition: namepath.h:51
Base class for objects handling the connection between data objects and QWidget instances.
Definition: qwidgetconnector.h:73

Rebuilding (continue on with tutorial Writing a Custom Widget )


rectanglewidgetconnector.cpp

Let's have a look at the implementation of the connector. Although we will not need to modify this class, it is useful to understand how it works. As usual, we include a bunch of standard Workspace headers:

#include "rectanglewidgetconnector.h"
#include "rectangle.h"
#include "rectanglewidget.h"
#include "simpleplugin.h"
namespace CSIRO

Our constructor accepts a QWidget as the type, and it is our responsibility to make sure it is of the correct type when it is used. Although the base class will actually store a pointer to the widget, we make sure to assert that it is of the correct type here. We also make sure to connect our widget's widgetUpdated signal to our requestUpdateData slot (defined on the QWidgetConnector base class). This means that whenever the widget emits its widgetUpdated signal, the connector will tell Workspace that it needs to update the underlying data. Workspace will then invoke the updateData method on the connector as soon as it is safe to do so.

RectangleWidgetConnector::RectangleWidgetConnector(QWidget& widget, const CSIRO::Widgets::NamePath& namePath) :
QWidgetConnector(widget, namePath)
{
RectangleWidget* typedWidget = qobject_cast<RectangleWidget*>(&widget);
WS_VERIFY_RUNTIME(typedWidget);
connect(typedWidget, &RectangleWidget::widgetUpdated, this, &RectangleWidgetConnector::requestUpdateData);
}

Next up is the implementation of our updateWidget method:

void RectangleWidgetConnector::updateWidget(CSIRO::DataExecution::DataObject& dataObject)
{
RectangleWidget* widget = qobject_cast<RectangleWidget*>(&getWidget());
if (dataObject.hasData())
{
widget->updateWidget(dataObject.getRawData<CSIRO::Rectangle>());
}
Traits class for data objects of type T.
Definition: datafactorytraits.h:143
T & getRawData()
Definition: dataobject.h:78
virtual bool hasData(bool recurse=true) const =0
virtual const DataFactory & getFactory() const =0

Here we can see that the method takes a DataObject, which from our previous tutorials we will remember as being a generic pointer to some data of a particular Workspace type. It then does the following:

  1. Gets the widget from the base class
  2. Using getWidget(), casts it to the correct type, RectangleWidget, and asserts that it's still valid
  3. Checks that the data factory associated with our DataObject is for the Rectangle type
  4. Checks that the DataObject contains data and if it does...
  5. Invokes the updateWidget method on the widget, passing in a reference to the raw data contained within the DataObject, which since we have checked the factories we know to be of type Rectangle.

Our updateData method is very similar:

void RectangleWidgetConnector::updateData(CSIRO::DataExecution::DataObject& dataObject)
{
RectangleWidget* widget = qobject_cast<RectangleWidget*>(&getWidget());
dataObject.ensureHasData();
widget->updateData(dataObject.getRawData<CSIRO::Rectangle>());
}
virtual void ensureHasData(bool recurse=true)=0

It does the same thing as the updateWidget method above, but invokes the updateData method of the RectangleWidget, passing in a non-const reference to the Rectangle data it extracts from the DataObject.

The setWidgetReadOnly method is called by the Workspace editor to toggle read only behaviour for your widget when it is attached to a data object that has read only access. The base class implementation of setWidgetReadOnly will simply disable your widget. If instead your widget requires partial interactivity while in read only mode, you can specify your widget's read only customisations here.

void RectangleWidgetConnector::setWidgetReadOnly(bool b)
{
}
virtual void setWidgetReadOnly(bool b)
Definition: qwidgetconnector.cpp:144

For this tutorial we'll use the default base class implementation.


Rebuilding (continue on with tutorial Writing a Custom Widget )


rectanglewidgetfactory.h

Similar to the WidgetConnector, the WidgetFactory files will not need to be modified. We are only looking at these to understand how the code works. As described above, the RectangleWidgetFactory is used by Workspace to create our RectangleWidget and our RectangleWidgetConnector. It's header file begins with the usual header guards, header includes and forward declarations:

#ifndef CSIRO_RECTANGLEWIDGET_FACTORY_H
#define CSIRO_RECTANGLEWIDGET_FACTORY_H
#include "simpleplugin.h"
namespace CSIRO
{
namespace Widgets
{
class NamePath;
class QWidgetConnector;
} // namespace Widgets
namespace DataExecution
{
class DataFactory;
}
} // namespace CSIRO

As we can see below, the class itself extends CSIRO::Widgets::WidgetFactory; the base class for creating widgets in Workspace:

class SIMPLEPLUGIN_API RectangleWidgetFactory : public CSIRO::Widgets::WidgetFactory
{
QWidget& createWidgetImpl(QWidget* parent) const override;
CSIRO::Widgets::QWidgetConnector& createWidgetConnectorImpl(QWidget& widget, const CSIRO::Widgets::NamePath& namePath) const override;
public:
static RectangleWidgetFactory& getInstance();
const QMetaObject& getQWidgetMetaObject() const override;
const CSIRO::DataExecution::DataFactory& getDataFactory() const override;
};
Base class for all data type factories.
Definition: datafactory.h:71
Definition: widgetfactory.h:63

It contains a number of important method declarations:

  1. createWidgetImpl: implementation method that creates our RectangleWidget and returns a reference to it
  2. createWidgetConnectorImpl: method that creates a new RectangleWidgetConnector and returns a reference to it
  3. getInstance: all WidgetFactories are Singletons, so this method returns a reference to the single instance of the factory.
  4. getQWidgetMetaObject: provides information to Workspace about the Widget's meta-object properties (those that qt generated during the meta-object compilation process). This is used by parts of the widget manager code that we don't generally need to the details of.
  5. getDataFactory: returns a reference to the data factory associated with our widget. In our case, this will be Rectangle.

Rebuilding (continue on with tutorial Writing a Custom Widget )


rectanglewidgetfactory.cpp

Let's have a look at the implementation of the methods in the WidgetFactory, defined in the .cpp file, starting with the getInstance function:

RectangleWidgetFactory& RectangleWidgetFactory::getInstance()
{
static RectangleWidgetFactory factory;
return factory;
}

As we can see, all it does is declare a static instance of the RectangleWidgetFactory and returns an instance of it.

const QMetaObject& RectangleWidgetFactory::getQWidgetMetaObject() const
{
return RectangleWidget::staticMetaObject;
}

The getQWidgetMetaObject function doesn't do much. It just returns a reference to the static meta object declared in our widget.

const CSIRO::DataExecution::DataFactory& RectangleWidgetFactory::getDataFactory() const
{
}
static DataFactory & getInstance(bool justThisDataFactory=false)

As expected, our getDataFactory method returns a reference of the Rectangle data factory. Lastly, our createWidget and createWidgetConnector methods are also straightforward.

QWidget& RectangleWidgetFactory::createWidgetImpl(QWidget* parent) const
{
return *(new RectangleWidget(parent));
}
CSIRO::Widgets::QWidgetConnector& RectangleWidgetFactory::createWidgetConnectorImpl(QWidget& widget, const CSIRO::Widgets::NamePath& namePath) const
{
return *new RectangleWidgetConnector(widget, namePath);
}

Rebuilding (continue on with tutorial Writing a Custom Widget )


Changes to the SimplePlugin implementation

As expected, the wizard has made a number of changes to our setup() function in the SimplePlugin implementation. First of all, we can see that it's added rectanglewidgetfactory.h to the include headers:

#include <QString>
#include <QStringList>
#include "calculaterectanglearea.h"
#include "rectangle.h"
#include "rectanglewidgetfactory.h"

If we look at the setup function, we'll also see a new line adding the widget factory to our plugin.

bool SimplePlugin::setup()
{
addFactory(RectangleWidgetFactory::getInstance());
// Add your data factories like this:
//addFactory( CSIRO::DataExecution::DataFactoryTraits<MyDataType>::getInstance() );
// Add your operation factories like this:
//addFactory( CSIRO::DataExecution::OperationFactoryTraits<MyOperation>::getInstance() );
// Add your widget factories like this:
//addFactory( MyNamespace::MyWidgetFactory::getInstance() );
// Add your adaptor factories like this:
//addFactory( CSIRO::DataExecution::SimpleAdaptorFactory<MyDataType, OtherDataType>::getInstance());
// Add your collection workflows here like this:
//addWorkspaceCollection(":/appdata/<MyPlugin>/myworkflow.wsx");
return true;
}
Traits class for operations of type T.
Definition: operationfactorytraits.h:64

Rebuilding (continue on with tutorial Writing a Custom Widget )


Changes to the CMakeLists file

The CMakeLists.txt file has also been modified to contain references to the new files. As we can see below, the HEADERS, INSTALL_HEADERS, MOC_HEADERS, SOURCES and UI_SOURCES have all been updated to refer to our newly generated files:

set(HEADERS
${SIMPLEPLUGIN_SOURCE_DIR}/rectanglewidgetconnector.h
${SIMPLEPLUGIN_SOURCE_DIR}/rectanglewidgetfactory.h
${SIMPLEPLUGIN_SOURCE_DIR}/rectanglewidget.h
${SIMPLEPLUGIN_SOURCE_DIR}/rectangle.h
${SIMPLEPLUGIN_SOURCE_DIR}/calculaterectanglearea.h
${SIMPLEPLUGIN_SOURCE_DIR}/simpleplugin_api.h
${SIMPLEPLUGIN_SOURCE_DIR}/simpleplugin.h
)
set(INSTALL_HEADERS
${SIMPLEPLUGIN_SOURCE_DIR}/rectanglewidgetconnector.h
${SIMPLEPLUGIN_SOURCE_DIR}/rectanglewidgetfactory.h
${SIMPLEPLUGIN_SOURCE_DIR}/rectanglewidget.h
${SIMPLEPLUGIN_SOURCE_DIR}/rectangle.h
${SIMPLEPLUGIN_SOURCE_DIR}/calculaterectanglearea.h
${SIMPLEPLUGIN_SOURCE_DIR}/simpleplugin_api.h
${SIMPLEPLUGIN_SOURCE_DIR}/simpleplugin.h
)
set(MOC_HEADERS
${SIMPLEPLUGIN_SOURCE_DIR}/rectanglewidgetconnector.h
${SIMPLEPLUGIN_SOURCE_DIR}/rectanglewidget.h
)
set(SOURCES
${SIMPLEPLUGIN_SOURCE_DIR}/rectanglewidgetconnector.cpp
${SIMPLEPLUGIN_SOURCE_DIR}/rectanglewidgetfactory.cpp
${SIMPLEPLUGIN_SOURCE_DIR}/rectanglewidget.cpp
${SIMPLEPLUGIN_SOURCE_DIR}/rectangle.cpp
${SIMPLEPLUGIN_SOURCE_DIR}/calculaterectanglearea.cpp
${SIMPLEPLUGIN_SOURCE_DIR}/simpleplugin.cpp
)
set(UI_SOURCES
${SIMPLEPLUGIN_SOURCE_DIR}/rectanglewidget.ui
)

Rebuilding (continue on with tutorial Writing a Custom Widget )