Wiki

Designing Visual Editors in Qt

by Prashanth N. Udupa

Much of the scientific and visualization software in the world today is designed to be constructed from two separate parts: a visual frontend and a solver backend. The solver backend specializes in solving a problem within a specific domain, and can be difficult to interact with, whereas the visual frontend replaces API interaction with quick-feedback graphical interaction that most end-users can use with ease.

When we look around, we can find solver and visual editor designs in almost all software. A database application, for example, makes use of SQL statements to interact with database backends, while the user interface presents the data in a more human-readable form. Scene graph systems can be thought of as solver backends for 3D visualization and modeling systems. Objects from the scene graph can be represented in several ways in the frontend for the user to work and interact with.

In principle the solver/visual editor paradigm is very similar to the model/view paradigm found in Qt. A model is a software object that can be interacted with using the API it exposes. Views are used to display model objects, and they help users to visually interact with the model.

In this article, we will look at how we can make use of Qt to design efficient visual editors for solver backends.

Solver Systems

Solver systems exist for different problem domains. Let's take the 3D visualization domain, for example. VTK (www.vtk.org) is a solver system for visualization problems. With VTK, one can give visual meaning to several kinds of data.

Similarly, there are solver systems for several other problem domains. QualNet (www.scalable-networks.com) is a solver for problems in the network simulation world. Using QualNet, one can simulate complex networks.

Most solver systems provide classes or simple C APIs to help construct problem scenarios, solve those scenarios, and to fetch the solution arrived at by the solver afterwards. A programmer can effectively make use of one or more classes in each of these categories to solve a problem and study the solution.

As the solver system matures, it may become necessary to ensure that even non-programmers can make use of the solver. Sometimes even the programmers developing the solver system may require easy-to-use interfaces for working with it. This is where visual editors come in.

Visual Editors

Visual editors for solver backends provide a graphical way to perform the following tasks:

  • Assembling the objects into a problem scenario.
  • Configuring the object properties and drawing relationships between them.
  • Instructing the solver to solve the problem. Message logs and animations can be used to show error, progress, and status messages.
  • Viewing the results given by the solver.

While the visual editors used are typically specific to each solver system, we can identify some design patterns that can help when designing most solver systems. This article specifically deals with how we can effectively use Qt to implement problem objects, a problem canvas, property editors and output viewers—all key parts of a solver system's design.

Problem Objects

Problem objects in a solver system describe aspects of a problem. They may expose one or more configurable properties and events. A programmer would use the getter and setter functions associated with properties, and callbacks associated with events, to configure the object. In a visual editor, configurable properties are shown in a property editor, and events are either handled internally or exposed as scriptable routines in the frontend.

Let's consider a real world example to understand problem objects better.

A visual editor pipeline

Suppose we wanted to visualize a 3D cone in VTK. To do this, we would have to assemble a pipeline as shown in the diagram on the left.

vtkConeSource, vtkPolyDataNormals, vtkPolyDataMapper, vtkActor, vtkRenderer and vtkRenderWindow are problem objects. They are connected to form a visualization pipeline which, when executed, produces the output as shown in the left-hand image below.

vtkConeSource has some properties like Height, Resolution and BaseRadius which can be adjusted programmatically using the appropriate getter and setter functions to alter the output. For example, if we set the Resolution property to 6 in the above pipeline, we get output as shown in the right-hand image below.

Different resolutions

In a visual editor, we would want problem objects to be graphically configurable. To enable this, we would have to create some mechanisms to transparently query property names and change their values. Most solver systems may not provide mechanisms to let us query or change their problem objects, therefore we can either modify the solver system or wrap problem objects in another layer that provides such mechanisms. Modifying the solver system may not be a practical solution in most cases because that may involve re-engineering the solver backend or, worse still, we may not have access to the source code of the solver.

Wrapping, on the other hand, is a more practical approach. Wrapping involves providing access to backend methods and objects via another layer. This layer could be a wrapper class. A wrapper class essentially manages another object. It provides means for accessing the methods on that class, and also ensures the "health" of the class at all times. The object that is wrapped is called a wrapped object, and its class is called a wrapped class.

Here's a simple Qt 3-based wrapper class for vtkConeSource:

