Qt provides a separate view framework, the Graphics View Framework, to draw hundreds or thousands of relatively lightweight customized items at once. You will choose the Graphics View Framework if you're implementing your own widget set from scratch (although, you might want to consider Qt Quick for this as well), or if you have a large number of items to display on the screen at once, each with their own position and data. This is especially important for applications that process and display a great deal of data, such as geographic information systems or computer-aided design applications.
In the Graphics View Framework, Qt defines the scene, responsible for providing a fast interface to a large number of items. (If you remember our discussion of MVC from the previous chapter, you can think of the scene as the model for the view renderer.) The scene also distributes events to the items it contains and manages the state of individual items in the scene. QGraphicsScene
is the Qt class responsible for the implementation of the scene. You can think of QGraphicsScene
as a container of drawable items, each a subclass of QGraphicsItem
.
Your QGraphicsItem
subclass can be used to override the drawing and event handling for each item, and you can then add your custom items to your QGraphicsScene
class by calling the addItem
method QGraphicsScene
. QGraphicsScene
offers an items
method that returns a collection of items contained by or intersecting with a point, rectangle, a polygon, or a general vector path. Under the hood, QGraphicsScene
uses a binary space partitioning tree (see Wikipedia's article on BSP trees at http://en.wikipedia.org/wiki/Binary_space_partitioning) for very fast searching of the item hierarchy by position.
Within the scene are one or more QGraphicsItem
subclass instances representing graphical items in the scene; Qt defines some simple subclasses for rendering, but you'll probably need to create your own. Qt provides:
QGraphicsItem
provides an interface you can override in your subclass to manage the mouse and keyboard events, drag and drop, interface hierarchies, and collision detection. Each item resides in its own local coordinate system, and helper functions provide you with fast transformations between an item's coordinates and the scene's coordinates.
The View framework uses one or more QGraphicsView
instances to display the contents of a QGraphicsScene
class. You can attach several views to the same scene, each with their own translation and rotation to see different parts of the scene. The QGraphicsView
widget is a scroll area, so you can also hook scrollbars to the view and let the user scroll around the view. The view receives input from the keyboard and the mouse, generating scene events for the scene and dispatching those scene events to the scene, which then dispatches those same events to the items in the scene.
The Graphics View Framework is ideally suited to creating games, and in fact, Qt's sample source code is just that: a towers-and-spaceships sample application you can see at http://qt-project.org/wiki/Towers_lasers_and_spacecrafts_example. The game, if you will, is simple, and is played by the computer; the stationary towers shoot the oncoming moving space ships, as you see in the following screenshot:
Let's look at bits from this sample application to get a feel of how the Graphics View Framework actually works.
The core of the game is a game timer that updates the positions of mobile units; the application's entry point sets up the timer, QGraphicsView
, and a subclass of QGraphicsScene
that will be responsible for tracking the state:
#include <QtGui> #include "scene.h" #include "simpletower.h" int main(int argc, char **argv) { QApplication app(argc, argv); Scene scene; scene.setSceneRect(0,0,640,360); QGraphicsView view(&scene); QTimer timer; QObject::connect(&timer, SIGNAL(timeout()), &scene, SLOT(advance())); view.show(); timer.start(10); return app.exec(); }
The timer kicks over every 10 milliseconds and is connected to the scene's advance slot, responsible for advancing game's state. The QGraphicsView
class is the rendering window for the entire scene; it takes an instance of the Scene
object from which it's going to render. The application's main
function initializes the view, scene, and timer, starts the timer, and then passes control to Qt's event loop.
The Scene
class has two methods: its constructor, which creates some non-moving towers in the scene, and the advance
method, which advances the scene's one-time tick, triggered each time that the timer in the main
function elapses. Let's look at the constructor first:
#include "scene.h" #include "mobileunit.h" #include "simpletower.h" Scene::Scene() : QGraphicsScene() , m_TicTacTime(0) { SimpleTower * simpleTower = new SimpleTower(); simpleTower->setPos(200.0, 100.0); addItem(simpleTower); simpleTower = new SimpleTower(); simpleTower->setPos(200.0, 180.0); addItem(simpleTower); simpleTower = new SimpleTower(); simpleTower->setPos(200.0, 260.0); addItem(simpleTower); simpleTower = new SimpleTower(); simpleTower->setPos(250.0, 050.0); addItem(simpleTower); simpleTower = new SimpleTower(); simpleTower->setPos(250.0, 310.0); addItem(simpleTower); simpleTower = new SimpleTower(); simpleTower->setPos(300.0, 110.0); addItem(simpleTower); simpleTower = new SimpleTower(); simpleTower->setPos(300.0, 250.0); addItem(simpleTower); simpleTower = new SimpleTower(); simpleTower->setPos(350.0, 180.0); addItem(simpleTower); }
Pretty boring—it just creates instances of the static towers and sets their positions, adding each one to the scene with the addItem
method. Before we look at the SimpleTower
class, let's look at the Scene
class's advance
method:
void Scene::advance() { m_TicTacTime++; // delete killed objects QGraphicsItem *item=NULL; MobileUnit * unit=NULL; int i=0; while (i<items().count()) { item=items().at(i); unit=dynamic_cast<MobileUnit*>(item); if (( unit!=NULL) && (unit->isFinished()==true)) { removeItem(item); delete unit; } else ++i; } // Add new units every 20 tictacs if(m_TicTacTime % 20==0) { MobileUnit * mobileUnit= new MobileUnit(); qreal h=static_cast<qreal>( qrand() % static_cast<int>(height()) ); mobileUnit->setPos(width(), h); addItem(mobileUnit); } QGraphicsScene::advance(); update(); }
This method has two key sections:
MobileUnit
instance. If it is, the code tests its isFinished
function, and if it's true, removes the item from the scene and frees it.advance
method and creates a new MobileUnit
object, randomly placing it on the right-hand side of the display. Finally, the method calls the inherited advance method, which triggers an advance call to each item in the scene, followed by calling update
, which triggers a redraw of the scene.Let's look at the QGraphicsItem
subclass of SimpleTower
next. First, let's look at the SimpleTower
constructor:
#include <QPainter> #include <QGraphicsScene> #include "simpletower.h" #include "mobileunit.h" SimpleTower::SimpleTower() : QGraphicsRectItem() , m_DetectionDistance(100.0) , m_Time(0, 0) , m_ReloadTime(100) , m_ShootIsActive(false) , m_Target(NULL) , m_TowerImage(QImage(":/lightTower")) { setRect(-15.0, -15.0, 30.0, 30.0); m_Time.start(); }
The constructor sets the bounds for the tower and starts a time counter used to determine the interval between the times that the tower fires at oncoming ships.
QgraphicsItem
instances do their drawing in their paint
method; the paint
method takes the QPainter
pointer you'll use to render the item, along with a pointer to the rendering options for the item and the owning widget in the hierarchy. Here's the paint
method of SimpleTower
:
void SimpleTower::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { painter->drawImage(-15,-15,m_TowerImage); if ( (m_Target!=NULL) && (m_ShootIsActive) ) { // laser beam QPointF towerPoint = mapFromScene(pos()); QPointF target = mapFromScene(m_Target->pos()); painter->setPen(QPen(Qt::yellow,8.0,Qt::SolidLine)); painter->drawLine(towerPoint.x(), towerPoint.y(), target.x(), target.y()); painter->setPen(QPen(Qt::red,5.0,Qt::SolidLine)); painter->drawLine(towerPoint.x(), towerPoint.y(), target.x(), target.y()); painter->setPen(QPen(Qt::white,2.0,Qt::SolidLine)); painter->drawLine(towerPoint.x(), towerPoint.y(), target.x(), target.y()); m_ShootIsActive=false; } }
The paint
method has to draw two things: the tower itself, which is a static image loaded at construction time (drawn with drawImage
), and if the tower is shooting at a target, draws colored lines between the tower and the mobile unit targeted by the tower.
Next is the advance
method:
void SimpleTower::advance(int phase) { if (phase==0) { searchTarget(); if ( (m_Target!=NULL) && (m_Time.elapsed()> m_ReloadTime) ) shoot(); } }
Each time the scene advances, each tower searches for a target, and if one is selected, it shoots at the target. The scene graph invokes each item's advance
method twice for each advance, passing an integer, indicating whether the items in the scene are about to advance (indicated when the phase
argument is 0), or that the items in the scene have advanced (indicated when the phase
segment is 1).
The searchTarget
method looks for the closest target within the detection distance, and if it finds one, sets the tower's target pointer to the closest unit in range:
void SimpleTower::searchTarget() { m_Target=NULL; QList<QGraphicsItem* > itemList = scene()->items(); int i = itemList.count()-1; qreal dx, dy, sqrDist; qreal sqrDetectionDist = m_DetectionDistance * m_DetectionDistance; MobileUnit * unit=NULL; while( (i>=0) && (NULL==m_Target) ) { QGraphicsItem * item = itemList.at(i); unit = dynamic_cast<MobileUnit * >(item); if ( (unit!=NULL) && ( unit->lifePoints()>0 ) ) { dx = unit->x()-x(); dy = unit->y()-y(); sqrDist = dx*dx+dy*dy; if (sqrDist < sqrDetectionDist) m_Target=unit; } --i; } }
Note that we cache a pointer to the targeted unit and adjust its position, because in subsequent frames, the targeted unit will move. Finally, the shoot method, which simply sets the Boolean flag used by paint to indicate that the shooting graphic should be drawn, indicates to the target that it's been damaged. This restarts the timer used to track the time between subsequent shots taken by the timer:
void SimpleTower::shoot() { m_ShootIsActive=true; m_Target->touched(3); m_Time.restart(); }
Finally, let's look at the MobileUnit
class that renders the individual moving space ships in the scene. Firstly, we define the include
directives and then the constructor:
#include "mobileunit.h" #include <QPainter> #include <QGraphicsScene> #include <math.h> MobileUnit::MobileUnit() : QGraphicsRectItem() , m_LifePoints(10) , m_Alpha(0) , m_DirX(1.0) , m_DirY(0.0) , m_Speed(1.0) , m_IsFinished(false) , m_IsExploding(false) , m_ExplosionDuration(500) , m_RedExplosion(0.0, 0.0, 20.0, 0.0, 0.0) , m_Time(0, 0) , m_SpacecraftImage(QImage(":/spacecraft00") ) { m_Alpha= static_cast<qreal> (qrand()%90+60); qreal speed= static_cast<qreal> (qrand()%10-5); m_DirY=cos(m_Alpha/180.0*M_PI ); m_DirX=sin(m_Alpha/180.0*M_PI); m_Alpha= -m_Alpha + 180.0 ; m_Speed=1.0+speed*0.1; setRect(-10.0, -10.0, 20.0, 20.0); m_Time.start(); m_RedExplosion.setColorAt(0.0, Qt::white); m_RedExplosion.setColorAt(0.2, QColor(255, 255, 100, 255)); m_RedExplosion.setColorAt(0.4, QColor(255, 80, 0, 200)); m_RedExplosion.setColorAt(1.0, QColor(255, 255, 255, 0)); }
The constructor's a little more complex than that of the stationary units. It needs to set an initial heading and speed for the mobile unit. Then, it sets the bounds for the unit and a timer to control its own behavior. If the unit is disabled, it'll explode; we will draw the explosion with concentric circles in a radial gradient, so we need to set the colors at the various points in the gradient.
Next is the paint
method, which paints the unit or its explosion if it's been damaged:
void MobileUnit::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { painter->setPen(Qt::NoPen); if (!m_IsExploding) { painter->rotate(m_Alpha); painter->drawImage(-15,-14, m_SpacecraftImage); } else { painter->setBrush(QBrush(m_RedExplosion)); qreal explosionRadius= 8.0 + m_Time.elapsed() / 50; painter->drawEllipse(-explosionRadius, -explosionRadius, 2.0*explosionRadius, 2.0*explosionRadius); } }
This is pretty straightforward: if the unit isn't exploding, it just sets the rotation for the image to be drawn and draws the image; otherwise, it draws the circle explosion with the radial gradient brush we configured in the constructor.
After that is the
advance
method, which is responsible for moving the ship from one frame to the next, as well as tracking the state of an exploding ship:
void MobileUnit::advance(int phase) { if (phase==0) { qreal xx=x(); qreal yy=y(); if ( (xx<0.0) || (xx > scene()->width() ) ) { // rebound m_DirX=-m_DirX; m_Alpha=-m_Alpha; } if ( (yy<0.0) || (yy > scene()->height())) { // rebound m_DirY=-m_DirY; m_Alpha=180-m_Alpha; } if (m_IsExploding) { m_Speed*=0.98; // decrease speed if (m_Time.elapsed() > m_ExplosionDuration) m_IsFinished=true; // is dead } setPos(x()+m_DirX*m_Speed, y()+m_DirY*m_Speed); } }
For simplicity's sake, the advance
method causes items at the edge of the scene to rebound off of the margins by reversing the direction and orientation. If an item is exploding, it slows down, and if the elapsed time in the timer is longer than the explosion duration, the method sets a flag indicating that the item should be removed from the scene during the next scene advance. Finally, this method updates the position of the item by adding the product of the direction and the speed to each coordinate.
Finally, the
touched
method decrements the health points of the mobile unit by the indicated amount, and if the unit's health points go to zero, starts the explosion timer and sets the explosion flag:
void MobileUnit::touched (int hurtPoints) { m_LifePoints -= hurtPoints; // decrease life if (m_LifePoints<0) m_LifePoints=0; if (m_LifePoints==0) { m_Time.start(); m_IsExploding=true; } }
For more documentation about the Graphics View Framework, see the Qt documentation at http://qt-project.org/doc/qt-4.8/graphicsview.html.