Traduzione a cura di Fabrizio Angius [qtsolutions -A-T- gmx.net]
English TOC.

12. La tela (canvas)

Il modulo riguardante la tela, cioe' le classi QCanvas, QCanvasView, QCanvasItem con le varie sottoclassi, viene utilizzato per rappresentare grafici 2D. Questi grafici possono essere di ogni tipo, dal videogioco al grafico commerciale. La documentazione ufficiale afferma quanto segue:

La tela e' ottimizzata per un elevato numero di oggetti, in particolare per situazioni in cui solo una percentuale ridotta di elementi cambia in continuazione. Se i cambiamenti riguardano l'intera scena, dovreste considerare l'ipotesi di utilizzare una vostra sottoclasse di QScrollView.

Le classi del modulo Canvas utilizzano un design pattern classico, quello model - view. La classe QCanvas e' un documento contenente un certo numero di QCanvasItem. Ciascuna tela puo' essere visualizzata mediante uno o piu' QCanvasView. Questo significa che potete vedere una stessa scena da diversi punti di vista, con diversi livelli di zoom, angolazioni, ecc.

Le figure di base

Qt offre una serie di elementi per la tela. Alcuni di questi sono mostrati nell'esempio 12-2 qui sotto. Basta inserire il codice in un file .cpp in una cartella vuota ed eseguire qmake -project && qmake && make per generare un eseguibile. La figura 12-1 mostra il risultato.

#include <qapplication.h>
#include <qcanvas.h>

int main( int argc, char **argv )
{
  QApplication a( argc, argv );
  
  QCanvas *c = new QCanvas( 210, 75 );
  QCanvasView *cv = new QCanvasView( c );

  QCanvasRectangle *rect = new QCanvasRectangle( 10, 10, 40, 40, c );
  rect->setPen( Qt::black );
  rect->setBrush( Qt::red );
  rect->show();

  QCanvasText *t = new QCanvasText( "Rect", c );
  t->setX( 30 );
  t->setY( 55 );
  t->setTextFlags( Qt::AlignHCenter );
  t->show();
  
  QPointArray points( 3 );
  points.setPoint( 0, 20, 0 );
  points.setPoint( 1, 0, 40 );
  points.setPoint( 2, 40, 40 );
  
  QCanvasPolygon *poly = new QCanvasPolygon( c );
  poly->setPoints( points );
  poly->setX( 60 );
  poly->setY( 10 );
  poly->setBrush( Qt::blue );
  poly->show();
  
  t = new QCanvasText( "Poly", c );
  t->setX( 80 );
  t->setY( 55 );
  t->setTextFlags( Qt::AlignHCenter );
  t->show();
  
  QCanvasLine *line = new QCanvasLine( c );
  line->setPoints( 110, 10, 150, 50 );
  line->setPen( QPen( Qt::green, 4 ) );
  line->show();
  
  t = new QCanvasText( "Line", c );
  t->setX( 130 );
  t->setY( 55 );
  t->setTextFlags( Qt::AlignHCenter );
  t->show();
  
  QCanvasEllipse *elli = new QCanvasEllipse( 40, 40, 45*16, 225*16, c );
  elli->setX( 180 );
  elli->setY( 30 );
  elli->setBrush( Qt::cyan );
  elli->show();
  
  t = new QCanvasText( "Elli", c );
  t->setX( 180 );
  t->setY( 55 );
  t->setTextFlags( Qt::AlignHCenter );
  t->show();
  
  c->update();

  a.setMainWidget( cv );
  cv->show();
  
  return a.exec();
}

Esempio 12-1

L'esempio 12-1 in esecuzione

Figura 12-1 L'esempio 12-1 in esecuzione.

Le classi utilizzate in questo esempio sono QCanvasText, QCanvasRectangle, QCanvasPolygon, QCanvasLine e QCanvasEllipse. Le ultime quattro sono sottoclassi di QCanvasPolygonalItem che e' il miglior punto di partenza per la maggior parte degli oggetti personalizzati.

Quando si ha a che fare con un poligono, e' necessario posizionare la figura prodotta utilizzando i metodi setX e setY. Questi metodi traducono i punti, per cui il punto (20, 0) diventa (20+60, 0+10) = (80, 10) e cosi' via.

