The Graphics View Framework

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:

  • QGraphicsRectItem: This is used to render rectangles
  • QGraphicsEllipseItem: This is used to render ellipses
  • QGraphicsTextItem: This is used to render text

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:

The Graphics View Framework

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:

  • The first section deletes any mobile units that have expired for some reason (such as their health dropping to 10). This works by looping over all the items in the scene and testing to see whether each is a 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.
  • The second section runs once every 20 passes through the 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;
  }
}

Note

For more documentation about the Graphics View Framework, see the Qt documentation at http://qt-project.org/doc/qt-4.8/graphicsview.html.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset