这是最后的例子:一个完整的游戏。
我们添加键盘快捷键并引入鼠标事件到CannonField
。我们在CannonField
周围放一个框架并添加一个障碍物(墙)使这个游戏更富有挑战性。
- lcdrange.h包含LCDRange类定义
- lcdrange.cpp包含LCDRange类实现
- cannon.h包含CannonField类定义
- cannon.cpp包含CannonField类实现
- gamebrd.h包含GameBoard类定义
- gamebrd.cpp包含GameBoard类实现
- main.cpp包含MyWidget和main
一行一行地解说
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。
cannon.cpp:
barrelPressed = false;
这一行被添加到构造函数中。最开始的时候,鼠标没有在炮筒上点击。
else if (shotR.x() > width() || shotR.y() > height() ||
shotR.intersects(barrierRect()))
现在我们有了一个障碍物,这样就有了三种射击的方法。我们来测试一下第三种。
void CannonField::mousePressEvent(QMouseEvent *e)
{
if (e->button() != Qt::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() == Qt::LeftButton)
barrelPressed = false;
}
只要用户释放鼠标按钮并且它是在窗口部件中按下的时候,这个Qt事件处理器就会被调用。
如果鼠标左键被释放,我们就会确认炮筒不再被按下了。
绘画事件包含了下述额外的两行:
if (updateR.intersects(barrierRect()))
paintBarrier(&p);
paintBarrier()做的和paintShot()、paintTarget()和paintCannon()是同样的事情。
void CannonField::paintBarrier(QPainter *p)
{
p->setBrush(Qt::yellow);
p->setPen(Qt::black);
p->drawRect(barrierRect());
}
这个私有函数用一个黑色边界黄色填充的矩形作为障碍物。
QRect CannonField::barrierRect() const
{
return QRect( 145, height() - 100, 15, 100 );
}
这个私有函数返回障碍物的矩形。我们把障碍物的下边界和窗口部件的下边界放在了一起。
bool CannonField::barrelHit(const QPoint &p) const
{
QTransform transform;
transform.translate(0, height() - 1);
transform.rotate(-ang);
transform = transform.inverted(); // QTransform 的逆矩阵操作
QPoint transformedPoint = transform.map(p); // 将点 p 转换到炮管坐标系
return barrelRect.contains(transformedPoint); // 检查转换后的点是否在 barrelRect 内
}
如果点在炮筒内,这个函数返回true;否则它就返回false。
这里我们使用QTransform
类。它是在头文件QTransform
中定义的,这个头文件被QPainter
包含。
QTransform
定义了一个坐标系统映射。它可以执行和QPainter
中一样的转换。
这里我们实现同样的转换的步骤就和我们在paintCannon()
函数中绘制炮筒的时候所作的一样。首先我们转换坐标系统,然后我们旋转它。
现在我们需要检查点p(在窗口部件坐标系统中)是否在炮筒内。为了做到这一点,我们倒置这个转换矩阵。倒置的矩阵就执行了我们在绘制炮筒时使用的倒置的转换。我们通过使用倒置矩阵来映射点p,并且如果它在初始的炮筒矩形内就返回true。
gameboard.cpp:
#include <QShortcut>
我们包含QShortcut
的类定义。
QVBoxLayout *boxLayout = new QVBoxLayout; // 创建 QVBoxLayout 对象
QFrame *boxFrame = new QFrame(this); //用于创建一个框架,可以用 `setFrameStyle` 设置样式。
boxFrame->setFrameStyle(QFrame::WinPanel | QFrame::Sunken); //设置一个框架样式,使其看起来像一个有阴影的面板。`QFrame::WinPanel` 和 `QFrame::Sunken` 是样式标志,使得框架有一个凹陷的效果。
boxFrame->setLayout(boxLayout); // 将 QVBoxLayout 设置到 QFrame 上
CannonField *cannonField = new CannonField(boxFrame);
boxLayout->addWidget(cannonField); // 添加 CannonField 到 QVBoxLayout
我们创建并设置一个QVBoxLayout
,然后设置它的框架风格,并在之后创建CannonField
作为这个盒子的子对象。因为没有其它的东西在这个盒子里了,效果就是QVBoxLayout
会在CannonField
周围生成了一个框架。
QShortcut *fireShortcut = new QShortcut(Qt::Key_Space, this);
connect(fireShortcut, SIGNAL(activated()), this, SLOT(fire()));
QShortcut *quitShortcut = new QShortcut(Qt::Key_Q, this);
connect(quitShortcut, SIGNAL(activated()), qApp, SLOT(quit()));
现在我们创建并设置一个加速键。加速键就是在应用程序中截取键盘事件并且如果特定的键被按下的时候调用相应的槽。这种机制也被称为快捷键。注意快捷键是窗口部件的子对象并且当窗口部件被销毁的时候销毁。QShortcut
不是窗口部件,并且在它的父对象中没有任何可见的效果。
我们定义两个快捷键。我们希望在Space
键被按下的时候调用fire()
槽,在Q键被按下的时候,应用程序退出。
Ctrl、Key_Enter、Key_Return和Key_Q都是Qt提供的常量。它们实际上就是Qt::Key_Enter等等,但是实际上所有的类都继承了Qt这个命名空间类。
QGridLayout *grid = new QGridLayout;
grid->addWidget(quit, 0, 0);
grid->addWidget(boxFrame, 1, 1);
grid->setColumnStretch(1, 10);
我们放置boxFrame
,不是CannonField
,在右下的单元格中。
行为
现在当你按下Space
的时候,加农炮就会发射。你也可以用鼠标来确定加农炮的角度。障碍物会使你在玩游戏的时候获得更多一点的挑战。我们还会在CannnonField
周围看到一个好看的框架。
练习
新的练习是:写一个突围游戏。
最后的劝告:现在向前进,创造编程艺术的杰作!
lcdrange.h
#ifndef LCDRANGE_H
#define LCDRANGE_H
#include <QWidget>
class QSlider;
class QLabel;
class LCDRange : public QWidget
{
Q_OBJECT
public:
LCDRange(QWidget *parent = 0);
LCDRange(const QString &s, QWidget *parent = 0);
int value() const;
const QString text() const;
public slots:
void setValue(int);
void setRange(int minVal, int maxVal);
void setText(const QString &s);
signals:
void valueChanged(int);
private:
void init();
QSlider *slider;
QLabel *label;
};
#endif // LCDRANGE_H
lcdrange.cpp
#include "lcdrange.h"
#include <QVBoxLayout>
#include <QLCDNumber>
#include <QSlider>
#include <QLabel>
LCDRange::LCDRange(QWidget *parent)
: QWidget(parent)
{
init();
}
LCDRange::LCDRange(const QString &s, QWidget *parent)
: QWidget(parent)
{
init();
setText(s);
}
void LCDRange::init()
{
QLCDNumber *lcd = new QLCDNumber(2);
slider = new QSlider(Qt::Horizontal);
slider->setRange(0, 99);
slider->setValue(0);
label = new QLabel;
label->setAlignment(Qt::AlignHCenter);
connect(slider, SIGNAL(valueChanged(int)), lcd, SLOT(display(int)));
connect(slider, SIGNAL(valueChanged(int)), SIGNAL(valueChanged(int)));
setFocusProxy(slider); //设置这个窗口部件的焦点为slider
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(lcd);
layout->addWidget(slider);
layout->addWidget(label);
setLayout(layout);
}
int LCDRange::value() const
{
return slider->value();
}
const QString LCDRange::text() const
{
return label->text();
}
void LCDRange::setValue(int value)
{
slider->setValue(value);
}
void LCDRange::setRange(int minVal, int maxVal)
{
if (minVal < 0 || maxVal > 99 || minVal > maxVal)
{
qWarning( "LCDRange::setRange(%d,%d)\n"
"\tRange must be 0~99\n"
"\tand minVal must not be greater than maxVal",
minVal, maxVal);
return;
}
slider->setRange(minVal, maxVal);
}
void LCDRange::setText(const QString &s)
{
label->setText(s);
}
cannon.h
#ifndef CANNON_H
#define CANNON_H
class QTimer;
#include <QWidget>
class CannonField : public QWidget
{
Q_OBJECT
public:
CannonField(QWidget *parent = 0);
int angle() const {return ang;}
int force() const {return f;}
bool gameOver() const {return gameEnded;}
bool isShooting() const;
QSize sizeHint() const;
public slots:
void setAngle(int degrees);
void setForce(int newton);
void shoot();
void newTarget();
void setGameOver();
void restartGame();
private slots:
void moveShot(); // 更新炮弹的位置
signals:
void hit();
void missed();
void angleChanged(int);
void forceChanged(int);
void canShoot(bool);
protected:
void paintEvent(QPaintEvent *); // 窗口刷新/重绘
void mousePressEvent(QMouseEvent *);
void mouseMoveEvent(QMouseEvent *);
void mouseReleaseEvent(QMouseEvent *);
private:
void paintShot(QPainter *);
void paintTarget(QPainter *);
void paintCannon(QPainter *);
void paintBarrier( QPainter *);
QRect cannonRect() const;
QRect shotRect() const;
QRect targetRect() const;
QRect barrierRect() const;
bool barrelHit(const QPoint &) const;
int ang;
int f;
int timerCount;
QTimer *autoShootTimer;
float shoot_ang;
float shoot_f;
QPoint target;
bool gameEnded;
bool barrelPressed;
};
#endif // CANNON_H
cannon.cpp
#include "cannon.h"
#include <QPaintEvent>
#include <QDateTime>
#include <QPainter>
#include <QPixmap>
#include <QTimer>
#include <stdlib.h>
#include <math.h>
CannonField::CannonField(QWidget *parent)
: QWidget(parent)
{
ang = 45;
f = 0;
timerCount = 0;
autoShootTimer = new QTimer;
connect(autoShootTimer, SIGNAL(timeout()), this, SLOT(moveShot()));
shoot_ang = 0;
shoot_f = 0;
target = QPoint(0, 0);
gameEnded = false;
barrelPressed = false;
setAutoFillBackground(true);
setPalette(QPalette(QColor(250, 250, 200)));
newTarget();
}
void CannonField::setAngle(int degrees)
{
if (degrees < 5)
degrees = 5;
if (degrees > 70)
degrees = 70;
if (ang == degrees)
return;
ang = degrees;
repaint(cannonRect());
emit angleChanged(ang);
}
void CannonField::setForce(int newton)
{
if (newton < 0)
newton = 0;
if (f == newton)
return;
f = newton;
emit forceChanged(f);
}
void CannonField::shoot()
{
if (isShooting())
return;
timerCount = 0;
shoot_ang = ang;
shoot_f = f;
autoShootTimer->start(50);
emit canShoot(false);
}
void CannonField::newTarget()
{
static bool first_time = true;
if (first_time)
{
first_time = false;
QTime midnight(0, 0, 0);
srand(midnight.secsTo(QTime::currentTime()));
}
QRegion r(targetRect());
target = QPoint(200 + rand() % 190, 10 + rand() % 255);
repaint(r.united(targetRect()));
emit canShoot(true);
}
void CannonField::setGameOver()
{
if (gameEnded)
return;
if (isShooting())
autoShootTimer->stop();
gameEnded = true;
repaint();
}
void CannonField::restartGame()
{
if (isShooting())
autoShootTimer->stop();
gameEnded = false;
repaint();
emit canShoot(true);
}
void CannonField::moveShot()
{
QRegion r(shotRect());
timerCount++;
QRect shotR = shotRect();
if (shotR.intersects(targetRect()))
{
autoShootTimer->stop();
emit hit();
}
else if (shotR.x() > width() || shotR.y() > height() ||
shotR.intersects(barrierRect()))
{
autoShootTimer->stop();
emit missed();
emit canShoot(true);
}
else
{
r = r.united(QRegion(shotR));
}
repaint(r);
}
void CannonField::mousePressEvent(QMouseEvent *e)
{
if (e->button() != Qt::LeftButton)
return;
if (barrelHit(e->pos()))
barrelPressed = true;
}
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));
}
//鼠标释放事件
void CannonField::mouseReleaseEvent(QMouseEvent *e)
{
if (e->button() == Qt::LeftButton)
barrelPressed = false;
}
void CannonField::paintEvent(QPaintEvent *e)
{
QRect updateR = e->rect();
QPainter p(this);
if (gameEnded)
{
p.setPen(Qt::black);
p.setFont(QFont("Courier", 48, QFont::Bold ));
p.drawText(rect(),Qt::AlignHCenter, "Game Over");
}
if (updateR.intersects(cannonRect()))
paintCannon(&p);
if (autoShootTimer->isActive() && updateR.intersects(shotRect()))
paintShot(&p);
if (updateR.intersects(targetRect()))
paintTarget(&p);
if (updateR.intersects(barrierRect()))
paintBarrier(&p);
}
void CannonField::paintShot(QPainter *p)
{
p->setBrush(Qt::black);
p->setPen(Qt::NoPen);
p->drawEllipse(shotRect()); // 绘制填充圆
}
void CannonField::paintTarget(QPainter *p)
{
p->setBrush(Qt::red);
p->setPen(Qt::NoPen);
p->drawRect(targetRect());
}
void CannonField::paintBarrier(QPainter *p)
{
p->setBrush(Qt::yellow);
p->setPen(Qt::black);
p->drawRect(barrierRect());
}
const QRect barrelRect(33, -4, 15, 8);
void CannonField::paintCannon(QPainter *p)
{
QRect cr = cannonRect();
QPixmap pix(cr.size());
pix.fill(Qt::transparent); //使用透明像素图
QPainter tmp(&pix);
tmp.setBrush(Qt::blue);
tmp.setPen(Qt::NoPen);
tmp.translate(0, pix.height() - 1);
tmp.drawPie(QRect(-35,-35, 70, 70), 0, 90*16);
tmp.rotate(-ang);
tmp.drawRect(barrelRect);
tmp.end();
p->drawPixmap(cr.topLeft(), pix);
}
QRect CannonField::cannonRect() const
{
QRect r(0, 0, 50, 50);
r.moveBottomLeft(rect().bottomLeft());
return r;
}
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 = (barrelRect.right() + 5) * cos(radians);
double y0 = (barrelRect.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;
}
QRect CannonField::targetRect() const
{
QRect r(0, 0, 20, 10);
r.moveCenter(QPoint(target.x(),height() - 1 - target.y()));
return r;
}
QRect CannonField::barrierRect() const
{
return QRect( 145, height() - 100, 15, 100 );
}
bool CannonField::barrelHit(const QPoint &p) const
{
QTransform transform;
transform.translate(0, height() - 1);
transform.rotate(-ang);
// QTransform 的逆矩阵操作
transform = transform.inverted();
// 将点 p 转换到炮管坐标系
QPoint transformedPoint = transform.map(p);
// 检查转换后的点是否在 barrelRect 内
return barrelRect.contains(transformedPoint);
}
bool CannonField::isShooting() const
{
return autoShootTimer->isActive();
}
QSize CannonField::sizeHint() const
{
return QSize( 400, 300 );
}
gameboard.h
#ifndef GAMEBOARD_H
#define GAMEBOARD_H
#include <QWidget>
#include "cannon.h"
class LCDRange;
class QLCDNumber;
class CannonField;
class QPushButton;
class GameBoard : public QWidget
{
Q_OBJECT
public:
explicit GameBoard(QWidget *parent = 0);
protected slots:
void fire();
void hit();
void missed();
void newGame();
private:
QLCDNumber *hits;
QLCDNumber *shotsLeft;
CannonField *cannonField;
};
#endif // GAMEBOARD_H
gameboard.cpp
#include "gameboard.h"
#include <QApplication>
#include <Qpushbutton>
#include <QVBoxLayout>
#include <QGridLayout>
#include <QLCDNumber>
#include <QShortcut>
#include <QLabel>
#include <QFrame>
#include <Qfont>
#include "lcdrange.h"
#include "cannon.h"
GameBoard::GameBoard(QWidget *parent)
: QWidget(parent)
{
QPushButton *quit = new QPushButton("Quit");
quit->setFont(QFont("Times", 18, QFont::Bold));
connect(quit, SIGNAL(clicked()), qApp, SLOT(quit()));
LCDRange *angle = new LCDRange("ANGLE");
angle->setRange(5, 70);
LCDRange *force = new LCDRange("FORCE");
force->setRange(10, 50);
QVBoxLayout *boxLayout = new QVBoxLayout; // 创建 QVBoxLayout 对象
QFrame *boxFrame = new QFrame(this); //用于创建一个框架,可以用 `setFrameStyle` 设置样式。
boxFrame->setFrameStyle(QFrame::WinPanel | QFrame::Sunken); //设置一个框架样式,使其看起来像一个有阴影的面板。`QFrame::WinPanel` 和 `QFrame::Sunken` 是样式标志,使得框架有一个凹陷的效果。
boxFrame->setLayout(boxLayout); // 将 QVBoxLayout 设置到 QFrame 上
cannonField = new CannonField(boxFrame);
boxLayout->addWidget(cannonField); // 添加 CannonField 到 QVBoxLayout
connect(angle, SIGNAL(valueChanged(int)), cannonField, SLOT(setAngle(int)));
connect(cannonField, SIGNAL(angleChanged(int)), angle, SLOT(setValue(int)));
connect(force, SIGNAL(valueChanged(int)), cannonField, SLOT(setForce(int)));
connect(cannonField, SIGNAL(forceChanged(int)), force, SLOT(setValue(int)));
connect(cannonField, SIGNAL(hit()), this, SLOT(hit()));
connect(cannonField, SIGNAL(missed()), this, SLOT(missed()));
QPushButton *shoot = new QPushButton("Shoot");
shoot->setFont(QFont("Times", 18, QFont::Bold));
connect(shoot, SIGNAL(clicked()), SLOT(fire()));
connect(cannonField, SIGNAL(canShoot(bool)), shoot, SLOT(setEnabled(bool)));
QPushButton *restart = new QPushButton("New Game");
restart->setFont(QFont("Times", 18, QFont::Bold));
connect(restart, SIGNAL(clicked()), this, SLOT(newGame()));
QShortcut *fireShortcut = new QShortcut(Qt::Key_Space, this);
connect(fireShortcut, SIGNAL(activated()), this, SLOT(fire()));
QShortcut *quitShortcut = new QShortcut(Qt::Key_Q, this);
connect(quitShortcut, SIGNAL(activated()), qApp, SLOT(quit()));
hits = new QLCDNumber(2);
shotsLeft = new QLCDNumber(2);
QLabel *hitsL = new QLabel("HITS");
QLabel *shotsLeftL = new QLabel("SHOTS LEFT");
QGridLayout *grid = new QGridLayout;
grid->addWidget(quit, 0, 0);
grid->addWidget(boxFrame, 1, 1);
grid->setColumnStretch(1, 10);
QVBoxLayout *leftBox = new QVBoxLayout;
grid->addLayout(leftBox, 1, 0);
leftBox->addWidget(angle);
leftBox->addWidget(force);
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);
QVBoxLayout *layout = new QVBoxLayout;
layout->addLayout(grid);
setLayout(layout);
angle->setValue(60);
force->setValue(25);
angle->setFocus();
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();
}
void GameBoard::missed()
{
if (shotsLeft->intValue() == 0)
cannonField->setGameOver();
}
void GameBoard::newGame()
{
shotsLeft->display(10);
hits->display(0);
cannonField->restartGame();
cannonField->newTarget();
}
main.cpp
#include <QApplication>
#include "gameboard.h"
int main(int argc, char **argv)
{
QApplication a(argc, argv);
GameBoard g;
g.setGeometry(100, 100, 500, 355);
g.show();
return a.exec();
}