Qt logo


Chapter 14: Facing the Wall


Screenshot of tutorial fourteen

This is the final example, a complete game.

We add keyboard accelerators, introduce mouse events to CannonField. We put a frame around the CannonField and add a barrier (wall) to make the game more challenging.

Line by Line Walk-Through

cannon.h

The CannonField can now receive mouse events to make the user aim the barrel by clicking on it and dragging. CannonField also has a barrier wall.

    protected:
        void  timerEvent( QTimerEvent * );
        void  paintEvent( QPaintEvent * );
        void  mousePressEvent( QMouseEvent * );
        void  mouseMoveEvent( QMouseEvent * );
        void  mouseReleaseEvent( QMouseEvent * );

In addition to the familiar event handlers, CannonField implements three mouse event handlers. The names say it all.

        void  paintBarrier( QPainter * );

This private function paints the barrier wall.

        QRect barrierRect() const;

This private function returns the enclosing rectangle of the barrier.

        bool  barrelHit( const QPoint & ) const;

This private function checks if a point is inside the barrel of the cannon.

        bool barrelPressed;

This private variable is TRUE if the user has pressed the mouse on the barrel and not released it.

cannon.cpp

        barrelPressed = FALSE;

This line has been added to the constructor. Initially, the barrel is not pressed.

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

In the timerEvent() we now have to test if the shot has hit the barrier, in addition to checking if the shot has moved beyond the right or bottom edge of the widget.

        if ( updateR.intersects( barrierRect() ) )
            paintBarrier( &p );

In the paint event, we have added painting of the barrier.

    void CannonField::mousePressEvent( QMouseEvent *e )
    {
        if ( e->button() != LeftButton )
            return;
        if ( barrelHit( e->pos() ) )
            barrelPressed = TRUE;
    }

This is a Qt event handler. It is called when the user presses a mouse button when the mouse cursor is over the widget.

If the event was not generated by the left mouse button, we return immediately. Otherwise, we check if the position of the mouse cursor is within the cannon's barrel. If it is, we set barrelPressed to TRUE.

Notice that the pos() function returns a point in the widget's coordinate system.

    void CannonField::mouseMoveEvent( QMouseEvent *e )
    {
        if ( !barrelPressed )
            return;
        QPoint pnt = e->pos();
        if ( pnt.x() <= 0 )
            pnt.setX( 1 );
        if ( pnt.y() >= height() )
            pnt.setY( height() - 1 );
        double rad = atan( ((double) rect().bottom() - pnt.y()) /  pnt.x() );
        setAngle( qRound ( rad*180/3.14159265 ) );
    }

This is another Qt event handler. It is called when the user already has pressed the mouse button inside this widget and then moves/drags the mouse. You can make Qt send mouse move events even when no buttons are pressed, see QWidget::setMouseTracking().

This handler repositions the cannon's barrel according to the position of the mouse cursor.

First, if the barrel is not pressed, we return. Next, we fetch the mouse cursor's position. If the mouse cursor is to the left or below the widget, we adjust the point to be inside the widget.

Then we calculate the angle between the bottom edge of the widget and the imaginary line between the bottom left corner of the widget and the cursor position. Finally, we set the cannon's angle to the new value converted to degrees.

Remember that setAngle() redraws the cannon.

    void CannonField::mouseReleaseEvent( QMouseEvent *e )
    {
        if ( e->button() == LeftButton )
            barrelPressed = FALSE;
    }

This Qt event handler is called whenever the user releases a mouse button after it has been pressed inside this widget.

If the left button is released, we can be sure that the barrel is no longer pressed.

    void CannonField::paintBarrier( QPainter *p )
    {
        p->setBrush( yellow );
        p->setPen( black );
        p->drawRect( barrierRect() );
    }

This private function paints the barrier as a rectangle filled with yellow and with a black outline.

    QRect CannonField::barrierRect() const
    {
        return QRect( 145, height() - 100, 15, 100 );
    }

This private function returns the rectangle of the barrier. We fix the bottom edge of the barrier to the bottom edge of the widget.

    bool CannonField::barrelHit( const QPoint &p ) const
    {
        QWMatrix mtx;
        mtx.translate( 0, height() - 1 );
        mtx.rotate( -ang );
        mtx = mtx.invert();
        return barrel_rect.contains( mtx.map(p) );
    }

This function returns TRUE if the point is in the barrel, otherwise FALSE.

Here we use the class QWMatrix. It is defined in the header file qwmatrix.h, which is included by qpainter.h.

QWMatrix defines a coordinate system mapping. It can perform the same transformations as the QPainter.

Here we perform the same transformation steps as we do when drawing the barrel in the paintCannon() function. First we translate the coordinate system, then we rotate it.

Now we need to check if the point p (in widget coordinates) lies inside the barrel. To do this, we invert the transformation matrix. The inverted matrix performs the inverse transformation that we used when drawing the barrel. We map the point p using the inverted matrix and return TRUE if it is inside the original barrel rectangle.

gamebrd.h

The only change is that a frame has been added.

    class QFrame;

We name declare QFrame...

        QFrame      *frame;

and add a QFrame to this widget.

gamebrd.cpp

    #include <qaccel.h>

We include the class definition of QAccel.

        frame = new QFrame( this, "cannonFrame" );
        frame->setFrameStyle( QFrame::WinPanel | QFrame::Sunken );

We create and set up a frame. We want this frame around the CannonField widget. Since widgets cannot be transparent, we have to put the frame behind the CannonField. We create the frame before we create the CannonField. Qt guarantees that the last created widget always will be on top of the others.

The frame style is set to a sunken WinPanel. See the QFrame documentation for more information.

        QAccel *accel = new QAccel( this );
        accel->connectItem( accel->insertItem( Key_Space), this, SLOT(fire()) );
        accel->connectItem( accel->insertItem( Key_Q), qApp, SLOT(quit()) );

Here we create and set up an accelerator. An accelerator is an object that intercepts keyboard events to an application and calls slots if certain keys are pressed. This mechanism is also called shortcut keys. Note that an accelerator can be a child of a widget and will be destroyed when the parent is destroyed. QAccel is not a widget and has no visible effect on its parent.

We define two shortcut keys. We want the slot fire() to be called when the user presses space, and we want the application to quit when key "Q" is pressed. The Key_Space and Key_Q constants are defined in the header file qkeycode.h (included by qaccel.h). Also note that Key_Q signifies the Q key on the keyboard. The slot will be called when typing both "q" and "Q".

        frame->move( angle->x() + angle->width() + 10, angle->y() );
        cannonField->move( frame->x() + 2, frame->y() + 2 );

We set the initial position of the frame to the same position we previously used for the CannonField. Next, we set the position of the CannonField relative to the frame.

    void GameBoard::resizeEvent( QResizeEvent * )
    {
        frame->resize( width()  - frame->x() - 10,
                       height() - frame->y() - 10 );
        cannonField->resize( frame->width() - 4, frame->height() - 4 );
    }

We must now incorporate the frame widget in our resize handler. First we resize the frame exactly as we did with the CannonField in the previous chapter. Then we simply resize the CannonField relative to the frame.

Behavior

The cannon now shoots when you press space. You can also position the cannon's angle using the mouse. The barrier makes it a little more challenging to play the game. We also have a nice looking frame around the CannonField.

Excercises

Write a space invaders game and give it to the KDE project.

You may now go on to write your own Qt applications.

[Previous tutorial] [First tutorial] [Main tutorial page]


Copyright © 1998 Troll TechTrademarks
Qt version 1.42