Home | All Classes | Main Classes | Annotated | Grouped Classes | Functions

Data Elements

We will use a C++ class called Element to provide storage and access for data elements.

(Extracts from element.h.)

    private:
        double m_value;
        QColor m_valueColor;
        int m_valuePattern;
        QString m_label;
        QColor m_labelColor;
        double m_propoints[2 * MAX_PROPOINTS];

Each element has a value. Each value is displayed graphically with a particular color and fill pattern. Values may have a label associated with them; the label is drawn using the label color and for each type of chart has a (relative) position stored in the m_propoints array.

    #include <qcolor.h>
    #include <qnamespace.h>
    #include <qstring.h>
    #include <qvaluevector.h>

Although the Element class is a purely internal data class, it #includes four Qt classes. Qt is often perceived as a purely GUI toolkit, but it provides many non-GUI classes to support most aspects of application programming. We use qcolor.h so that we can hold the paint color and text color in the Element class. The use of qnamespace.h is slightly obscure. Most Qt classes are derived from the Qt superclass which contains various enumerations. The Element class does not derive from Qt, so we need to include qnamespace.h to have access to the Qt enum names. An alternative approach would have been to have made Element a Qt subclass. We include qstring.h to make use of Qt's Unicode strings. As a convenience we will typedef a vector container for Elements, which is why we pull in the qvaluevector.h header.

    typedef QValueVector<Element> ElementVector;

Qt provides a number of containers, some value based like QValueVector, and others pointer based. (See Collection Classes.) Here we've just typedefed one container type; we will keep each data set of elements in one ElementVector.

    const double EPSILON = 0.0000001; // Must be > INVALID.

