主页 | 所有的类 | 主要的类 | 注释的类 | 分组的类 | 函数

Qt教程一 —— 第十四章:面对墙壁

Screenshot of tutorial fourteen

这是最后的例子:一个完整的游戏。

我们添加键盘快捷键并引入鼠标事件到CannonField。我们在CannonField周围放一个框架并添加一个障碍物(墙)使这个游戏更富有挑战性。

一行一行地解说

t14/cannon.h

CannonField现在可以接收鼠标事件,使得用户可以通过点击和拖拽炮筒来瞄准。CannonField也有一个障碍物的墙。

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

除了常见的事件处理器,CannonField实现了三个鼠标事件处理器。名称说明了一切。

        void  paintBarrier( QPainter * );

这个私有函数绘制了障碍物墙。

        QRect barrierRect() const;

这个私有寒暑返回封装障碍物的矩形。

        bool  barrelHit( const QPoint & ) const;

这个私有函数检查是否一个点在加农炮炮筒的内部。

        bool barrelPressed;

当用户在炮筒上点击鼠标并且没有放开的话,这个私有变量为TRUE。

t14/cannon.cpp

        barrelPressed = FALSE;

这一行被添加到构造函数中。最开始的时候,鼠标没有在炮筒上点击。

        } else if ( shotR.x() > width() || shotR.y() > height() ||
                    shotR.intersects(barrierRect()) ) {

现在我们有了一个障碍物,这样就有了三种射失的方法。我们来测试一下第三种。

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

这是一个Qt事件处理器。当鼠标指针在窗口部件上,用户按下鼠标的按键时,它被调用。

如果事件不是由鼠标左键产生的,我们立即返回。否则,我们检查鼠标指针是否在加农炮的炮筒内。如果是的,我们设置barrelPressed为TRUE。

注意pos()函数返回的是窗口部件坐标系统中的点。

    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 ) );
    }

这是另外一个Qt事件处理器。当用户已经在窗口部件中按下了鼠标按键并且移动/拖拽鼠标时,它被调用。(你可以让Qt在没有鼠标按键被按下的时候发送鼠标移动事件。请看QWidget::setMouseTracking()。)

这个处理器根据鼠标指针的位置重新配置加农炮的炮筒。

首先,如果炮筒没有被按下,我们返回。接下来,我们获得鼠标指针的位置。如果鼠标指针到了窗口部件的左面或者下面,我们调整鼠标指针使它返回到窗口部件中。

然后我们计算在鼠标指针和窗口部件的左下角所构成的虚构的线和窗口部件下边界的角度。最后,我们把加农炮的角度设置为我们新算出来的角度。

记住要用setAngle()来重新绘制加农炮。

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

只要用户释放鼠标按钮并且它是在窗口部件中按下的时候,这个Qt事件处理器就会被调用。

如果鼠标左键被释放,我们就会确认炮筒不再被按下了。

绘画事件包含了下述额外的两行:

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

paintBarrier()做的和paintShot()、paintTarget()和paintCannon()是同样的事情。

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

这个私有函数用一个黑色边界黄色填充的矩形作为障碍物。

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

这个私有函数返回障碍物的矩形。我们把障碍物的下边界和窗口部件的下边界放在了一起。

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

如果点在炮筒内,这个函数返回TRUE;否则它就返回FALSE。

这里我们使用QWMatrix类。它是在头文件qwmatrix.h中定义的,这个头文件被qpainter.h包含。

QWMatrix定义了一个坐标系统映射。它可以执行和QPainter中一样的转换。

这里我们实现同样的转换的步骤就和我们在paintCannon()函数中绘制炮筒的时候所作的一样。首先我们转换坐标系统,然后我们旋转它。

现在我们需要检查点p(在窗口部件坐标系统中)是否在炮筒内。为了做到这一点,我们倒置这个转换矩阵。倒置的矩阵就执行了我们在绘制炮筒时使用的倒置的转换。我们通过使用倒置矩阵来映射点p,并且如果它在初始的炮筒矩形内就返回TRUE。

t14/gamebrd.cpp

    #include <qaccel.h>

我们包含QAccel的类定义。

        QVBox *box = new QVBox( this, "cannonFrame" );
        box->setFrameStyle( QFrame::WinPanel | QFrame::Sunken );
        cannonField = new CannonField( box, "cannonField" );

我们创建并设置一个QVBox,设置它的框架风格,并在之后创建CannonField作为这个盒子的子对象。因为没有其它的东西在这个盒子里了,效果就是QVBox会在CannonField周围生成了一个框架。

        QAccel *accel = new QAccel( this );
        accel->connectItem( accel->insertItem( Key_Enter ),
                            this, SLOT(fire()) );
        accel->connectItem( accel->insertItem( Key_Return ),
                            this, SLOT(fire()) );

现在我们创建并设置一个加速键。加速键就是在应用程序中截取键盘事件并且如果特定的键被按下的时候调用相应的槽。这种机制也被称为快捷键。注意快捷键是窗口部件的子对象并且当窗口部件被销毁的时候销毁。QAccel不是窗口部件,并且在它的父对象中没有任何可见的效果。

我们定义两个快捷键。我们希望在Enter键被按下的时候调用fire()槽,在Ctrl+Q键被按下的时候,应用程序退出。因为Enter有时又被称为Return,并且有时键盘中两个键都有,所以我们让这两个键都调用fire()。

        accel->connectItem( accel->insertItem( CTRL+Key_Q ),
                            qApp, SLOT(quit()) );

并且之后我们设置Ctrl+Q和Alt+Q做同样的事情。一些人通常使用Ctrl+Q更多一些(并且无论如何它显示了如果做到它)。

CTRL、Key_Enter、Key_Return和Key_Q都是Qt提供的常量。它们实际上就是Qt::Key_Enter等等,但是实际上所有的类都继承了Qt这个命名空间类。

        QGridLayout *grid = new QGridLayout( this, 2, 2, 10 );
        grid->addWidget( quit, 0, 0 );
        grid->addWidget( box, 1, 1 );
        grid->setColStretch( 1, 10 );

我们放置boxQVBox),不是CannonField,在右下的单元格中。

行为

现在当你按下Enter的时候,加农炮就会发射。你也可以用鼠标来确定加农炮的角度。障碍物会使你在玩游戏的时候获得更多一点的挑战。我们还会在CannnonField周围看到一个好看的框架。

(请看编译来学习如何创建一个makefile和连编应用程序。)

练习

写一个空间入侵者的游戏。

(这个练习首先被Igor Rafienko作出来了。你可以下载他的游戏。)

新的练习是:写一个突围游戏。

最后的劝告:现在向前进,创造编程艺术的杰作

[上一章] [第一章] [教程一主页]


Copyright © 2002 Trolltech Trademarks 译者:Cavendish
Qt 3.0.5版