2008年7月17日 星期四

QT-chap 13

這一章要學會QVBoxLayoutQHBoxLayout要怎麼使用

* t13/lcdrange.h包含LCDRange類別定義。
* t13/lcdrange.cpp包含LCDRange類別實現。
* t13/cannon.h包含CannonField類別定義。
* t13/cannon.cpp包含CannonField類別實現。
* t13/gamebrd.h包含GameBoard類別定義。
* t13/gamebrd.cpp包含GameBoard類別實現。
* t13/main.cpp包含MyWidget和main。

lcdrange.h

class LCDRange : public QWidget

注意,現在LCDRange不是繼承QVBox,而是繼承QWidget
而我們選用功能強大且有一點困難的QVBoxLayout來管理我們的視窗元件

lcdrange.cpp


#include <qlayout.h>

我們現在需要qlayout來管理我們的API(Application Programming Interface)


LCDRange::LCDRange( QWidget *parent, const char *name ) : QWidget( parent, name )

之前lcdrange是繼承QVBox,
現在改繼承QWidget,並且由layout管理視窗元件


QVBoxLayout * l = new QVBoxLayout( this );

我們建立一個QVBoxLayout,其所有的設定值都是用預設的,其parent是this(也就是QWidget)
用來管理此視窗的子元件


l->addWidget( lcd, 1 );

加入lcd進入視窗元件中,並且,它的stretch factor是1,表示會延展
若有一個是1另一個是2,則最後的長寬比是1比2


l->addWidget( slider );
l->addWidget( label );

再加入兩個元件,其延展因數是預設值的0
這個伸展控制是QVBoxLayout(和QHBoxLayout,和QGridLayout)所提供的,
而像QVBox這樣的類別卻不提供。在這種情況下我們讓QLCDNumber可以伸展,而其它的不可以。

cannon.h

新增了遊戲結束和新的函數


bool gameOver() const { return gameEnded; }

若遊戲結束則回傳TRUE,否則回傳FALSE


void setGameOver();
void restartGame();

這是兩個新增的函數,分別是設定「遊戲結束」與「遊戲重新開始」


void canShoot( bool );

這一個函數是用來控制shoot的按紐是否有效


bool gameEnded;

這一個非公開變數是用來記錄遊戲是否結束,TRUE為結束;FALSE則還沒有結束

cannon.cpp


gameEnded = FALSE;

一開始在建構子中,設定遊戲尚未結束


void CannonField::shoot()
{
  if ( isShooting() )
    return;
  timerCount = 0;
  shoot_ang = ang;
  shoot_f = f;
  autoShootTimer->start( 50 );
  emit canShoot( FALSE );
}

我們現在改用isShooting這一個函數代替isActive
並且由shoot觸發canShoot不能再發設子彈


void CannonField::setGameOver()
{
  if ( gameEnded )
    return;
  if ( isShooting() )
    autoShootTimer->stop();
  gameEnded = TRUE;
  repaint();
}

我們把游戲結束的這一個函數獨立出來,以便可以讓外面呼叫
當此副程式被呼叫時,
則停止timer的運行,把gameEnded的標誌設定為TRUE
並重畫


void CannonField::restartGame()
{
  if ( isShooting() )
    autoShootTimer->stop();
  gameEnded = FALSE;
  repaint();
  emit canShoot( TRUE );
}

如果炮彈還在空中,則停止timer計時,設定標誌為FALSE,並且重畫
就像hit()或miss()一樣,moveShot()同時也發射新的canShoot(TRUE)信號。


void CannonField::paintEvent( QPaintEvent *e )
{
  QRect updateR = e->rect();
  QPainter p( this );

  if ( gameEnded ) {
    p.setPen( black );
    p.setFont( QFont( "Courier", 48, QFont::Bold ) );
    p.drawText( rect(), AlignCenter, "Game Over" );
}

先把目前要更新的區域放在updateR中,
若遊戲結束則設定字型與要顯示的字放在中間


if ( updateR.intersects( cannonRect() ) )
  paintCannon( &p );
if ( isShooting() && updateR.intersects( shotRect() ) )
  paintShot( &p );
if ( !gameEnded && updateR.intersects( targetRect() ) )
  paintTarget( &p );
}

我們只有在炸彈在空中的時候,才會畫炮彈

gamebrd.h