Quando si crea un'elisse, gli angoli vengono specificati in sedicesimi di grado, ecco perche' c'e' quel ...*16 nel costruttore. Le coordinate indicano il centro dell'elisse.

Prima di passare al prossimo esempio e creare un oggetto personalizzato, consiglio di dare uno sguardo veloce alla tabella 12-1. La classe per gli oggetti poligonali introduce le proprieta' pen e brush. Queste non sono comunque utilizzate da tutte le sottoclassi. I dettagli si possono vedere nella tabella ma anche nella documentazione ufficiale riguardante i singoli oggetti.

ClasseUtilizza BrushUtilizza Pen
QCanvasRectangleSiSi
QCanvasPolygonSiNo
QCanvasLineNoSi
QCanvasEllipseSiNo

Tabella 12-1

Una figura personalizzata

Il secondo esempio introduce uo oggetto per tela personalizzato, MyCanvasItem. L'esempio 12-2 mostra la classe che implementa MyCanvasItem e l'esempio 12-3 mostra i cambiamenti alla routine main dell'esempio precedente.

class MyCanvasItem : public QCanvasPolygonalItem
{
public:
  MyCanvasItem( int width, int height, QCanvas *c ) : QCanvasPolygonalItem( c )
  {
    m_width = width;
    m_height = height;
  }
  
  ~MyCanvasItem()
  {
    hide(); // Necessario per evitare segmentation fault - vedere la documentazione
  }
  
  QPointArray areaPoints() const
  {
    QPointArray res(8);
    
    res.setPoint( 0, QPoint( (int)x() + m_width/4, (int)y() ) );
    res.setPoint( 1, QPoint( (int)x() + 3*m_width/4, (int)y() ) );
    res.setPoint( 2, QPoint( (int)x() + m_width, (int)y() + 3*m_height/8 ) );
    res.setPoint( 3, QPoint( (int)x() + m_width, (int)y() + 5*m_height/8 ) );
    res.setPoint( 4, QPoint( (int)x() + 3*m_width/4, (int)y() + m_height ) );
    res.setPoint( 5, QPoint( (int)x() + m_width/4, (int)y() + m_height ) );
    res.setPoint( 6, QPoint( (int)x(), (int)y() + 5*m_height/8 ) );
    res.setPoint( 7, QPoint( (int)x(), (int)y() + 3*m_height/8 ) );
    
    return res;
  }
  
protected:
  void drawShape( QPainter &p )
  {
    p.drawEllipse( (int)x()+m_width/4, (int)y(), m_width/2, m_height );
    p.drawRect( (int)x(), (int)y()+3*m_height/8, m_width, m_height/4 );
  }
  
private:
  int m_width;
  int m_height;
};

Esempio 12-2

Il main cambia in due punti. Per prima cosa, cambiano le dimensioni della tela, inoltre sono state agiunte alcune righe per creare, posizionare ed etichettare l'oggetto personalizzato.

  ...  
  
  QCanvas *c = new QCanvas( 260, 75 );
  
  ...
  
  // Oggetto per tela personalizzato
  MyCanvasItem *my = new MyCanvasItem( 40, 40, c );
  my->setX( 210 );
  my->setY( 10 );
  my->setBrush( Qt::yellow );
  my->setPen( Qt::black );
  my->show();

  t = new QCanvasText( "Custom", c );
  t->setX( 230 );
  t->setY( 55 );
  t->setTextFlags( Qt::AlignHCenter );
  t->show();
  
  ...

Esempio 12-3

L'implementazione dell'oggetto e' suddivisa in cinque parti. Per iniziare c'e' il costruttore, che non necessita di commenti. I parametri iniziano con quelli specifici per la classe seguiti da quelli da passare alla superclasse. Anche il distruttore e' banale. La documentazione ufficiale per QCanvasPolygonalItem indica che il distruttore di una sottoclasse deve invocare il metodo hide.