#include "vtkConeSource.h"
class ConeSourceWrapper
{
public:
    ConeSourceWrapper() {
        _vtkConeSource = vtkConeSource::New();
    }
    ~ConeSourceWrapper() { 
        _vtkConeSource->Delete();
    }
    void setHeight(double v) {
        _vtkConeSource->SetHeight(v);
    }
    double getHeight() const {
        return _vtkConeSource->GetHeight();
    }
    void setCenter(float x, float y, float z) {
        _vtkConeSource->SetCenter(x, y, z);
    }
    void getCenter(float &x, float &y, float &z) const {
        float v[3];
        _vtkConeSource->GetCenter(v);
        x = v[0];
        y = v[1];
        z = v[2];
    }
    /* Other properties ... */
 
protected:
    vtkConeSource* _vtkConeSource;
};

ConeSourceWrapper does two things: It manages the lifetime of the objects— each vtkConeSource object is constructed and destroyed along with its corresponding ConeSourceWrapper object—and it provides wrapper methods that are used to access methods within vtkConeSource.

Wrapping Properties

Now, you might wonder why writing such classes can be of any use, since all we have done so far is duplicate the getter and setter functions of vtkConeSource in the wrapper class. Let's take a look at a slightly modified version:

class ConeSourceWrapper : public QObject
{
  Q_OBJECT
  Q_PROPERTY(double Height READ getHeight WRITE setHeight)
  Q_PROPERTY(QValueList Center READ getCenter
             WRITE setCenter)
public:
    ConeSourceWrapper() {
        _vtkConeSource = vtkConeSource::New();
    }
    ~ConeSourceWrapper() { 
        _vtkConeSource->Delete();
    }
    void setHeight(double v) {
        _vtkConeSource->SetHeight(v);
    }
    double getHeight() const {
        return _vtkConeSource->GetHeight();
    }
    void setCenter(const QValueList <QVariant> & val) {
        _vtkConeSource->SetCenter(val[0].toDouble(), 
                                  val[1].toDouble(), 
                                  val[2].toDouble());
    }
    const QValueList <QVariant> getCenter() const {
        QValueList <QVariant> ret;
        float v[3];
        _vtkConeSource->GetCenter(v);
        ret.append( QVariant(v[0]) );
        ret.append( QVariant(v[1]) );
        ret.append( QVariant(v[2]) );
        return ret;
    }
    /* Other properties ... */
 
protected:
    vtkConeSource* _vtkConeSource;
};

The following things have been changed in the second version of the wrapper class:

  • The wrapper class is now a subclass of QObject.
  • Information about the class hierarchy that the wrapper class belongs to can be obtained using QMetaObject functions like className() and superClass().
  • Property names can be queried by using QMetaObject methods on the meta-object associated with ConeSourceWrapper.
  • Properties can be queried using setProperty() and property() methods on instances of ConeSourceWrapper.

QMetaObject also provides mechanisms for querying the signals and slots exposed by Qt objects. In a visual editor environment events are represented by signals and commands are represented by slots. By using Qt's meta-object system one can easily support events and commands. However, a complete description of this is beyond the scope of this article.

Wrapping Other Behaviors

Problem objects are not just containers for properties, signals and slots. They may have custom behavioral patterns that need to be regularized, so that those patterns can be accessed transparently for all problem objects.

For example, objects that take part in a VTK pipeline accept one or more inputs and produce one or more outputs. Each input and output path has a specific data type. The output of one object can be used as the input to another object. Wrapper classes for VTK will therefore have to expose these connection paths and the methods that perform connections via a common interface.

To ensure that all wrappers employ common mechanisms to make and break such connections, we will need to ensure that all wrappers are derived from a single class that provides virtual functions to perform these tasks. Here is the code for one such wrapper class:

class CWrapper : public QObject
{
  Q_OBJECT
  Q_PROPERTY (QString Name READ name WRITE setName)
 
public:
  static bool link(CWrapper *output, int outLine,
      CWrapper *input, int inLine, QString *errorMsg=0);
  static bool unlink(CWrapper *output, int outLine,
      CWrapper *input, int inLine, QString *errorMsg=0);
  ...
  virtual int inputLinkCount() const;
  virtual CWrapperLinkDesc inputLinkDesc(int index) const;
  virtual int outputLinkCount() const;
  virtual CWrapperLinkDesc outputLinkDesc(int index)
          const;
  ...
 
public slots:
  void setName(const QString name);
  void removeInputLinks();
  void removeOutputLinks();
  void removeAllLinks();
 
protected:
  virtual bool hasInput(int index);
  virtual bool setInput(int index,
               const CWrapperLinkData &linkData);
  virtual bool removeInput(int index,
               const CWrapperLinkData &linkData);
  virtual bool getOutput(int index,
               CWrapperLinkData &output);
  virtual void setVtkObject(vtkObject* object);
  ...
  void addLink(CWrapperLink *link);
  void removeLink(CWrapperLink *link);
...
};

