what is this?

here i collect some thoughts on how to design a custom QAbstractItemView. In general this is documented well but in my case I want to make use of a QGraphicsScene/QGraphicsView as a QAbstractItemView, see [3] and [4]. the examples here are taken from [0]. you should download [0] and play with it while reading this document. also see my blog [6].

the basics

there are 3 standard views which can be used with a custom model out of the box, namely:

  • QTreeView : Inherits QAbstractItemView
  • QTableView : Inherits QAbstractItemView
  • QListView : Inherits QAbstractItemView

say you write your own model using a QAbstractItemModel and you want to have a simple ListView, then you can use the QListView without having to write a single line of code for the view code. all you need to do is to write the model!

but we need more than one of the 3 views above, we need a QGraphicsScene used as either of the three views above. i personally would like to call this a QGraphicsView (since the above naming scheme could be continued) but there is already a QGraphicsView so that won't work. so i will call it a QGraphicsSceneView instead. don't mix that up! so now we have:

  • QTreeView : Inherits QAbstractItemView
  • QTableView : Inherits QAbstractItemView
  • QListView : Inherits QAbstractItemView
  • QGraphicsSceneView : Inherits QAbstractItemView (but not directly, see below!)

questions

what is covered by a QGraphicsSceneView

QGraphicsSceneView is composed of several important objects:

  • ItemView : Inherits QAbstractItemView
  • GraphicsScene : Inherits QGraphicsScene

Both classes interact tightly. If the model wants all views to add a QModelIndex the ItemView receives a rowsInserted(..) call which is then forwarded to either a insertNode or insertConnection call depending on it's type (node or connection).

The GraphicsScene does provide support to access the model for all QGraphicsItem(s) with a generic extension data()/setData() provided by multiple inheritance. So every SceneItem_* does inherit another class called AbstractMVCGraphicsItem (might be renamed). This way each item can use data and setData to query or write data using the model.