La terza parte riguarda l'implementazione di areaPoints. L'area delimitata dai punti ritornati deve contenere l'intera figura pur restando il piu' piccola possibile. L'area ritornata dalla nostra implementazione e' mostrata nella figura 12-2 in blu; la figura appare invece colorata di giallo.

L'area e la figura

Figura 12-2 L'area e la figura.

La quarta parte e' il disegno vero e' proprio, eseguito all'interno del metodo drawShape. Il painter passato come parametro viene impostato con i pen e brush corretti e viene poi traslato in modo che le coordinate impostate con i metodi setX e setY corrispondano al punto (0, 0).

La parte finale riguarda le variabili private m_width e m_height. In una implementazione corretta, la larghezza dovrebbe essere modificabile mediante un metodo setWidth, leggibile mediante un metodo width e dichiarata come proprieta' utilizzando la macro Q_PROPERTY. Lo stesso vale ovviamente anche per l'altezza.

La finestra prodotta dal secondo esempio e' mostrata nella figura 12-3.

Una figura personalizzata

Figura 12-3 Una figura personalizzata.

Spostamenti

Nel terzo esempio cambieremo il punto di vista utilizzato per visualizzare la scena. Creeremo una versione personalizzata con una sottoclasse e la classe prodotta ci permettera' di muovere le figure utilizzando un dispositivo di puntamento.

L'esempio 12-4 mostra il codice dell'implementazione della vista personalizzata.

class MyCanvasView : public QCanvasView
{
public:
  MyCanvasView( QCanvas *c, QWidget *parent=0, const char *name=0, WFlags f=0 ) : QCanvasView( c, parent, name, f )
  {
    dragging = 0;
  }
  
protected:
  void contentsMousePressEvent( QMouseEvent *e )
  {
    QCanvasItemList il = canvas()->collisions( e->pos() );
    for( QCanvasItemList::Iterator it=il.begin(); it!=il.end(); ++it )
    {
      if( (*it)->rtti() != QCanvasText::RTTI )
      {
        dragging = (*it);
      
        xoffset = (int)(e->x() - dragging->x());
        yoffset = (int)(e->y() - dragging->y());
        
        return;
      }
    }
  }
  
  void contentsMouseReleaseEvent( QMouseEvent *e )
  {
    if( dragging )
    {
      dragging->setX( e->x() - xoffset );
      dragging->setY( e->y() - yoffset );

      dragging = 0;
      
      canvas()->update();
    }
  }
  
  void contentsMouseMoveEvent( QMouseEvent *e )
  {
    if( dragging )
    {
      dragging->setX( e->x() - xoffset );
      dragging->setY( e->y() - yoffset );

      canvas()->update();
    }
  }
  
private:
  QCanvasItem *dragging;
  int xoffset, yoffset;
};      

Esempio 12-4

Il costruttore imposta semplicemente il puntatore dragging a null. Questo ci indica che nessun oggetto e' momentaneamente trascinato.

Il metodo contentsMousePressEvent ci dice se un click e' l'inizio di un'operazione di trascinamento oppure no prendendo tutti gli oggetti sotto al puntatore del mouse e vedendo se sono istanze di QCanvasText. Lo scopo di questa limitazione e' di mostare come sia possibile utilizzare il metodo rtti per determinate che cosa si sta trascinando. Se si trova un oggetto corretto, lo si fa puntare da dragging e si impostano le variabili di offset in base alla differenza tra l'hot-spot (p.es. il punto indicato dalle sue coordinate x e y) e la posizione del mouse.

Quando si sposta il mouse si aggiorna la posizione dell'eventuale oggetto. Notate la chiamata a canvas()->update() necessaria per aggiornare la vista corrente. Quando viene rilasciato il pulsante del mouse, si aggiorna nuovamente la posizione dell'oggetto e si rimette a null il puntatore dragging. In questo modo completiamo l'operazione di spostamento.

Per poter utilizzare questa vista dal main e' necessario cambiare la linea in cui si crea la vista per la tela in modo da creare quella personalizzata.

Riassunto

Il codice sorgente per questo capitolo puo' essere scaricato da qui.

Letture consigliate

Qt Quarterly ha un articolo sull'implementazione di oggetti raggruppabili.

This is a part of digitalfanatics.org and is valid XHTML.