Qt logo


Chapter 12: Hanging in the Air the Way Bricks Don't


Screenshot of tutorial twelve

In this example, we extend our LCDRange class to include a text label. We also provide something to shoot at.

Line by Line Walk-Through

lcdrange.h

The LCDRange now has a text label.

    class QLabel;

We name declare QLabel since we want to use a pointer to it in the class definition.

    class LCDRange : public QWidget
    {
        Q_OBJECT
    public:
        LCDRange( QWidget *parent=0, const char *name=0 );
        LCDRange( const char *s, QWidget *parent=0, const char *name=0 );

We have added a new constructor that sets the label text in addition to the parent and name.

        const char *text()  const;

This function returns the label text.

        void setText( const char * );

This slot sets the label text.

    private:
        void init();

Since we now have two constructors, we have chosen to put the common initialization in the private init() function.

        QLabel      *label;

We also have a new private variable; a QLabel. QLabel is one of Qt's standard widgets and can show a text or a pixmap with or without a frame.

lcdrange.cpp

    #include <qlabel.h>

Here we include the QLabel class definition.

    LCDRange::LCDRange( QWidget *parent, const char *name )
            : QWidget( parent, name )
    {
        init();
    }

This constructor calls the init() function, which contains the common initialization code.

    LCDRange::LCDRange( const char *s, QWidget *parent, const char *name )
            : QWidget( parent, name )
    {
        init();
        setText( s );
    }

This constructor first calls init(), then sets the label text.

    void LCDRange::init()
    {
        lcd  = new QLCDNumber( 2, this, "lcd"  );
        lcd->move( 0, 0 );
        sBar = new QScrollBar( 0, 99,                       // range
                               1, 10,                       // line/page steps
                               0,                           // inital value
                               QScrollBar::Horizontal,      // orientation
                               this, "scrollbar" );
        label  = new QLabel( this, "label"  );
        label->setAlignment( AlignCenter );
        connect( sBar, SIGNAL(valueChanged(int)), lcd, SLOT(display(int)) );
        connect( sBar, SIGNAL(valueChanged(int)), SIGNAL(valueChanged(int)) );
    
    }

The set up of lcd and sBar is the same as in the previous chapter. Next, we create a QLabel and tell it to align the contents centered (both vertically and horizontally). The connect() statements have also been taken from the previous chapter.

    const char *LCDRange::text() const
    {
        return label->text();
    }

This function returns the label text.

    void LCDRange::setText( const char *s )
    {
        label->setText( s );
    }

This function sets the label text.

    void LCDRange::resizeEvent( QResizeEvent * )
    {
        lcd->resize( width(), height() - 41 - 5 );
        sBar->setGeometry( 0, lcd->height() + 5, width(), 16 );
        label->setGeometry( 0, lcd->height() + 16 + 5, width(), 20 );
    }

This event handler is similar to the one in the previous chapter, except that we also must make room for the label. The number 41 in the lcd resize statment is the height of the scroll bar plus the height of the label plus the spacing between them (16+20+5).

cannon.h

The CannonField now has two new signals: hit() and missed(). In addition it contains a target.

        void  newTarget();

This slot creates a target at a new position.

    signals:
        void  hit();
        void  missed();

The hit() signal is emitted when a shot hits the target. The missed() signal is emitted when the shot moves beyond the right or bottom edge of the widget (i.e. it has not hit the target).

        void  paintTarget( QPainter * );

This private function paints the target.

        QRect targetRect() const;

This private function returns the enclosing rectangle of the target.

        QPoint target;

This private variable contains the center point of the target.

cannon.cpp

    #include <qdatetime.h>

We include the QDate, QTime and QDateTime class definitions.

    #include <stdlib.h>

We include the stdlib library because we need the rand() function.

        newTarget();

This line has been added to the constructor. It creates a "random" position for the target. In fact, the newTarget() function will try to paint the target. Since we are in a constructor, the CannonField widget is invisible. Qt guarantees that no harm is done when painting a hidden widget.

    void  CannonField::newTarget()
    {
        static bool first_time = TRUE;
        if ( first_time ) {
            first_time = FALSE;
            QTime midnight( 0, 0, 0 );
            srand( midnight.secsTo(QTime::currentTime()) );
        }
        erase( targetRect() );
        target = QPoint( 200 + rand() % 190,
                         10  + rand() % 255 );
        repaint( targetRect() );
    }

This private function creates a target center point at a new "random" position.

We use the rand() function to fetch random integers. The rand() function normally returns the same series of numbers each time you run a program. This would make the target appear at the same position every time. To avoid this, we must set a random seed the first time this function is called. The random seed must also be random in order to avoid equal random number series. The solution is to use the number of seconds that have passed since midnight as a pseudo-random value.

First we create a static bool local variable. A static variable like this one is guaranteed to keep its value between calls to the function.

The if test will only succeed the first time this function is called, because we set first_time to FALSE inside the if block.

Then we create the QTime object midnight which represents the time 00:00:00. Next, we fetch the number of seconds from midnight until now and use it as a random seed. See the documentation of QDate, QTime and QDateTime for more information.

Finally, we calculate the target's center point. We keep it withing the rectangle (x=200,y=35,width=190,height=255), (i.e. the possible x and y values are x = 200..390 and y = 35..290) in a coordinate system where we put y position 0 at the bottom edge of the widget and let y values increase upwards. x is as normal, with 0 at the left edge and with x values increasing to the right.

By experimentation, we have found this to always be in reach of the shot.

Note that rand() return a random integer >= 0.

    void CannonField::timerEvent( QTimerEvent * )
    {
        erase( shotRect() );
        timerCount++;
    
        QRect shotR = shotRect();

This part of the timer event has not changed from the previous chapter.

        if ( shotR.intersects( targetRect() ) ) {
            stopShooting();
            emit hit();     
            return;
        }

This if statement checks if the shot rectangle intersects the target rectangle. If it does, the shot has hit the target (ouchh!). We stop shooting and emit the hit() signal to tell the outside world that a target was destroyed and return.

Note that we could have created a new target on the spot, but since the CannonField is a component, we leave such decisions to the user of the component.

        if ( shotR.x() > width() || shotR.y() > height() ) {
            stopShooting();
            emit missed();
            return;
        }   

This if statement is the same as in the previous chapter, except that it now emits the missed() signal to tell the outside world about the failure.

        repaint( shotR, FALSE );
    }

No changes here.

Modifications in CannonField::paintEvent():

        if ( updateR.intersects( targetRect() ) )
            paintTarget( &p );

These two lines have been added to the paintEvent() to paint the target if necessary.

    void CannonField::paintTarget( QPainter *p )
    {
        p->setBrush( red );
        p->setPen( black );
        p->drawRect( targetRect() );
    }

This private function paints the target; a rectangle filled with red and with a black outline.

    QRect CannonField::targetRect() const
    {
        QRect r( 0, 0, 20, 10 );
        r.moveCenter( QPoint(target.x(),height() - 1 - target.y()) );
        return r;
    }

This private function returns the enclosing rectangle of the target. Remember from newTarget() that the target point uses y coordinate 0 at the bottom of the widget. We calculate the point in widget coordinates before we call moveCenter().

The reason we have chosen this coordinate mapping is to fix the distance between the target and the bottom of the widget. Remember that the widget can be resized by the user or the program at any time.

main.cpp

There are no new members in the MyWidget class, but we have slightly changed the constructor to set the new LCDRange text labels.

        angle  = new LCDRange( "ANGLE", this, "angle" );

We set the angle text label to "ANGLE".

        force  = new LCDRange( "FORCE", this, "force" );

We set the force text label to "FORCE".

Note that we have not changed the position or the size of the LCDRanges. The QLCDNumber in LCDRange will become a bit smaller to make room for the label. MyWidget doesn't care about that, because it uses LCDRange as a component and trust that it will show its value in the best way possible.

Behavior

The cannon can shoot at a target and a new target is automatically created when one has been hit.

Excercises

Make a cheat button that when pressed makes the CannonField display the shot trajectory for 5 seconds.

Make a moving target.

Make it possible to have several shots in the air at the same time. Hint: make a Shot object.

You may now go on to chapter thirteen.

[Previous tutorial] [Next tutorial] [Main tutorial page]


Copyright © 1998 Troll TechTrademarks
Qt version 1.42