From the above code you can see that, if we make CWrapper the base class for all wrappers in the visual editor for creating and editing VTK pipelines, we can do the following for each subclass:

  • Reimplement the inputLinkCount() and outputLinkCount() methods to return the number of input and output paths the wrapper supports.
  • Reimplement inputLinkDesc() and outputLinkDesc() methods to return information about the links.
  • Reimplement hasInput(), setInput(), removeInput() and getOutput() to actually establish input and output paths.

The link() and unlink() functions make use of these reimplementations to establish and break links between wrappers. Since CWrapper is a subclass of QObject, subclasses of CWrapper can provide transparent access to properties, too.

Problem Canvases

A problem canvas can be thought of as a surface on which problem objects can be placed, configured and connected together to create a problem scenario. A problem canvas should ideally have these properties:

  • It should provide ample space to place problem objects on.
  • It should provide sufficient user interface mechanisms to re-position problem objects that have already been placed on the canvas, select one or more problem objects, and also draw relationships or connections between problem objects.
  • A problem canvas can provide optional features for zooming in and out to show the scenario at different levels of detail.

The Graphics View framework in Qt 4.2 provides an excellent framework with which to design such modules, enabling us to manage and interact with a large number of custom-made 2D graphical items, and it supports features such as zooming and rotation that can help to visualize the items on a problem canvas.

In the previous section we saw that by creating a framework class called CWrapper as a subclass of QObject we were able to provide transparent access to properties and connections. Here, we will modify the architecture of CWrapper a bit to make it more usable within the problem canvas.

The class hierarchy

To provide the functionality of a problem canvas, CWrapperCanvas, the most ideal class to derive from would be QGraphicsScene. If we derived CWrapper from QGraphicsRectItem, we could place instances of CWrapper on the problem canvas. To represent connections, we would have to create a new class called CWrapperLink, which would be a subclass of QGraphicsLineItem. The graphics scene, CWrapperCanvas could then be shown in a QGraphicsView subclass.

With the above framework in place, all we will need to do is to create subclasses of CWrapper for each and every VTK class we need to support in the frontend. For the VTK pipeline explained in the previous section, we will need to create six wrappers.

Property Editors

A property editor is a user interface component in the front end that helps users configure values of properties exposed by problem object wrappers. We would expect the following features from a property editor:

  • It should list all the properties supported by a given object along with their values.
  • It should provide intuitive editors to configure the value of a property.
  • It should optionally provide the capability to undo and redo changes made to properties.

The Qt Solutions kit comes with a robust "Property Browser" framework that can be used (almost off the shelf) as a property editor. If you do not have access to Qt Solutions, then you can subclass from QTreeWidget or QListWidget to create a simple property editor.

A property editor typically shows all editable properties exposed by any QObject subclass and allows the user to edit them. Optionally, it may also list signals emitted by the QObject and provide a user interface to associate scripts with them. With Qt 4.3 you can make use of the QtScript module to associate JavaScript code with events.

Output Viewers

An output viewer is a user interface component that shows the output of a process or the solution given by the solver. Some solvers have implicit output viewers; for example, VTK has a vtkRenderWindow that shows the visualized output. For solvers that do not have their own output viewers, we will have to implement custom output viewing mechanisms. Output viewers are mostly specific to the solver, hence a complete description of them is beyond the scope of this article.

Conclusions and a Demo

In this article, we have seen how easy it is to construct the building blocks for a framework that we can use to visualize problems and their solutions. Qt provides many of the user interface features to make this possible and, via its meta-object system, allows us to do this in an extensible way.

The visual editor demo

Accompanying this article is a complete working demo of a simple VTK pipeline editor. The code for the demo illustrates how the above principles can be used to create a visual editor for VTK. (The libraries you need can be obtained from www.vtk.org.)


Copyright © 2007 Trolltech Trademarks