這是一個新的文件,可以把它想成是最底下的一個白板,就包含左邊的輸入介面,與右邊的顯示介面


protected slots:
  void fire();
  void hit();
  void missed();
  void newGame();

我們增加了四個slot,只能讓內部使用與繼承的類別內部使用

gamebrd.cpp

這一個文件是新的,是用來做為MyWidget中的gameboard類別的實作


cannonField = new CannonField( this, "cannonField" );

在gamebrd類別的建構子中宣告一個CannonField的物件


connect( cannonField, SIGNAL(hit()),this, SLOT(hit()) );
connect( cannonField, SIGNAL(missed()),this, SLOT(missed()) );

CannonField類別中有一個signal hit(),且GameBoard類別中也有一個slot hit()
當CannonField觸發hit()時,會使得GameBoard類別中的hit()同時運作(使命中的lcd加1,且若炮彈數為0時,則遊戲結束)
同理
當CannonField類別中的missed()運作時,GameBoard類別中的missed()同時運作(,若炮彈為0時,則遊戲結束)


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

當shoot的pushbutton被點後,本物件的fire()會同時被執行(把炮彈送出去[cannonField->shoot()],並且把炮彈減一)


connect( cannonField, SIGNAL(canShoot(bool)),shoot, SLOT(setEnabled(bool)) );

用canShoot來決定shoot的pushbutton按鈕生效和失效


QPushButton *restart = new QPushButton( "&New Game", this, "newgame" );
restart->setFont( QFont( "Times", 18, QFont::Bold ) );
connect( restart, SIGNAL(clicked()), this, SLOT(newGame()) );

設定一個新按紐來決定否是重新一個新的遊戲


hits = new QLCDNumber( 2, this, "hits" );
shotsLeft = new QLCDNumber( 2, this, "shotsleft" );
QLabel *hitsL = new QLabel( "HITS", this, "hitsLabel" );
QLabel *shotsLeftL = new QLabel( "SHOTS LEFT", this, "shotsleftLabel" );

這裡我們建立四個視窗元件,這裡說明我們在GameBoard類別建構子中才宣告hitsL與shotsLeftL指標主要是因為,我們沒有要對它們做任何改變
當GameBoard視窗元件被銷毀的時候,Qt將會刪除它們,並且佈局類會適當地重新定義它們的大小。
換句話說,當我們宣告指標在標頭檔時,當視窗元件被銷毀時,Qt不會刪除它?!


QHBoxLayout *topBox = new QHBoxLayout;
grid->addLayout( topBox, 0, 1 );
topBox->addWidget( shoot );
topBox->addWidget( hits );
topBox->addWidget( hitsL );
topBox->addWidget( shotsLeft );
topBox->addWidget( shotsLeftL );
topBox->addStretch( 1 );
topBox->addWidget( restart );

我們在右上的補足了一些視窗元件,並且讓它展現出適當的大小
並且在new game的左邊增加了一個可以延展的元件,當視窗變大時,new game 與其它元件的距離就會變大


  newGame();
}

在完成視窗元件的配置之後,我們直接以newGame函數開始執行


void GameBoard::fire()
{
  if ( cannonField->gameOver() || cannonField->isShooting() )
    return;
  shotsLeft->display( shotsLeft->intValue() - 1 );
  cannonField->shoot();
}

此函數會使得炮彈被送出


void GameBoard::hit()
{
  hits->display( hits->intValue() + 1 );
  if ( shotsLeft->intValue() == 0 )
    cannonField->setGameOver();
  else
    cannonField->newTarget();
}

當CannonField確定擊中目標時,會通知此函數,並且增加lcd上顯示的命中次數
當炮彈沒有了,則遊戲結束了


void GameBoard::missed()
{
  if ( shotsLeft->intValue() == 0 )
    cannonField->setGameOver();
}

當CannonField確定沒有命中目標時,通知此函數,則減少lcd上顯示的剩餘炮彈數,
當炮彈沒有了,則遊戲結束了


void GameBoard::newGame()
{
  shotsLeft->display( 15 );
  hits->display( 0 );
  cannonField->restartGame();
  cannonField->newTarget();
}

當new game的按紐被按下去時,則把lcd上顯示的命中次數歸0與剩下炮彈個數恢復成15

main.cpp

刪掉一些宣告MyWidget的程式碼,並且把它移到gamebrd中

沒有留言:

張貼留言