Elements may only have positive values. Because we hold values as doubles we cannot readily compare them with zero. Instead we specify a value, EPSILON, which is close to zero, and consider any value greater than EPSILON to be positive and valid.

    class Element
    {
    public:
        enum { INVALID = -1 };
        enum { NO_PROPORTION = -1 };
        enum { MAX_PROPOINTS = 3 }; // One proportional point per chart type

We define three public enums for Elements. INVALID is used by the isValid() function. It is useful because we are going to use a fixed size vector of Elements, and can mark unused Elements by giving them INVALID values. The NO_PROPORTION enum is used to signify that the user has not positioned the Element's label; any positive proportion value is taken to be the text element's position proportional to the canvas's size.

If we stored each label's actual x and y position, then every time the user resized the main window (and therefore the canvas), the text would retain its original (now incorrect) position. So instead of storing absolute (x, y) positions we store proportional positions, i.e. x/width and y/height. We can then multiply these positions by the current width and height respectively when we come to draw the text and the text will be positioned correctly regardless of any resizing. For example, if a label has an x position of 300 and the canvas is 400 pixels wide, the proportional x value is 300/400 = 0.75.

The MAX_PROPOINTS enum is problematic. We need to store the x and y proportions for the text label for every chart type. And we have chosen to store these proportions in a fixed-size array. Because of this we must specify the maximum number of proportion pairs needed. This value must be changed if we change the number of chart types, which means that the Element class is strongly coupled to the number of chart types provided by the ChartForm class. In a larger application we might have used a vector to store these points and dynamically resized it depending on how many chart types are available.

        Element( double value = INVALID, QColor valueColor = Qt::gray,
                 int valuePattern = Qt::SolidPattern,
                 const QString& label = QString::null,
                 QColor labelColor = Qt::black ) {
            init( value, valueColor, valuePattern, label, labelColor );
            for ( int i = 0; i < MAX_PROPOINTS * 2; ++i )
                m_propoints[i] = NO_PROPORTION;
        }

The constructor provides default values for all members of the Element class. New elements always have label text with no position. We use an init() function because we also provide a set() function which works like the constructor apart from leaving the proportional positions alone.

        bool isValid() const { return m_value > EPSILON; }

Since we are storing Elements in a fixed size vector we need to be able to check whether a particular element is valid (i.e. should be used in calculations and displayed) or not. This is easily achieved with the isValid() function.

(Extracts from element.cpp.)

    double Element::proX( int index ) const
    {
        Q_ASSERT(index >= 0 && index < MAX_PROPOINTS);
        return m_propoints[2 * index];
    }

Getters and setters are provided for all the members of Element. The proX() and proY() getters and the setProX() and setProY() setters take an index which identifies the type of chart the proportional position applies to. This means that the user can have labels positioned separately for the same data set for a vertical bar chart, a horizontal bar chart and for a pie chart. Note also that we use the Q_ASSERT macro to provide pre-condition tests on the chart type index; (see Debugging).

Reading and Writing Data Elements

(Extracts from element.h.)

    QTextStream &operator<<( QTextStream&, const Element& );
    QTextStream &operator>>( QTextStream&, Element& );

To make our Element class more self-contained we provide overloads for the << and >> operators so that Elements may be written to and read from text streams. We could just as easily have used binary streams, but using text makes it possible for users to manipulate their data using a text editor and makes it easier to generate and filter the data using a scripting language.

(Extracts from element.cpp.)

    #include "element.h"

    #include <qstringlist.h>
    #include <qtextstream.h>

Our implementation of the operators requires the inclusion of qtextstream.h and qstringlist.h.

    const char FIELD_SEP = ':';
    const char PROPOINT_SEP = ';';
    const char XY_SEP = ',';

The format we are using to store the data is colon separated fields and newline separated records. The proportional points are semi-colon separated, with their x, y pairs being comma separated. The field order is value, value color, value pattern, label color, label points, label text. For example:

20:#ff0000:14:#000000:0.767033,0.412946;0,0.75;0,0:Red :with colons:!
70:#00ffff:2:#ffff00:0.450549,0.198661;0.198516,0.125954;0,0.198473:Cyan
35:#0000ff:8:#555500:0.10989,0.299107;0.397032,0.562977;0,0.396947:Blue
55:#ffff00:1:#000080:0.0989011,0.625;0.595547,0.312977;0,0.59542:Yellow
80:#ff00ff:1:#000000:0.518681,0.694196;0.794063,0;0,0.793893:Magenta or Violet

There's no problem having whitespace and field separators in label text due to the way we read Element data.

    QTextStream &operator<<( QTextStream &s, const Element &element )
    {
        s << element.value() << FIELD_SEP
          << element.valueColor().name() << FIELD_SEP
          << element.valuePattern() << FIELD_SEP
          << element.labelColor().name() << FIELD_SEP;

        for ( int i = 0; i < Element::MAX_PROPOINTS; ++i ) {
            s << element.proX( i ) << XY_SEP << element.proY( i );
            s << ( i == Element::MAX_PROPOINTS - 1 ? FIELD_SEP : PROPOINT_SEP );
        }

        s << element.label() << '\n';

        return s;
    }

Writing elements is straight-forward. Each member is written followed by a field separator. The points are written as comma separated (XY_SEP) x, y pairs, each pair separated by the PROPOINT_SEP separator. The final field is the label followed by a newline.

    QTextStream &operator>>( QTextStream &s, Element &element )
    {
        QString data = s.readLine();
        element.setValue( Element::INVALID );

        int errors = 0;
        bool ok;

        QStringList fields = QStringList::split( FIELD_SEP, data );
        if ( fields.count() >= 4 ) {
            double value = fields[0].toDouble( &ok );
            if ( !ok )
                errors++;
            QColor valueColor = QColor( fields[1] );
            if ( !valueColor.isValid() )
                errors++;
            int valuePattern = fields[2].toInt( &ok );
            if ( !ok )
                errors++;
            QColor labelColor = QColor( fields[3] );
            if ( !labelColor.isValid() )
                errors++;
            QStringList propoints = QStringList::split( PROPOINT_SEP, fields[4] );
            QString label = data.section( FIELD_SEP, 5 );

            if ( !errors ) {
                element.set( value, valueColor, valuePattern, label, labelColor );
                int i = 0;
                for ( QStringList::iterator point = propoints.begin();
                    i < Element::MAX_PROPOINTS && point != propoints.end();
                    ++i, ++point ) {
                    errors = 0;
                    QStringList xy = QStringList::split( XY_SEP, *point );
                    double x = xy[0].toDouble( &ok );
                    if ( !ok || x <= 0.0 || x >= 1.0 )
                        errors++;
                    double y = xy[1].toDouble( &ok );
                    if ( !ok || y <= 0.0 || y >= 1.0 )
                        errors++;
                    if ( errors )
                        x = y = Element::NO_PROPORTION;
                    element.setProX( i, x );
                    element.setProY( i, y );
                }
            }
        }

        return s;
    }

To read an element we read one record (i.e. one line). We break the data into fields using QStringList::split(). Because it is possible that a label will contain FIELD_SEP characters we use QString::section() to extract all the text from the last field to the end of the line. If there are enough fields and the value, colors and pattern data is valid we use Element::set() to write this data into the element; otherwise we leave the element INVALID. We then iterate through the points. If the x and y proportions are valid and in range we set them for the element. If one or both proportions is invalid they will hold the value zero; this is not suitable so we change invalid (and out-of-range) proportional point values to NO_PROPORTION.

Our Element class is now sufficient to store, manipulate, read and write element data. We have also created an element vector typedef for storing a collection of elements.

We are now ready to create main.cpp and the user interface through which our users will create, edit and visualise their data sets.

For more information on Qt's data streaming facilities see QDataStream Operators' Formats, and see the source code for any of the Qt classes mentioned that are similar to what you want to store.

« The 'Big Picture' | Contents | Mainly Easy »


Copyright © 2003 TrolltechTrademarks
Qt version 3.2.0b2