Qt logo


Chapter 11: Giving It a Shot


Screenshot of tutorial eleven

In this example, we introduce timer events to implement shooting.

Line by Line Walk-Through

cannon.h

The CannonField now has a shooting capabilities.

        bool  isShooting() const { return shooting; }

Returns TRUE if a shot is underway.

        void  shoot();

Calling this slot will make the cannon shoot if a shot is not in the air.

    protected:
        void  timerEvent( QTimerEvent * );

The timer event is the third type of widget event we encounter. You can make Qt call this event handler at regular intervals.

    private:
        void  stopShooting();

This private function stops a shot in mid-air.

        void  paintShot( QPainter * );

This private function paints the shot.

        QRect shotRect() const;

This private function returns the shot's enclosing rectangle if one is in the air, otherwise the returned rectangle is undefined.

        bool  shooting;

This private variable is TRUE when there's a shot in the air.

        int   timerCount;
        float shoot_ang;
        float shoot_f;
    };

These private variables contain information that describes the shot. The timerCount keeps track of the time passed since the shot was fired. The shoot_ang is the cannon angle and shoot_f is the cannon force when the shot was fired.

cannon.cpp

    #include <math.h>

We include the math library because we need the sin() and cos() functions.

    CannonField::CannonField( QWidget *parent, const char *name )
            : QWidget( parent, name )
    {
        ang           = 45;
        f             = 0;
        shooting      = FALSE;
        timerCount    = 0;
        shoot_ang     = 0;
        shoot_f       = 0;
    }

We initialize our new private variables.

    void CannonField::shoot()
    {
        if ( shooting )
            return;
        timerCount = 0;
        shoot_ang  = ang;
        shoot_f    = f;
        shooting   = TRUE;
        startTimer( 50 );
    }

This function shoots a shot unless a shot is in the air. The timerCount is reset to zero. The shoot_ang and shoot_f are set to the current cannon angle and force. shooting is set to TRUE to indicate that a shot is in the air. Finally we start timer events.

Calling startTimer() will make Qt send us timer events at regular intervals. In this case, we want timer events every 50 milliseconds.

    void CannonField::timerEvent( QTimerEvent * )
    {
        erase( shotRect() );
        timerCount++;
    
        QRect shotR = shotRect();
    
        if ( shotR.x() > width() || shotR.y() > height() ) {
            stopShooting();
            return;
        }   
        repaint( shotR, FALSE );
    }

The timerEvent() function is an event handler that receives timer events from Qt. In our case, it is called every 50 milliseconds when a shot is in the air.

First we erase the old shot using the QWidget::erase() function. Note that this is not strictly necessary the first time the timerEvent() is called after a shot has been fired (but it does no harm). The shotRect() function (explained below) returns the enclosing rectangle of the shot at the current position.

Then we increment the timerCount, which has the effect of moving the shot one step along its trajectory.

Next, we fetch the new shot rectangle.

If the shot has moved beyond the right or bottom edge of the widget, we stop shooting and return.

If not, we paint the new shot by repainting the portion of the widget that contains the shot. The FALSE argument indicates that the specified rectangle should not be erased before a paint event is sent to the widget.

    void CannonField::paintEvent( QPaintEvent *e )
    {
        QRect updateR = e->rect();
        QPainter p;
        p.begin( this );
    
        if ( updateR.intersects( cannonRect() ) )
            paintCannon( &p );
        if ( shooting &&  updateR.intersects( shotRect() ) )
            paintShot( &p );
        p.end();
    }

This paint event function is basically the same as the one in the previous chapter. The only difference is that we also paint the shot if necessary.

    void CannonField::stopShooting()
    {
        shooting = FALSE;
        killTimers();
    }

This private function stops a shot in mid-air. First it resets the shooting variable, then it kills all timer events for this widget. It is also possible to kill a single timer.

    void CannonField::paintShot( QPainter *p )
    {
        p->setBrush( black );
        p->setPen( NoPen );
        p->drawRect( shotRect() );
    }

This private function paints the shot by drawing a black filled rectangle.

    QRect CannonField::shotRect() const
    {
        const double gravity = 4;
    
        double time      = timerCount / 4.0;
        double velocity  = shoot_f; 
        double radians   = shoot_ang*3.14159265/180;
    
        double velx      = velocity*cos( radians );
        double vely      = velocity*sin( radians );
        double x0        = ( barrel_rect.right()  + 5 )*cos(radians);
        double y0        = ( barrel_rect.right()  + 5 )*sin(radians);
        double x         = x0 + velx*time;
        double y         = y0 + vely*time - 0.5*gravity*time*time;
    
        QRect r = QRect( 0, 0, 6, 6 );
        r.moveCenter( QPoint( qRound(x), height() - 1 - qRound(y) ) );
        return r;
    }

This private function calculates the center point of the shot and returns the enclosing rectangle of the shot. It uses the initial cannon force and angle in addition to timerCount, which increases as time passes.

The formula used is the classical Newtonian formula for frictionless movement in a gravity field. For simplicity, we've chosen to disregard any Einsteinian effects.

We calculate the center point in a coordinate system where y coordinates increase upwards. After we have calculated the center point, we construct a QRect with size 6x6 and move its center point to the point calculated above. In the same operation, we convert the point into the widget's coodinate system (see The Coordinate System).

The qRound() function is an inline function defined in qglobal.h (included by all other Qt header files). qRound() rounds a double to the closest integer.

main.cpp

    class MyWidget : public QWidget
    {
    public:
        MyWidget( QWidget *parent=0, const char *name=0 );
    protected:
        void resizeEvent( QResizeEvent * );
    private:
        QPushButton *quit;
        QPushButton *shoot;
        LCDRange    *angle;
        LCDRange    *force;
        CannonField *cannonField;
    };

The only addition is the shoot button.

        shoot = new QPushButton( "Shoot", this, "shoot" );
        shoot->setGeometry( 90, 10, 75, 30 );
        shoot->setFont( QFont( "Times", 18, QFont::Bold ) );

In the constructor we create and set up the shoot button exactly like we did with the quit button. Note that the first argument to the constructor is the button text and the third is the widget's name.

        connect( shoot, SIGNAL(clicked()), cannonField, SLOT(shoot()) );

Connects the clicked() signal of the shoot button to the shoot() slot of the CannonField.

Behavior

The cannon can shoot, but there's nothing to shoot at.

Excercises

Make the shot a filled circle (hint: QPainter::drawEllipse() ).

Change the color of the cannon when a shot is in the air.

You may now go on to chapter twelve.

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


Copyright © 1998 Troll TechTrademarks
Qt version 1.42