Designing Visual Editors in Qt

Материал из Wiki.crossplatform.ru

Версия от 18:34, 20 февраля 2009; Lit-uriy (Обсуждение | вклад)
(разн.) ← Предыдущая | Текущая версия (разн.) | Следующая → (разн.)
Перейти к: навигация, поиск
Image:qt-logo_new.png Image:qq-title-article.png
Qt Quarterly | Выпуск 22 | Документация


by Prashanth N. Udupa

Much of the scientific and visualization software in the world today is designed tobe 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 replacesAPI interaction with quick-feedback graphical interaction that most end-userscan use with ease.

Содержание

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

In principle the solver/visual editor paradigm is very similar to themodel/view paradigm found in Qt. A model is a software object that can beinteracted with using the API it exposes. Views are used to display modelobjects, 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 efficientvisual editors for solver backends.

[править] Solver Systems

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

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

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

As the solver systemmatures, it may become necessary to ensure that even non-programmers canmake use of the solver. Sometimes even the programmers developing the solversystem 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 thefollowing 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 animationscan 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, wecan identify some design patterns that can help when designing most solversystems. This article specifically deals with how we can effectively use Qtto implement problem objects, a problem canvas, property editors and outputviewers—all key parts of a solver system's design.

[править] Problem Objects

Problem objects in a solver system describe aspects of a problem. Theymay expose one or more configurable properties and events. A programmer woulduse the getter and setter functions associated with properties, and callbacksassociated with events, to configure the object. In a visual editor,configurable properties are shown in a property editor, and events are eitherhandled internally or exposed as scriptable routines in the frontend.

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

center

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

vtkConeSource, vtkPolyDataNormals, vtkPolyDataMapper,vtkActor, vtkRenderer and vtkRenderWindow areproblem 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, Resolutionand BaseRadius which can be adjusted programmatically using theappropriate getter and setter functions to alter the output. For example, if weset the Resolution property to 6 in the above pipeline, we get output asshown in the right-hand image below.

center

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 queryproperty names and change their values. Most solver systems may not providemechanisms to let us query or change their problem objects, therefore we caneither modify the solver system or wrap problem objects in another layer thatprovides such mechanisms. Modifying the solver system may not be a practicalsolution in most cases because that may involve re-engineering the solverbackend or, worse still, we may not have access to the source code of thesolver.

Wrapping, on the other hand, is a more practical approach. Wrapping involvesproviding access to backend methods and objects via another layer. This layercould be a wrapper class. A wrapper class essentially manages anotherobject.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 awrapped 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 theobjects— each vtkConeSource object is constructed and destroyedalong with its corresponding ConeSourceWrapper object—and itprovides wrapper methods that are used to access methods withinvtkConeSource.

[править] Wrapping Properties

Now, you might wonder why writing such classes can be of any use, since allwe have done so far is duplicate the getter and setter functions ofvtkConeSource in the wrapper class. Let's take a look at a slightlymodified 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> &amp; 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 wrapperclass:

  • The wrapper class is now a subclass of QObject.
  • Information about the class hierarchy that the wrapper class belongs tocan be obtained using QMetaObject functions like className() andsuperClass().
  • Property names can be queried by using QMetaObject methods on themeta-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 slotsexposed by Qt objects. In a visual editor environment events are representedby signals and commands are represented by slots. By using Qt's meta-objectsystem one can easily support events and commands. However, a completedescription 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, sothat those patterns can be accessed transparently for all problem objects.

For example, objects that take part in a VTK pipeline accept one or moreinputs and produce one or more outputs. Each input and output path has aspecific data type. The output of one object can be used as the input toanother object. Wrapper classes for VTK will therefore have to expose theseconnection paths and the methods that perform connections via a commoninterface.

To ensure that all wrappers employ common mechanisms to makeand break such connections, we will need to ensure that all wrappers arederived from a single class that provides virtual functions to performthese 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 &amp;linkData);
  virtual bool removeInput(int index,
               const CWrapperLinkData &amp;linkData);
  virtual bool getOutput(int index,
               CWrapperLinkData &amp;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 classfor 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() methodsto 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 thesereimplementations to establish and break links between wrappers. SinceCWrapper 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 canbe 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-positionproblem objects that have already been placed on the canvas, select one ormore problem objects, and also draw relationships or connections betweenproblem objects.
  • A problem canvas can provide optional features for zooming in and outto show the scenario at different levels of detail.

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

In the previous section we saw that by creating a framework class calledCWrapper as a subclass of QObject we wereable to provide transparent access to properties and connections. Here, we willmodify the architecture of CWrapper a bit to make it more usablewithin the problem canvas.

center

To provide the functionality of a problem canvas, CWrapperCanvas, themost ideal class to derive from would be QGraphicsScene. If we derivedCWrapper from QGraphicsRectItem,we could place instances of CWrapper on the problem canvas. Torepresent connections, we would have to create a new class calledCWrapperLink, 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 createsubclasses of CWrapper for each and every VTK class we need to supportin the frontend.For the VTK pipeline explained in the previous section, we will need to createsix wrappers.

[править] Property Editors

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

  • It should list all the properties supported by a given objectalong with their values.
  • It should provide intuitive editors to configure the value of aproperty.
  • It should optionally provide the capability to undo and redochanges made to properties.

The Qt Solutions kit comes with a robust "Property Browser" framework thatcan be used (almost off the shelf) as a property editor. If you do not haveaccess 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 associatescripts with them. With Qt 4.3 you can make use of the QtScript module toassociate JavaScript code with events.

[править] Output Viewers

An output viewer is a user interface component that shows the output of aprocess or the solution given by the solver. Some solvers have implicitoutput viewers; for example, VTK has a vtkRenderWindow that shows thevisualized output. For solvers that do not have their own output viewers, wewill have to implement custom output viewing mechanisms. Output viewers aremostly specific to the solver, hence a complete description of them is beyondthe scope of this article.

[править] Conclusions and a Demo

In this article, we have seen how easy it is to construct the building blocksfor 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.

center

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