All keyboard and mouse input which is not handled by an QGraphicsItem directly (say you have selected a GraphicsItem, then the keyboard input would be redirect to that items event handler). So if no item is focused all input is redirect to the GraphicsScene and this way we handle:

  • scene move (scrolling the scene using the right mouse button RMB
  • selecting items with a boundingbox
  • removing selected items
  • inserting items are a special position using keyboard shortcuts

ItemView

The ItemView is just a wrapper class to access the model. Most functions are not used at all but used functions including:

  • void ItemView::reset() {
  • void rowsInserted( const QModelIndex & parent, int start, int end );
  • void rowsAboutToBeRemoved( const QModelIndex & parent, int start, int end );
  • void dataChanged( const QModelIndex & , const QModelIndex & );

GraphicsScene

The GraphicsScene:

  • void insertNode();
  • void insertNode(QPoint pos);
  • void updateNode( QGraphicsItem* item );
  • void updateConnection( QGraphicsItem* item );
  • void keyPressEvent( QKeyEvent * keyEvent );
  • void mousePressEvent( QGraphicsSceneMouseEvent * event );
  • void mouseMoveEvent( QGraphicsSceneMouseEvent * event );
  • void mouseReleaseEvent( QGraphicsSceneMouseEvent * event );
  • QGraphicsItem* nodeInserted( QPersistentModelIndex item );
  • QGraphicsItem* connectionInserted( QPersistentModelIndex item );
  • void updateNode( QPersistentModelIndex item );
  • void updateConnection( QPersistentModelIndex item );
  • bool nodeRemoved( QPersistentModelIndex item );
  • bool connectionRemoved( QPersistentModelIndex item );
  • QVariant data( const QModelIndex &index, int role ) const;
  • bool setData( const QModelIndex & index, const QVariant & value, int role = Qt::EditRole );

node and node_connection are used in the data structure with properties, why?

the basic idea is to have one root node (i call it rootitem from now on) which is always there when using a QAbstractItemModel. all other items are childs of this rootitem. the next thing one can do is to append childs as for instance node items. finally nodes can have connections (outgoing and incoming connections). the outgoing connections are again childs of a 'node' item while incoming connections are no child items to any node.

so why properties?

i've been asking myself this question for quite some time now but i finally found the answer by reading the documentaion and thinking about pro/cons.

first let's have a look at the concept. say a node has a name and that name should be changed. this is what would be done:

  • 1. a view calls: model()->bool Model::setData( const QModelIndex & index, const QVariant & value, int role ) {
  • 2. now the model changes the data and emits a signal: emit dataChanged( index, index );
  • 3. finally all attached views receive the signal and they use QVariant Model::data( const QModelIndex &index, int role ) const {
    for all roles they are interested in (they can't know that only one tiny name changed, they simply update every aspect of the item)

now my idea was to associate every role with a QModelIndex so that we know exactly what to do. this however seems to be more complicated than i thought first since both node and node_connection have properties and the model navigates using functions as int Model::rowCount( const QModelIndex & sibling ) const { and if there were properties which would be handled as nodes or node_connections it would make navigation very complicated (if not impossible) but i haven't coded it (yet?) but currently i think it would be wrong to do it that way.

to sum up: node and node_connection are the only types which are used in the data structure. and both use a property system for their associated properties as colors/names for instance. one should NOT insert properties as QModelIndexes as this would complicate the 'data structure'-navigation.

hierarchy vs flat data structures

the datastructure on which the QAbstractItemModel relies forms a hierarchy of rootnode->node->node_connection, example:

rootnode 
         - node (node 1)
         |   |- node_connection (connection 1)
         |   |- node_connection (connection 2)
         |   |- node_connection (connection 3)
         - node (node 2)
         |   |- node_connection (connection 1)
         |   |- node_connection (connection 2)

and navigation is done via a structure of pointers. removing childs or adding childs is done via the model. YOU MUST NOT add new items from within the data structure since attached views won't notice it then.

in contrast a QGraphicsScene has a flat structure hosting all items in one container which can be queried using QList<QGraphicsItem *> QGraphicsScene::items () const. and it is worth noticing that you can't model fractal like structures (as [5]) with a QGraphicsSceneView since one property of fractal structures is you can always zoom in but in contrast a QGraphicsScene has only a limited subset of items. sure they can be zoomed but not to infinity.

so how to model a 'fractal like structure' then? one could use a QAbstractItemView instead but then you will loose all the convenient functions as item clicking and item moving which a QGraphicsScene has by default but on the other hand you are not limited to finite structures. maybe one could trick the QGraphicsScene with removing all old items on zoom (inserting new items afterwards). this has serious drawbacks as items which were removed aren't there anymore so they will loose focus. however this is not important for what i'm talking about here, i just wanted to set some bounds on what a QGraphicsSceneView can do and what it can't.

in general it makes sense to have a loose item coupling meaning that one QGraphicsItem (that are items in a QGraphicsScene and NOT items of a QGrarphicsModel) item can query other items but it must check if those items exist before doing so. for instance when a arrow is dran between two nodes we have to find the coordinates for the source node and also the coordinates of the destination node.

say someone moves one node, then we have to notify (on each pixel we move the node) the associated childs (which are of type node_connection) that they have to redraw themselves as well. of course node_connections are not drawn since they live in the data structure but their equivalents in the QGraphicsScene are!

init the view

next we have a look at how the QGraphicsSceneView is attached to a model. as any custom QGraphicsView we call reset() which then uses the always existing rootitem (that is accesed (via the model) using QModelIndex()) to query all the data:

    1  void ItemView::init() {
    2  // qDebug() << __PRETTY_FUNCTION__;
    3    for ( int i = 0; i < model->rowCount( QModelIndex() ); ++i ) {
    4  // qDebug() << "adding node i =" << i;
    5      QModelIndex item = model->index( i, 0, QModelIndex() );
    6      scene->nodeInserted( QPersistentModelIndex( item ) );
    7    }
    8    for ( int i = 0; i < model->rowCount( QModelIndex() ); ++i ) {
    9  // qDebug() << "adding connection to node i =" << i;
   10      QModelIndex item = model->index( i, 0, QModelIndex() );
   11      for ( int x = 0; x < model->rowCount( item ); ++x ) {
   12  // qDebug() << "adding connection x =" << x;
   13        QModelIndex citem = model->index( x, 0, item );
   14        scene->connectionInserted( QPersistentModelIndex( citem ) );
   15      }
   16    }
   17  // qDebug() << __FUNCTION__ << "END";
   18  }

changing data

a user would change data using a model call, say model()->setData(QModelIndex, Role) but in contrast this is not a 'function call' but a 'function callback'. the model calls this function belonging to the QGraphicsSceneView to notify of a change.

it is important to notice that an item can have several properties but since we don't know what exactly has changed (we only see that the item has somehow changed) we have to query for ALL possible changes and update the item properly.

    1  void ItemView::dataChanged( const QModelIndex & topLeft, const QModelIndex & bottomRight ) {
    2  // qDebug() << __FUNCTION__;
    3    QModelIndex tmpIndex = topLeft;
    4    do {
    5  // qDebug() << "dataChanged is now called()";
    6      switch (model->data( tmpIndex, customRole::TypeRole ).toInt()) {
    7        case ViewTreeItemType::NODE:
    8  // qDebug() << __FUNCTION__ << "Node modification";
    9          scene->updateNode( QPersistentModelIndex( tmpIndex ) );
   10          break;
   11        case ViewTreeItemType::NODE_CONNECTION:
   12  // qDebug() << __FUNCTION__ << "Connection modification";
   13          scene->updateConnection( QPersistentModelIndex( tmpIndex ) );
   14          break;
   15        default:
   16          qDebug() << __PRETTY_FUNCTION__ << " didn't understand what i should be doing";
   17          exit(0);
   18      }
   19      if (tmpIndex == bottomRight)
   20        break;
   21      tmpIndex = traverseTroughIndexes( tmpIndex );
   22    } while ( tmpIndex.isValid());
   23  }


adding/removing data

a user would add data using a model call, say model()->insertRows() but in contrast this is not a 'function call' but a 'function callback'. the model calls this function belonging to the QGraphicsSceneView to insert data into the view.


    1  void ItemView::rowsInserted( const QModelIndex & parent, int start, int end ) {
    2  // qDebug() << "rowsInserted in ItemView called: need to insert " << end - start + 1 << " item(s).";
    3    for ( int i = start; i <= end; ++i ) {
    4      QModelIndex item = model->index( i, 0, parent );
    5      if ( model->data( item, customRole::TypeRole ).toInt() == ViewTreeItemType::NODE )
    6        scene->nodeInserted( QPersistentModelIndex( item ) );
    7      else if ( model->data( item, customRole::TypeRole ).toInt() == ViewTreeItemType::NODE_CONNECTION )
    8        scene->connectionInserted( QPersistentModelIndex( item ) );
    9    }
   10  }
   11
   12  void ItemView::rowsAboutToBeRemoved( const QModelIndex & parent, int start, int end ) {
   13  // qDebug() << "rowsAboutToBeRemoved in ItemView called: need to remove " << end-start+1 << " item(s).";
   14    for ( int i = start; i <= end; ++i ) {
   15      QModelIndex item = model->index( i, 0, parent );
   16      if ( model->data( item, customRole::TypeRole ).toInt() == ViewTreeItemType::NODE )
   17        scene->nodeRemoved( QPersistentModelIndex( item ) );
   18      else if ( model->data( item, customRole::TypeRole ).toInt() == ViewTreeItemType::NODE_CONNECTION )
   19        scene->connectionRemoved( QPersistentModelIndex( item ) );
   20    }
   21  }

inserting data using the model

once i had this problem: items are inserted into the model using bool Model::insertRows( int row, int count, const QModelIndex & parent ) { and if you simply call this function to add a node_connection to a node item a default node_connection was synthesized and inserted. later i used setData to modify the items. this however meant that each attached view had to issue two calls per added item. and a more critical problem was that the 'standard' item didn't have any meaningful data resulting in confusing object properties (which were of course wrong).

currently i use this function instead: bool Model::insertRows( int row, int count, const QModelIndex & parent, QPoint pos ) { which let's me modify the data before a view queries it. this concept could be extended with a QVariant of values say:

bool Model::insertRows( int row, int count, const QModelIndex & parent, QVariant values ) {

this way one can use the QModelIndex to query what kind of item is actually inserted, which can be:

  • QModelIndex is invalid: then we insert a node (and the parent is the rootnode)
  • QModelIndex is a node: then we insert a node_connection
  • QModelIndex is a node_connection: then something is wrong

so we have basically three cases to handle but only two of them make sense since our data structure has only 3 hierarchy levels, that is rootnode->node->node_connection.

so say we have a 'case switch' to handle the first two cases. in this 'case switches' we could query the 'QVariant values' for values which we use to init the item as for instance (but not limited to):

  • item color
  • item position (x,y coordinates)
  • item name property (or any other property you might use)

doing so will circumvent the issue discussed above.

links

Powered by MediaWiki